Skip to content

Commit 79587ac

Browse files
committed
implementation
1 parent ccaf1d6 commit 79587ac

17 files changed

+177
-281
lines changed

Diff for: docs/src/pages/docs/routing/middleware.mdx

+3-4
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,13 @@ Since the middleware is aware of all your domains, if a domain receives a reques
9393
4. The middleware recognizes that the user wants to switch to another domain and responds with a redirect to `ca.example.com/fr`.
9494

9595
<Details id="domain-matching">
96-
<summary>How is the best matching domain for a given locale detected?</summary>
96+
<summary>How is the best-matching domain for a given locale detected?</summary>
9797

98-
The bestmatching domain is detected based on these priorities:
98+
The best-matching domain is detected based on these priorities:
9999

100100
1. Stay on the current domain if the locale is supported here
101101
2. Use an alternative domain where the locale is configured as the `defaultLocale`
102-
3. Use an alternative domain where the available `locales` are restricted and the locale is supported
103-
4. Use an alternative domain that supports all locales
102+
3. Use an alternative domain that supports the locale
104103

105104
</Details>
106105

Diff for: examples/example-app-router-playground/src/i18n/routing.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ export const routing = defineRouting({
1818
? [
1919
{
2020
domain: 'example.com',
21-
defaultLocale: 'en'
21+
defaultLocale: 'en',
22+
locales: ['en', 'de', 'es', 'ja']
2223
},
2324
{
2425
domain: 'example.de',
25-
defaultLocale: 'de'
26+
defaultLocale: 'de',
27+
locales: ['en', 'de', 'es', 'ja']
2628
}
2729
]
2830
: undefined,

Diff for: packages/next-intl/.size-limit.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ const config: SizeLimitConfig = [
2121
name: "import {createNavigation} from 'next-intl/navigation' (react-client)",
2222
path: 'dist/esm/production/navigation.react-client.js',
2323
import: '{createNavigation}',
24-
limit: '2.485 KB'
24+
limit: '2.285 KB'
2525
},
2626
{
2727
name: "import {createNavigation} from 'next-intl/navigation' (react-server)",
2828
path: 'dist/esm/production/navigation.react-server.js',
2929
import: '{createNavigation}',
30-
limit: '3.275 KB'
30+
limit: '3.055 KB'
3131
},
3232
{
3333
name: "import * from 'next-intl/server' (react-client)",
@@ -42,7 +42,7 @@ const config: SizeLimitConfig = [
4242
{
4343
name: "import * from 'next-intl/middleware'",
4444
path: 'dist/esm/production/middleware.js',
45-
limit: '9.315 KB'
45+
limit: '9.285 KB'
4646
},
4747
{
4848
name: "import * from 'next-intl/routing'",

Diff for: packages/next-intl/src/middleware/getAlternateLinksHeaderValue.test.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,8 @@ describe.each([{basePath: undefined}, {basePath: '/base'}])(
197197
domains: [
198198
{
199199
domain: 'example.com',
200-
defaultLocale: 'en'
201-
// (supports all locales)
200+
defaultLocale: 'en',
201+
locales: ['en', 'es', 'fr']
202202
},
203203
{
204204
domain: 'example.es',
@@ -264,8 +264,8 @@ describe.each([{basePath: undefined}, {basePath: '/base'}])(
264264
domains: [
265265
{
266266
domain: 'example.com',
267-
defaultLocale: 'en'
268-
// (supports all locales)
267+
defaultLocale: 'en',
268+
locales: ['en', 'es', 'fr']
269269
},
270270
{
271271
domain: 'example.es',

Diff for: packages/next-intl/src/middleware/middleware.test.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -2480,7 +2480,8 @@ describe('domain-based routing', () => {
24802480
domains: [
24812481
{
24822482
defaultLocale: 'fr',
2483-
domain: 'ca.example.com'
2483+
domain: 'ca.example.com',
2484+
locales: ['en', 'fr']
24842485
}
24852486
]
24862487
});

Diff for: packages/next-intl/src/middleware/resolveLocale.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ function resolveLocaleFromDomain<
197197
if (!locale && routing.localeDetection) {
198198
const headerLocale = getAcceptLanguageLocale(
199199
requestHeaders,
200-
domain.locales || routing.locales,
200+
domain.locales,
201201
domain.defaultLocale
202202
);
203203

Diff for: packages/next-intl/src/middleware/utils.tsx

+3-17
Original file line numberDiff line numberDiff line change
@@ -258,11 +258,7 @@ export function isLocaleSupportedOnDomain<AppLocales extends Locales>(
258258
locale: Locale,
259259
domain: DomainConfig<AppLocales>
260260
) {
261-
return (
262-
domain.defaultLocale === locale ||
263-
!domain.locales ||
264-
domain.locales.includes(locale)
265-
);
261+
return domain.defaultLocale === locale || domain.locales.includes(locale);
266262
}
267263

268264
export function getBestMatchingDomain<AppLocales extends Locales>(
@@ -282,19 +278,9 @@ export function getBestMatchingDomain<AppLocales extends Locales>(
282278
domainConfig = domainsConfig.find((cur) => cur.defaultLocale === locale);
283279
}
284280

285-
// Prio 3: Use alternative domain with restricted matching locale
286-
if (!domainConfig) {
287-
domainConfig = domainsConfig.find((cur) => cur.locales?.includes(locale));
288-
}
289-
290-
// Prio 4: Stay on the current domain if it supports all locales
291-
if (!domainConfig && curHostDomain?.locales == null) {
292-
domainConfig = curHostDomain;
293-
}
294-
295-
// Prio 5: Use alternative domain that supports all locales
281+
// Prio 3: Use alternative domain that supports the locale
296282
if (!domainConfig) {
297-
domainConfig = domainsConfig.find((cur) => !cur.locales);
283+
domainConfig = domainsConfig.find((cur) => cur.locales.includes(locale));
298284
}
299285

300286
return domainConfig;

Diff for: packages/next-intl/src/navigation/createNavigation.test.tsx

+19-42
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,13 @@ const defaultLocale = 'en' as const;
5353
const domains = [
5454
{
5555
defaultLocale: 'en',
56-
domain: 'example.com'
56+
domain: 'example.com',
57+
locales: ['en']
5758
},
5859
{
5960
defaultLocale: 'de',
6061
domain: 'example.de',
61-
locales: ['de', 'en']
62+
locales: ['de', 'ja']
6263
}
6364
] satisfies DomainsConfig<typeof locales>;
6465

@@ -907,10 +908,9 @@ describe.each([
907908
});
908909

909910
describe('Link', () => {
910-
it('renders a prefix during SSR even for the default locale', () => {
911-
// (see comment in source for reasoning)
911+
it('renders no prefix during SSR for the default locale', () => {
912912
const markup = renderToString(<Link href="/about">About</Link>);
913-
expect(markup).toContain('href="/en/about"');
913+
expect(markup).toContain('href="/about"');
914914
});
915915

916916
it('does not render a prefix eventually on the client side for the default locale of the given domain', () => {
@@ -935,11 +935,11 @@ describe.each([
935935

936936
it('renders a prefix when currently on a secondary locale', () => {
937937
mockLocation({host: 'example.de'});
938-
mockCurrentLocale('en');
938+
mockCurrentLocale('ja');
939939
render(<Link href="/about">About</Link>);
940940
expect(
941941
screen.getByRole('link', {name: 'About'}).getAttribute('href')
942-
).toBe('/en/about');
942+
).toBe('/ja/about');
943943
});
944944

945945
it('does not render a prefix when currently on a domain with a different default locale', () => {
@@ -973,30 +973,9 @@ describe.each([
973973
});
974974

975975
describe('getPathname', () => {
976-
it('does not add a prefix for the default locale', () => {
977-
expect(
978-
getPathname({locale: 'en', href: '/about', domain: 'example.com'})
979-
).toBe('/about');
980-
expect(
981-
getPathname({locale: 'de', href: '/about', domain: 'example.de'})
982-
).toBe('/about');
983-
});
984-
985-
it('adds a prefix for a secondary locale', () => {
986-
expect(
987-
getPathname({locale: 'de', href: '/about', domain: 'example.com'})
988-
).toBe('/de/about');
989-
expect(
990-
getPathname({locale: 'en', href: '/about', domain: 'example.de'})
991-
).toBe('/en/about');
992-
});
993-
994-
it('prints a warning when no domain is provided', () => {
995-
const consoleSpy = vi.spyOn(console, 'error');
996-
// @ts-expect-error -- Domain is not provided
997-
getPathname({locale: 'de', href: '/about'});
998-
expect(consoleSpy).toHaveBeenCalled();
999-
consoleSpy.mockRestore();
976+
it('does not add a prefix for a default locale', () => {
977+
expect(getPathname({locale: 'en', href: '/about'})).toBe('/about');
978+
expect(getPathname({locale: 'de', href: '/about'})).toBe('/about');
1000979
});
1001980
});
1002981

@@ -1005,21 +984,19 @@ describe.each([
1005984
['permanentRedirect', permanentRedirect, nextPermanentRedirect]
1006985
])('%s', (_, redirectFn, nextRedirectFn) => {
1007986
it('adds a prefix even for the default locale', () => {
1008-
// (see comment in source for reasoning)
987+
// There's one edge case that is not handled here: If `localePrefix:
988+
// 'as-needed'` is used and the user redirects from a non-default locale
989+
// to the default locale, no cookie will be updated and therefore the user
990+
// redirected back to the original locale. Typically, redirect is not used
991+
// for language switching though and uses the current locale of the user,
992+
// therefore this is currently neglected.
1009993
runInRender(() => redirectFn({href: '/', locale: 'en'}));
1010-
expect(nextRedirectFn).toHaveBeenLastCalledWith('/en');
1011-
});
1012-
1013-
it('does not add a prefix when domain is provided for the default locale', () => {
1014-
runInRender(() =>
1015-
redirectFn({href: '/', locale: 'en', domain: 'example.com'})
1016-
);
1017994
expect(nextRedirectFn).toHaveBeenLastCalledWith('/');
1018995
});
1019996

1020997
it('adds a prefix for a secondary locale', () => {
1021-
runInRender(() => redirectFn({href: '/', locale: 'de'}));
1022-
expect(nextRedirectFn).toHaveBeenLastCalledWith('/de');
998+
runInRender(() => redirectFn({href: '/', locale: 'ja'}));
999+
expect(nextRedirectFn).toHaveBeenLastCalledWith('/ja');
10231000
});
10241001
});
10251002
});
@@ -1052,7 +1029,7 @@ describe.each([
10521029
runInRender(() =>
10531030
redirectFn({href: {pathname: '/', query: {foo: 'bar'}}, locale: 'en'})
10541031
);
1055-
expect(nextRedirectFn).toHaveBeenLastCalledWith('/en?foo=bar');
1032+
expect(nextRedirectFn).toHaveBeenLastCalledWith('/?foo=bar');
10561033
});
10571034
});
10581035
});

Diff for: packages/next-intl/src/navigation/react-client/createNavigation.test.tsx

+14-43
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,13 @@ const defaultLocale = 'en' as const;
5555
const domains: DomainsConfig<typeof locales> = [
5656
{
5757
defaultLocale: 'en',
58-
domain: 'example.com'
58+
domain: 'example.com',
59+
locales: ['en']
5960
},
6061
{
6162
defaultLocale: 'de',
6263
domain: 'example.de',
63-
locales: ['de', 'en']
64+
locales: ['de', 'ja']
6465
}
6566
];
6667

@@ -563,43 +564,20 @@ describe("localePrefix: 'as-needed', with `basePath` and `domains`", () => {
563564
describe('useRouter', () => {
564565
const invokeRouter = getInvokeRouter(useRouter);
565566

566-
describe('example.com, defaultLocale: "en"', () => {
567-
beforeEach(() => {
568-
mockLocation(
569-
{pathname: '/base/path/about', host: 'example.com'},
570-
'/base/path'
571-
);
572-
});
573-
574-
it('can compute the correct pathname when the default locale on the current domain matches the current locale', () => {
575-
invokeRouter((router) => router.push('/test'));
576-
expect(useNextRouter().push).toHaveBeenCalledWith('/test');
577-
});
578-
579-
it('can compute the correct pathname when the default locale on the current domain does not match the current locale', () => {
580-
invokeRouter((router) => router.push('/test', {locale: 'de'}));
581-
expect(useNextRouter().push).toHaveBeenCalledWith('/de/test');
582-
});
567+
it('can compute the correct pathname when on the default locale and not supplying a locale', () => {
568+
invokeRouter((router) => router.push('/test'));
569+
expect(useNextRouter().push).toHaveBeenCalledWith('/test');
583570
});
584571

585-
describe('example.de, defaultLocale: "de"', () => {
586-
beforeEach(() => {
587-
mockCurrentLocale('de');
588-
mockLocation(
589-
{pathname: '/base/path/about', host: 'example.de'},
590-
'/base/path'
591-
);
592-
});
593-
594-
it('can compute the correct pathname when the default locale on the current domain matches the current locale', () => {
595-
invokeRouter((router) => router.push('/test'));
596-
expect(useNextRouter().push).toHaveBeenCalledWith('/test');
597-
});
572+
it('can compute the correct pathname when on the default locale and supplying a secondary locale', () => {
573+
invokeRouter((router) => router.push('/test', {locale: 'ja'}));
574+
expect(useNextRouter().push).toHaveBeenCalledWith('/ja/test');
575+
});
598576

599-
it('can compute the correct pathname when the default locale on the current domain does not match the current locale', () => {
600-
invokeRouter((router) => router.push('/test', {locale: 'en'}));
601-
expect(useNextRouter().push).toHaveBeenCalledWith('/en/test');
602-
});
577+
it('can compute the correct pathname when on a secondary locale and navigating to the default locale', () => {
578+
mockCurrentLocale('ja');
579+
invokeRouter((router) => router.push('/test', {locale: 'en'}));
580+
expect(useNextRouter().push).toHaveBeenCalledWith('/test');
603581
});
604582
});
605583
});
@@ -639,13 +617,6 @@ describe("localePrefix: 'as-needed', with `domains`", () => {
639617
expect(consoleSpy).not.toHaveBeenCalled();
640618
consoleSpy.mockRestore();
641619
});
642-
643-
it('prefixes the default locale when on a domain with a different defaultLocale', () => {
644-
mockCurrentLocale('de');
645-
mockLocation({pathname: '/about', host: 'example.de'});
646-
invokeRouter((router) => router[method]('/about', {locale: 'en'}));
647-
expect(useNextRouter()[method]).toHaveBeenCalledWith('/en/about');
648-
});
649620
});
650621
});
651622

Diff for: packages/next-intl/src/navigation/react-client/createNavigation.tsx

+1-3
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,9 @@ export default function createNavigation<
8585
): void {
8686
const {locale: nextLocale, ...rest} = options || {};
8787

88-
// @ts-expect-error -- We're passing a domain here just in case
8988
const pathname = getPathname({
9089
href,
91-
locale: nextLocale || curLocale,
92-
domain: window.location.host
90+
locale: nextLocale || curLocale
9391
});
9492

9593
const args: [href: string, options?: Options] = [pathname];

0 commit comments

Comments
 (0)