Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add nextra/locales middleware which can be exported from root/middleware.{js,ts} file to detect and redirect to the user-selected language for i18n websites #3439

Merged
merged 6 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/large-feet-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'nextra': patch
---

add `nextra/locales` middleware which can be exported from `root-of-your-project/middleware.{js,ts}` file to detect and redirect to the user-selected language for i18n websites
27 changes: 25 additions & 2 deletions docs/pages/docs/guide/i18n.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,30 @@ i18n: [
]
```

</Steps>
## Automatically Detect and Redirect to User-Selected Language (Optional)

You can automatically detect the user's preferred language and redirect them to
the corresponding version of the site. To achieve this, create a `middleware.js`
file in the root of your project and export Nextra's middleware function from
`nextra/locales`:

```js filename="middleware.js" {1}
export { middleware } from 'nextra/locales'

export const config = {
// Matcher ignoring `/_next/` and `/api/`
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico|icon.svg|apple-icon.png|manifest).*)'
]
}
```

<Callout type="warning">
**Note**: This approach will not work for i18n sites that are statically
exported with `output: "export"` in `nextConfig`.
</Callout>

## Custom 404 Page
## Custom 404 Page (Optional)

In **Nextra 3**, it's not possible to create a `404.mdx` page for an i18n
website that uses a shared theme layout. However, you can implement a `404.jsx`
Expand All @@ -67,3 +88,5 @@ In **Nextra 4**, you can have a custom `not-found.jsx` with translations for an
i18n website that uses a shared theme layout. For guidance on implementing this,
you can check out the
[SWR i18n example](https://github.com/shuding/nextra/blob/c9d0ffc8687644401412b8adc34af220cccddf82/examples/swr-site/app/%5Blang%5D/not-found.ts).

</Steps>
8 changes: 8 additions & 0 deletions examples/swr-site/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export { middleware } from 'nextra/locales'

export const config = {
// Matcher ignoring `/_next/` and `/api/`
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico|icon.svg|apple-icon.png|manifest).*)'
]
}
5 changes: 0 additions & 5 deletions examples/swr-site/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,6 @@ export default withBundleAnalyzer(
source: '/examples',
destination: '/examples/basic',
statusCode: 302
},
{
source: '/',
destination: '/en',
permanent: true
}
],
reactStrictMode: true
Expand Down
10 changes: 10 additions & 0 deletions packages/nextra/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
"import": "./dist/server/schemas.js",
"types": "./dist/server/schemas.d.ts"
},
"./locales": {
"import": "./dist/server/locales.js",
"types": "./dist/server/locales.d.ts"
},
"./fetch-filepaths-from-github": {
"import": "./dist/server/fetch-filepaths-from-github.js",
"types": "./dist/server/fetch-filepaths-from-github.d.ts"
Expand Down Expand Up @@ -74,6 +78,9 @@
"schemas": [
"./dist/server/schemas.d.ts"
],
"locales": [
"./dist/server/locales.d.ts"
],
"catch-all": [
"./dist/client/catch-all.d.ts"
],
Expand Down Expand Up @@ -120,6 +127,7 @@
"react-dom": ">=18"
},
"dependencies": {
"@formatjs/intl-localematcher": "^0.5.4",
"@headlessui/react": "^2.1.2",
"@mdx-js/mdx": "^3.0.0",
"@mdx-js/react": "^3.0.0",
Expand All @@ -136,6 +144,7 @@
"gray-matter": "^4.0.3",
"hast-util-to-estree": "^3.1.0",
"katex": "^0.16.9",
"negotiator": "^0.6.3",
"p-limit": "^6.0.0",
"rehype-katex": "^7.0.0",
"rehype-pretty-code": "0.14.0",
Expand All @@ -161,6 +170,7 @@
"@types/graceful-fs": "^4.1.9",
"@types/hast": "^3.0.4",
"@types/mdast": "^4.0.4",
"@types/negotiator": "^0.6.3",
"@types/react": "^18.3.3",
"@types/webpack": "^5.28.5",
"@vitejs/plugin-react": "^4.3.1",
Expand Down
3 changes: 2 additions & 1 deletion packages/nextra/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ const nextra: Nextra = nextraConfig => {
...nextConfig.env,
...(hasI18n && {
NEXTRA_DEFAULT_LOCALE:
nextConfig.i18n?.defaultLocale || DEFAULT_LOCALE
nextConfig.i18n?.defaultLocale || DEFAULT_LOCALE,
NEXTRA_LOCALES: JSON.stringify(nextConfig.i18n?.locales)
}),
NEXTRA_SEARCH: String(!!loaderOptions.search)
},
Expand Down
50 changes: 50 additions & 0 deletions packages/nextra/src/server/locales.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { match as matchLocale } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const locales = JSON.parse(process.env.NEXTRA_LOCALES!) as string[]

const defaultLocale = process.env.NEXTRA_DEFAULT_LOCALE!

const HAS_LOCALE_RE = new RegExp(`^\\/(${locales.join('|')})(\\/|$)`)

const COOKIE_NAME = 'NEXT_LOCALE'

function getHeadersLocale(request: NextRequest): string {
const headers = Object.fromEntries(
// @ts-expect-error -- this works
request.headers.entries()
)

// Use negotiator and intl-localematcher to get best locale
const languages = new Negotiator({ headers }).languages(locales)
const locale = matchLocale(languages, locales, defaultLocale)

return locale
}

export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl

// Check if there is any supported locale in the pathname
const pathnameHasLocale = HAS_LOCALE_RE.test(pathname)
const cookieLocale = request.cookies.get(COOKIE_NAME)?.value

// Redirect if there is no locale
if (!pathnameHasLocale) {
const locale = cookieLocale || getHeadersLocale(request)

// e.g. incoming request is /products
// The new URL is now /en-US/products
return NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url))
}

const requestLocale = pathname.split('/', 2)[1]

if (requestLocale !== cookieLocale) {
const response = NextResponse.next()
response.cookies.set(COOKIE_NAME, requestLocale)
return response
}
}
26 changes: 26 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.