Skip to content

Commit 42988b7

Browse files
authored
fix: When using domain-based routing, use defaultLocale of a domain instead of the top-level one in case no other locale matches better on the domain (#1000)
Fixes #998 Note that the `defaultLocale` of a domain is used if no other locale matches better. However, if a domain supports multiple locales, the best-matching one will be selected based on the `accept-language` header. If you want to always use the `defaultLocale` in case no prefix is provided, then you can turn off `localeDetection`.
1 parent 672eccf commit 42988b7

File tree

4 files changed

+109
-53
lines changed

4 files changed

+109
-53
lines changed

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

+7-8
Original file line numberDiff line numberDiff line change
@@ -84,16 +84,14 @@ export default createMiddleware({
8484
{
8585
domain: 'us.example.com',
8686
defaultLocale: 'en',
87-
// Optionally restrict the locales managed by this domain. If this
88-
// domain receives requests for another locale (e.g. us.example.com/fr),
89-
// then the middleware will redirect to a domain that supports it.
87+
// Optionally restrict the locales available on this domain
9088
locales: ['en']
9189
},
9290
{
9391
domain: 'ca.example.com',
9492
defaultLocale: 'en'
9593
// If there are no `locales` specified on a domain,
96-
// all global locales will be supported here.
94+
// all available locales will be supported here
9795
}
9896
]
9997
});
@@ -110,11 +108,12 @@ To match the request against the available domains, the host is read from the `x
110108

111109
The locale is detected based on these priorities:
112110

113-
1. A locale prefix is present in the pathname and the domain supports it (e.g. `ca.example.com/fr`)
114-
2. If the host of the request is configured in `domains`, the `defaultLocale` of the domain is used
115-
3. As a fallback, the [locale detection of prefix-based routing](#locale-detection) applies
111+
1. A locale prefix is present in the pathname (e.g. `ca.example.com/fr`)
112+
2. A locale is stored in a cookie and is supported on the domain
113+
3. A locale that the domain supports is matched based on the [`accept-language` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language)
114+
4. As a fallback, the `defaultLocale` of the domain is used
116115

117-
Since the middleware is aware of all your domains, the domain will automatically be switched when the user requests to change the locale.
116+
Since the middleware is aware of all your domains, if a domain receives a request for a locale that is not supported (e.g. `en.example.com/fr`), it will redirect to an alternative domain that does support the locale.
118117

119118
**Example workflow:**
120119

Diff for: packages/next-intl/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@
138138
},
139139
{
140140
"path": "dist/production/middleware.js",
141-
"limit": "5.95 KB"
141+
"limit": "6 KB"
142142
}
143143
]
144144
}

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

+83-43
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,35 @@ export function getAcceptLanguageLocale<Locales extends AllLocales>(
5555
return locale;
5656
}
5757

58+
function getLocaleFromPrefix<Locales extends AllLocales>(
59+
pathname: string,
60+
locales: Locales
61+
) {
62+
const pathLocaleCandidate = getFirstPathnameSegment(pathname);
63+
return findCaseInsensitiveLocale(pathLocaleCandidate, locales);
64+
}
65+
66+
function getLocaleFromCookie<Locales extends AllLocales>(
67+
requestCookies: RequestCookies,
68+
locales: Locales
69+
) {
70+
if (requestCookies.has(COOKIE_LOCALE_NAME)) {
71+
const value = requestCookies.get(COOKIE_LOCALE_NAME)?.value;
72+
if (value && locales.includes(value)) {
73+
return value;
74+
}
75+
}
76+
}
77+
5878
function resolveLocaleFromPrefix<Locales extends AllLocales>(
5979
{
6080
defaultLocale,
6181
localeDetection,
6282
locales
63-
}: MiddlewareConfigWithDefaults<Locales>,
83+
}: Pick<
84+
MiddlewareConfigWithDefaults<Locales>,
85+
'defaultLocale' | 'localeDetection' | 'locales'
86+
>,
6487
requestHeaders: Headers,
6588
requestCookies: RequestCookies,
6689
pathname: string
@@ -69,24 +92,12 @@ function resolveLocaleFromPrefix<Locales extends AllLocales>(
6992

7093
// Prio 1: Use route prefix
7194
if (pathname) {
72-
const pathLocaleCandidate = getFirstPathnameSegment(pathname);
73-
const matchedLocale = findCaseInsensitiveLocale(
74-
pathLocaleCandidate,
75-
locales
76-
);
77-
if (matchedLocale) {
78-
locale = matchedLocale;
79-
}
95+
locale = getLocaleFromPrefix(pathname, locales);
8096
}
8197

8298
// Prio 2: Use existing cookie
8399
if (!locale && localeDetection && requestCookies) {
84-
if (requestCookies.has(COOKIE_LOCALE_NAME)) {
85-
const value = requestCookies.get(COOKIE_LOCALE_NAME)?.value;
86-
if (value && locales.includes(value)) {
87-
locale = value;
88-
}
89-
}
100+
locale = getLocaleFromCookie(requestCookies, locales);
90101
}
91102

92103
// Prio 3: Use the `accept-language` header
@@ -108,37 +119,66 @@ function resolveLocaleFromDomain<Locales extends AllLocales>(
108119
requestCookies: RequestCookies,
109120
pathname: string
110121
) {
111-
const {domains} = config;
112-
113-
const localeFromPrefixStrategy = resolveLocaleFromPrefix(
114-
config,
115-
requestHeaders,
116-
requestCookies,
117-
pathname
118-
);
119-
120-
// Prio 1: Use a domain
121-
if (domains) {
122-
const domain = findDomainFromHost(requestHeaders, domains);
123-
const hasLocalePrefix =
124-
pathname && pathname.startsWith(`/${localeFromPrefixStrategy}`);
125-
126-
if (domain) {
127-
return {
128-
locale:
129-
isLocaleSupportedOnDomain<Locales>(
130-
localeFromPrefixStrategy,
131-
domain
132-
) || hasLocalePrefix
133-
? localeFromPrefixStrategy
134-
: domain.defaultLocale,
135-
domain
136-
};
122+
const domains = config.domains!;
123+
const domain = findDomainFromHost(requestHeaders, domains);
124+
125+
if (!domain) {
126+
return {
127+
locale: resolveLocaleFromPrefix(
128+
config,
129+
requestHeaders,
130+
requestCookies,
131+
pathname
132+
)
133+
};
134+
}
135+
136+
let locale;
137+
138+
// Prio 1: Use route prefix
139+
if (pathname) {
140+
const prefixLocale = getLocaleFromPrefix(pathname, config.locales);
141+
if (prefixLocale) {
142+
if (isLocaleSupportedOnDomain(prefixLocale, domain)) {
143+
locale = prefixLocale;
144+
} else {
145+
// Causes a redirect to a domain that supports the locale
146+
return {locale: prefixLocale, domain};
147+
}
148+
}
149+
}
150+
151+
// Prio 2: Use existing cookie
152+
if (!locale && config.localeDetection && requestCookies) {
153+
const cookieLocale = getLocaleFromCookie(requestCookies, config.locales);
154+
if (cookieLocale) {
155+
if (isLocaleSupportedOnDomain(cookieLocale, domain)) {
156+
locale = cookieLocale;
157+
} else {
158+
// Ignore
159+
}
137160
}
138161
}
139162

140-
// Prio 2: Use prefix strategy
141-
return {locale: localeFromPrefixStrategy};
163+
// Prio 3: Use the `accept-language` header
164+
if (!locale && config.localeDetection && requestHeaders) {
165+
const headerLocale = getAcceptLanguageLocale(
166+
requestHeaders,
167+
domain.locales || config.locales,
168+
domain.defaultLocale
169+
);
170+
171+
if (headerLocale) {
172+
locale = headerLocale;
173+
}
174+
}
175+
176+
// Prio 4: Use default locale
177+
if (!locale) {
178+
locale = domain.defaultLocale;
179+
}
180+
181+
return {locale, domain};
142182
}
143183

144184
export default function resolveLocale<Locales extends AllLocales>(

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

+18-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function createMockRequest(
4141
customHeaders?: HeadersInit
4242
) {
4343
const headers = new Headers({
44-
'accept-language': `${acceptLanguageLocale};q=0.9,en;q=0.8`,
44+
'accept-language': `${acceptLanguageLocale};q=0.9`,
4545
host: new URL(host).host,
4646
...(localeCookieValue && {
4747
cookie: `${COOKIE_LOCALE_NAME}=${localeCookieValue}`
@@ -1765,6 +1765,23 @@ describe('domain-based routing', () => {
17651765
);
17661766
});
17671767

1768+
it('prioritizes the default locale of a domain', () => {
1769+
const m = createIntlMiddleware({
1770+
defaultLocale: 'en',
1771+
locales: ['en', 'fr'],
1772+
domains: [
1773+
{
1774+
defaultLocale: 'fr',
1775+
domain: 'ca.example.com'
1776+
}
1777+
]
1778+
});
1779+
m(createMockRequest('/', 'de', 'http://ca.example.com'));
1780+
expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe(
1781+
'http://ca.example.com/fr'
1782+
);
1783+
});
1784+
17681785
describe('unknown hosts', () => {
17691786
it('serves requests for unknown hosts at the root', () => {
17701787
middleware(createMockRequest('/', 'en', 'http://localhost'));

0 commit comments

Comments
 (0)