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

feat: Add built-in pathname localization #426

Merged
merged 40 commits into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
bdbca1c
feat: Add built-in pathname localization
amannn Jul 21, 2023
b9e05a4
Update example
amannn Jul 21, 2023
f4dd8fa
More redirects
amannn Jul 21, 2023
c8c3339
No umlauts
amannn Jul 21, 2023
a85254c
Merge branch 'feat/402-pathname-localization' into feat/next-13-rsc-p…
amannn Jul 26, 2023
2c25935
Fix tests
amannn Jul 26, 2023
96bdb5d
No const type argument for now
amannn Jul 26, 2023
1513e03
PROGRESS
amannn Jul 28, 2023
88e908c
Migrate advanced example
amannn Jul 28, 2023
90d1452
Docs
amannn Jul 28, 2023
3704dd7
Small fix
amannn Jul 28, 2023
9723080
Improve docs and convention
amannn Jul 31, 2023
d76904e
Upgrade ESLint
amannn Jul 31, 2023
6b4037c
Handle catchall
amannn Jul 31, 2023
b50d82a
Strict params WIP
amannn Aug 1, 2023
b7c3158
Strict params
amannn Aug 1, 2023
7d0714c
Fix types
amannn Aug 1, 2023
524e996
Catch-all segments
amannn Aug 1, 2023
c6f34f9
Fix test
amannn Aug 2, 2023
976fc59
Support optional catchall segments
amannn Aug 2, 2023
0113c9c
Add `createSharedPathnamesNavigation`
amannn Aug 2, 2023
4e975a8
Refactor WIP
amannn Aug 10, 2023
4e57fe6
Fix TS error
amannn Aug 10, 2023
66493f3
Fix test
amannn Aug 10, 2023
a37dca0
Alternate links ✅
amannn Aug 10, 2023
3135a3f
Add tests for `localePrefix: 'never'`
amannn Aug 14, 2023
9135ba4
Fix middleware import
amannn Aug 14, 2023
e05c6bd
Replace useLocale to work without provider
amannn Aug 17, 2023
e3376cb
Resolve todos, fix tests
amannn Aug 17, 2023
ebcfd56
Add test for using a different internal pathname than the default locale
amannn Aug 17, 2023
dce64cd
Handle params in redirect and add more tests
amannn Aug 17, 2023
0e7f42e
Resolve remaining todo and update comments
amannn Aug 17, 2023
9326127
Support localized pathnames with domain-based config
amannn Aug 22, 2023
e0cb09d
Merge remote-tracking branch 'origin/feat/next-13-rsc' into feat/next…
amannn Aug 24, 2023
cfa1b02
Merge branch 'feat/next-13-rsc' into feat/next-13-rsc-pathname-locali…
amannn Aug 24, 2023
9439940
Support search params
amannn Aug 24, 2023
d598e54
Move `params` of `Link` to `href` for consistency
amannn Aug 24, 2023
7e605fa
Fix wrapping of link
amannn Aug 24, 2023
846115c
Minor wording fixes [skip ci]
amannn Aug 25, 2023
f3046c7
Un-export `createSharedPathnamesNavigation`
amannn Aug 25, 2023
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
3 changes: 1 addition & 2 deletions docs/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
require('eslint-config-molindo/setupPlugins');

module.exports = {
extends: [
'molindo/typescript',
'molindo/react',
'molindo/tailwind',
'plugin:@next/next/recommended'
],
env: {
Expand Down
2 changes: 1 addition & 1 deletion docs/components/Callout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default function Callout({
)}
>
<div
className="select-none text-xl ltr:pl-3 ltr:pr-2 rtl:pr-3 rtl:pl-2"
className="select-none text-xl ltr:pl-3 ltr:pr-2 rtl:pl-2 rtl:pr-3"
style={{
fontFamily: '"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"'
}}
Expand Down
4 changes: 2 additions & 2 deletions docs/components/CodeSnippets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,8 @@ function typeSafe() {
style={{color: 'var(--shiki-token-string-expression)'}}
>
{"''"}
<div className="absolute top-0 left-2 h-full border-l-[1.5px] border-slate-400" />
<div className="absolute top-[calc(100%+2px)] left-2 min-w-[8rem] rounded-sm border border-slate-300 bg-slate-100 text-slate-900 dark:border-slate-700 dark:bg-slate-800 dark:text-white">
<div className="absolute left-2 top-0 h-full border-l-[1.5px] border-slate-400" />
<div className="absolute left-2 top-[calc(100%+2px)] min-w-[8rem] rounded-sm border border-slate-300 bg-slate-100 text-slate-900 dark:border-slate-700 dark:bg-slate-800 dark:text-white">
<div className="bg-sky-100 p-1 dark:bg-slate-600">title</div>
<div className="p-1">followers</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion docs/components/CommunityLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default function CommunityLink({
<div className="mt-2">
{type && (
<Chip
className="mr-2 -translate-y-[1px]"
className="mr-2 translate-y-[-1px]"
color={({article: 'green', video: 'yellow'} as const)[type]}
>
{{article: 'Article', video: 'Video'}[type]}
Expand Down
2 changes: 1 addition & 1 deletion docs/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default function Footer() {

return (
<div className="border-t border-slate-200 bg-slate-100 dark:border-t-slate-800 dark:bg-transparent">
<div className="mx-auto max-w-[90rem] py-2 px-4 md:flex md:justify-between ">
<div className="mx-auto max-w-[90rem] px-4 py-2 md:flex md:justify-between ">
<div>
<FooterLink href="/docs">Docs</FooterLink>
<FooterSeparator />
Expand Down
4 changes: 2 additions & 2 deletions docs/components/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export default function Hero({
}: Props) {
return (
<div className="dark overflow-hidden">
<div className="relative max-w-full overflow-hidden bg-slate-850 py-16 sm:px-2 lg:py-40 lg:px-0">
<div className="absolute top-0 left-0 h-[20500px] w-[20500px] -translate-x-[47.5%] rounded-full bg-gradient-to-b from-slate-900 via-cyan-500 md:top-1" />
<div className="relative max-w-full overflow-hidden bg-slate-850 py-16 sm:px-2 lg:px-0 lg:py-40">
<div className="absolute left-0 top-0 h-[20500px] w-[20500px] translate-x-[-47.5%] rounded-full bg-gradient-to-b from-slate-900 via-cyan-500 md:top-1" />
<Wrapper>
<div className="flex flex-col gap-16 xl:flex-row xl:items-center xl:justify-between">
<div className="max-w-2xl">
Expand Down
2 changes: 1 addition & 1 deletion docs/components/HeroCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function Tab({
className={clsx(
'flex items-center rounded-md px-4 py-2 text-sm font-medium transition-colors',
active
? 'bg-slate-800 text-sky-100/70 text-white'
? 'bg-slate-800 text-white'
: 'bg-slate-800/40 text-slate-500 hover:bg-slate-800'
)}
onClick={onClick}
Expand Down
2 changes: 1 addition & 1 deletion docs/components/LinkButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default function LinkButton({variant = 'primary', ...rest}: Props) {
return (
<Link
className={clsx(
'inline-block rounded-full py-2 px-4 text-base font-semibold transition-colors lg:py-4 lg:px-8',
'inline-block rounded-full px-4 py-2 text-base font-semibold transition-colors lg:px-8 lg:py-4',
variant === 'primary'
? 'bg-slate-800 text-white hover:bg-slate-700 dark:bg-primary dark:text-slate-900 dark:hover:bg-sky-200'
: 'bg-slate-200 text-slate-700 dark:bg-slate-800 dark:text-white/90 dark:hover:bg-slate-700'
Expand Down
6 changes: 3 additions & 3 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@
"nextra-theme-docs": "^2.8.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.2.4"
"tailwindcss": "^3.3.2"
},
"devDependencies": {
"@types/node": "^20.1.2",
"@types/react": "^18.2.5",
"autoprefixer": "^10.4.0",
"eslint": "^8.39.0",
"eslint-config-molindo": "^6.0.0",
"eslint": "^8.46.0",
"eslint-config-molindo": "7.0.0-alpha.7",
"eslint-config-next": "^13.4.0",
"next-sitemap": "^4.0.7",
"prettier-plugin-tailwindcss": "^0.2.3",
Expand Down
6 changes: 4 additions & 2 deletions docs/pages/docs/routing/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import {Card} from 'nextra-theme-docs';

# Internationalized routing

With the introduction of the App Router, Next.js no longer provides integrated i18n routing. To fill in the gap, `next-intl` provides the two necessary pieces:
When you provide content in multiple languages, you want to make the content available under distinct URLs (e.g. `ca.example.com/en/about`). `next-intl` provides the building blocks to set up internatinoalized routing as well as the navigation APIs to enable you to link between pages.

<div className="mt-8 flex flex-col gap-4 md:w-1/2">
<Card
arrow
icon={<BoltIcon />}
title="Internationalized routing middleware"
title="Routing middleware"
href="/docs/routing/middleware"
/>
<Card
Expand All @@ -19,3 +19,5 @@ With the introduction of the App Router, Next.js no longer provides integrated i
href="/docs/routing/navigation"
/>
</div>

Note that these features are only relevant if you use the App Router. If you're using [`next-intl` with the Pages Router](/docs/getting-started/pages-router), you can use the [built-in capabilities from Next.js](https://nextjs.org/docs/pages/building-your-application/routing/internationalization).
70 changes: 51 additions & 19 deletions docs/pages/docs/routing/middleware.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export const config = {
};
```

In addition to handling i18n routing, the middleware sets [the `link` header](https://developers.google.com/search/docs/specialty/international/localized-versions#http) to inform search engines that your content is available in different languages.

## Strategies

There are two strategies for detecting the locale:
Expand All @@ -31,7 +33,7 @@ There are two strategies for detecting the locale:

Once a locale is detected, it will be saved in a cookie.

### Prefix-based routing (default) [#prefix-based-routing]
### Strategy 1: Prefix-based routing (default) [#prefix-based-routing]

Since your pages are nested within a `[locale]` folder, all routes are prefixed with one of your supported locales (e.g. `/de/about`). To keep the URL short, requests for the default locale are rewritten internally to work without a locale prefix.

Expand Down Expand Up @@ -60,7 +62,7 @@ To change the locale, users can visit a prefixed route. This will take precedenc
4. When the user clicks on the link, a request to `/en` is initiated.
5. The middleware will update the cookie value to `en` and subsequently redirects the user to `/`.

### Domain-based routing
### Strategy 2: Domain-based routing [#domain-based-routing]

If you want to serve your localized content based on different domains, you can provide a list of mappings between domains and locales to the middleware.

Expand Down Expand Up @@ -189,7 +191,7 @@ In this case, only the locale prefix and a potentially [matching domain](#domain

The middleware automatically sets [the `link` header](https://developers.google.com/search/docs/specialty/international/localized-versions#http) to inform search engines that your content is available in different languages. Note that this automatically integrates with your routing strategy and will generate the correct links based on your configuration.

If you prefer to include these links yourself, e.g. because you're using [locale-specific rewrites](#localizing-pathnames), you can opt-out of this behavior.
If you prefer to include these links yourself, you can opt-out of this behavior.

```tsx filename="middleware.ts" {6}
import createMiddleware from 'next-intl/middleware';
Expand All @@ -203,27 +205,56 @@ export default createMiddleware({

## Localizing pathnames

If you want to localize the pathnames of your app, you can accomplish this by using appropriate [rewrites](https://nextjs.org/docs/api-reference/next.config.js/rewrites).
Many apps choose to localize pathnames, especially when search engine optimization is relevant, e.g.:

- `/en/about`
- `/de/ueber-uns`

Since you want to define these routes only once internally, you can use the `next-intl` middleware to [rewrite](https://nextjs.org/docs/api-reference/next.config.js/rewrites) such incoming requests to shared pathnames.

```js filename="next.config.js" {7-8}
const withNextIntl = require('next-intl/plugin')();
```tsx filename="middleware.ts"
import createMiddleware from 'next-intl/middleware';

export default createMiddleware({
defaultLocale: 'en',
locales: ['en', 'de'],

module.exports = withNextIntl({
rewrites() {
return [
{
source: '/de/über',
destination: '/de/about'
}
];
// The `pathnames` object holds pairs of internal and
// external paths. Based on the locale, the external
// paths are rewritten to the shared, internal ones.
pathnames: {
// If all locales use the same pathname, a single
// external path can be used for all locales.
'/': '/',
'/blog': '/blog',

// If locales use different paths, you can
// specify each external path per locale.
'/about': {
en: '/about',
de: '/ueber-uns'
},

// Dynamic params are supported via square brackets
'/news/[articleSlug]-[articleId]': {
en: '/news/[articleSlug]-[articleId]',
de: '/neuigkeiten/[articleSlug]-[articleId]'
},

// Also (optional) catch-all segments are supported
'/categories/[...slug]': {
en: '/categories/[...slug]',
de: '/kategorien/[...slug]'
}
}
});
```

Since `next-intl` isn't aware of the rewrites you've configured, you likely want to make some adjustments:

1. Translate the pathnames you're passing to [navigation APIs](/docs/routing/navigation) like `Link` based on the `locale`. See the [named routes example](https://github.com/amannn/next-intl/blob/feat/next-13-rsc/examples/example-next-13-named-routes/) that uses the proposed APIs from [the Server Components beta](https://next-intl-docs.vercel.app/docs/getting-started/app-router-server-components).
2. Turn off [the `alternateLinks` option](/docs/routing/middleware#disable-alternate-links) and provide [search engine hints about localized versions of your content](https://developers.google.com/search/docs/specialty/international/localized-versions) by yourself.
<Callout>
If you have pathname localization set up in the middleware, you likely want to
use the [localized navigation
APIs](/docs/routing/navigation#localized-pathnames) in your components.
</Callout>

## Composing other middlewares

Expand Down Expand Up @@ -332,7 +363,8 @@ If you're using the [static export feature from Next.js](https://nextjs.org/docs

1. There's no default locale that can be used without a prefix (same as [`localePrefix: 'always'`](#always-use-a-locale-prefix))
2. The locale can't be negotiated at runtime (same as [`localeDetection: false`](#disable-automatic-locale-detection))
3. You need to add a redirect for the root of the app
3. You can't use [pathname localization](#localizing-pathnames).
4. You need to add a redirect for the root of the app

```tsx filename="app/page.tsx"
import {redirect} from 'next/navigation';
Expand Down
Loading
Loading