Skip to content

Commit 5cab03f

Browse files
authored
Add handling for domain to locale mapping (#17771)
Follow-up to #17370 this adds mapping of locales to domains and handles default locales for specific domains also allowing specifying which locales can be visited for each domain. This PR also updates to output all statically generated pages under the locale prefix to make it easier to locate/lookup and to not redirect to the default locale prefixed path when no `accept-language` header is provided.
1 parent e334c4e commit 5cab03f

File tree

11 files changed

+310
-80
lines changed

11 files changed

+310
-80
lines changed

packages/next/build/index.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -780,7 +780,6 @@ export default async function build(
780780
const isFallback = isSsg && ssgStaticFallbackPages.has(page)
781781

782782
for (const locale of i18n.locales) {
783-
if (!isSsg && locale === i18n.defaultLocale) continue
784783
// skip fallback generation for SSG pages without fallback mode
785784
if (isSsg && isDynamic && !isFallback) continue
786785
const outputPath = `/${locale}${page === '/' ? '' : page}`
@@ -869,22 +868,19 @@ export default async function build(
869868
// for SSG files with i18n the non-prerendered variants are
870869
// output with the locale prefixed so don't attempt moving
871870
// without the prefix
872-
if (!i18n || !isSsg || additionalSsgFile) {
871+
if (!i18n || additionalSsgFile) {
873872
await promises.mkdir(path.dirname(dest), { recursive: true })
874873
await promises.rename(orig, dest)
874+
} else if (i18n && !isSsg) {
875+
// this will be updated with the locale prefixed variant
876+
// since all files are output with the locale prefix
877+
delete pagesManifest[page]
875878
}
876879

877880
if (i18n) {
878881
if (additionalSsgFile) return
879882

880883
for (const locale of i18n.locales) {
881-
// auto-export default locale files exist at root
882-
// TODO: should these always be prefixed with locale
883-
// similar to SSG prerender/fallback files?
884-
if (!isSsg && locale === i18n.defaultLocale) {
885-
continue
886-
}
887-
888884
const localeExt = page === '/' ? path.extname(file) : ''
889885
const relativeDestNoPages = relativeDest.substr('pages/'.length)
890886

packages/next/build/webpack/loaders/next-serverless-loader.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -222,24 +222,33 @@ const nextServerlessLoader: loader.Loader = function () {
222222
const i18n = ${i18n}
223223
const accept = require('@hapi/accept')
224224
const { detectLocaleCookie } = require('next/dist/next-server/lib/i18n/detect-locale-cookie')
225+
const { detectDomainLocales } = require('next/dist/next-server/lib/i18n/detect-domain-locales')
225226
const { normalizeLocalePath } = require('next/dist/next-server/lib/i18n/normalize-locale-path')
226227
let detectedLocale = detectLocaleCookie(req, i18n.locales)
227228
229+
const { defaultLocale, locales } = detectDomainLocales(
230+
req,
231+
i18n.domains,
232+
i18n.locales,
233+
i18n.defaultLocale,
234+
)
235+
228236
if (!detectedLocale) {
229237
detectedLocale = accept.language(
230238
req.headers['accept-language'],
231-
i18n.locales
239+
locales
232240
)
233241
}
234242
235243
const denormalizedPagePath = denormalizePagePath(parsedUrl.pathname || '/')
236-
const detectedDefaultLocale = detectedLocale === i18n.defaultLocale
244+
const detectedDefaultLocale = !detectedLocale || detectedLocale === defaultLocale
237245
const shouldStripDefaultLocale =
238246
detectedDefaultLocale &&
239-
denormalizedPagePath === \`/\${i18n.defaultLocale}\`
247+
denormalizedPagePath === \`/\${defaultLocale}\`
240248
const shouldAddLocalePrefix =
241249
!detectedDefaultLocale && denormalizedPagePath === '/'
242-
detectedLocale = detectedLocale || i18n.defaultLocale
250+
251+
detectedLocale = detectedLocale || defaultLocale
243252
244253
if (
245254
!fromExport &&
@@ -260,8 +269,7 @@ const nextServerlessLoader: loader.Loader = function () {
260269
return
261270
}
262271
263-
// TODO: domain based locales (domain to locale mapping needs to be provided in next.config.js)
264-
const localePathResult = normalizeLocalePath(parsedUrl.pathname, i18n.locales)
272+
const localePathResult = normalizeLocalePath(parsedUrl.pathname, locales)
265273
266274
if (localePathResult.detectedLocale) {
267275
detectedLocale = localePathResult.detectedLocale
@@ -272,11 +280,13 @@ const nextServerlessLoader: loader.Loader = function () {
272280
parsedUrl.pathname = localePathResult.pathname
273281
}
274282
275-
detectedLocale = detectedLocale || i18n.defaultLocale
283+
detectedLocale = detectedLocale || defaultLocale
276284
`
277285
: `
278286
const i18n = {}
279287
const detectedLocale = undefined
288+
const defaultLocale = undefined
289+
const locales = undefined
280290
`
281291

282292
if (page.match(API_ROUTE)) {
@@ -468,8 +478,8 @@ const nextServerlessLoader: loader.Loader = function () {
468478
nextExport: fromExport,
469479
isDataReq: _nextData,
470480
locale: detectedLocale,
471-
locales: i18n.locales,
472-
defaultLocale: i18n.defaultLocale,
481+
locales,
482+
defaultLocale,
473483
},
474484
options,
475485
)

packages/next/client/index.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,7 @@ import type {
1111
AppProps,
1212
PrivateRouteInfo,
1313
} from '../next-server/lib/router/router'
14-
import {
15-
delBasePath,
16-
hasBasePath,
17-
delLocale,
18-
} from '../next-server/lib/router/router'
14+
import { delBasePath, hasBasePath } from '../next-server/lib/router/router'
1915
import { isDynamicRoute } from '../next-server/lib/router/utils/is-dynamic'
2016
import * as querystring from '../next-server/lib/router/utils/querystring'
2117
import * as envConfig from '../next-server/lib/runtime-config'
@@ -65,10 +61,9 @@ const {
6561
isFallback,
6662
head: initialHeadData,
6763
locales,
68-
defaultLocale,
6964
} = data
7065

71-
let { locale } = data
66+
let { locale, defaultLocale } = data
7267

7368
const prefix = assetPrefix || ''
7469

@@ -88,19 +83,21 @@ if (hasBasePath(asPath)) {
8883
asPath = delBasePath(asPath)
8984
}
9085

91-
asPath = delLocale(asPath, locale)
92-
9386
if (process.env.__NEXT_i18n_SUPPORT) {
9487
const {
9588
normalizeLocalePath,
9689
} = require('../next-server/lib/i18n/normalize-locale-path')
9790

98-
if (isFallback && locales) {
91+
if (locales) {
9992
const localePathResult = normalizeLocalePath(asPath, locales)
10093

10194
if (localePathResult.detectedLocale) {
10295
asPath = asPath.substr(localePathResult.detectedLocale.length + 1)
103-
locale = localePathResult.detectedLocale
96+
} else {
97+
// derive the default locale if it wasn't detected in the asPath
98+
// since we don't prerender static pages with all possible default
99+
// locales
100+
defaultLocale = locale
104101
}
105102
}
106103
}

packages/next/client/page-loader.ts

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -203,23 +203,13 @@ export default class PageLoader {
203203
* @param {string} href the route href (file-system path)
204204
* @param {string} asPath the URL as shown in browser (virtual path); used for dynamic routes
205205
*/
206-
getDataHref(
207-
href: string,
208-
asPath: string,
209-
ssg: boolean,
210-
locale?: string,
211-
defaultLocale?: string
212-
) {
206+
getDataHref(href: string, asPath: string, ssg: boolean, locale?: string) {
213207
const { pathname: hrefPathname, query, search } = parseRelativeUrl(href)
214208
const { pathname: asPathname } = parseRelativeUrl(asPath)
215209
const route = normalizeRoute(hrefPathname)
216210

217211
const getHrefForSlug = (path: string) => {
218-
const dataRoute = addLocale(
219-
getAssetPathFromRoute(path, '.json'),
220-
locale,
221-
defaultLocale
222-
)
212+
const dataRoute = addLocale(getAssetPathFromRoute(path, '.json'), locale)
223213
return addBasePath(
224214
`/_next/data/${this.buildId}${dataRoute}${ssg ? '' : search}`
225215
)
@@ -239,26 +229,15 @@ export default class PageLoader {
239229
* @param {string} href the route href (file-system path)
240230
* @param {string} asPath the URL as shown in browser (virtual path); used for dynamic routes
241231
*/
242-
prefetchData(
243-
href: string,
244-
asPath: string,
245-
locale?: string,
246-
defaultLocale?: string
247-
) {
232+
prefetchData(href: string, asPath: string, locale?: string) {
248233
const { pathname: hrefPathname } = parseRelativeUrl(href)
249234
const route = normalizeRoute(hrefPathname)
250235
return this.promisedSsgManifest!.then(
251236
(s: ClientSsgManifest, _dataHref?: string) =>
252237
// Check if the route requires a data file
253238
s.has(route) &&
254239
// Try to generate data href, noop when falsy
255-
(_dataHref = this.getDataHref(
256-
href,
257-
asPath,
258-
true,
259-
locale,
260-
defaultLocale
261-
)) &&
240+
(_dataHref = this.getDataHref(href, asPath, true, locale)) &&
262241
// noop when data has already been prefetched (dedupe)
263242
!document.querySelector(
264243
`link[rel="${relPrefetch}"][href^="${_dataHref}"]`
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { IncomingMessage } from 'http'
2+
3+
export function detectDomainLocales(
4+
req: IncomingMessage,
5+
domainItems:
6+
| Array<{
7+
domain: string
8+
locales: string[]
9+
defaultLocale: string
10+
}>
11+
| undefined,
12+
locales: string[],
13+
defaultLocale: string
14+
) {
15+
let curDefaultLocale = defaultLocale
16+
let curLocales = locales
17+
18+
const { host } = req.headers
19+
20+
if (host && domainItems) {
21+
// remove port from host and remove port if present
22+
const hostname = host.split(':')[0].toLowerCase()
23+
24+
for (const item of domainItems) {
25+
if (hostname === item.domain.toLowerCase()) {
26+
curDefaultLocale = item.defaultLocale
27+
curLocales = item.locales
28+
break
29+
}
30+
}
31+
}
32+
33+
return {
34+
defaultLocale: curDefaultLocale,
35+
locales: curLocales,
36+
}
37+
}

packages/next/next-server/lib/router/router.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -974,8 +974,7 @@ export default class Router implements BaseRouter {
974974
formatWithValidation({ pathname, query }),
975975
delBasePath(as),
976976
__N_SSG,
977-
this.locale,
978-
this.defaultLocale
977+
this.locale
979978
)
980979
}
981980

packages/next/next-server/server/config.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,47 @@ function assignDefaults(userConfig: { [key: string]: any }) {
227227
throw new Error(`Specified i18n.defaultLocale should be a string`)
228228
}
229229

230+
if (typeof i18n.domains !== 'undefined' && !Array.isArray(i18n.domains)) {
231+
throw new Error(
232+
`Specified i18n.domains must be an array of domain objects e.g. [ { domain: 'example.fr', defaultLocale: 'fr', locales: ['fr'] } ] received ${typeof i18n.domains}`
233+
)
234+
}
235+
236+
if (i18n.domains) {
237+
const invalidDomainItems = i18n.domains.filter((item: any) => {
238+
if (!item || typeof item !== 'object') return true
239+
if (!item.defaultLocale) return true
240+
if (!item.domain || typeof item.domain !== 'string') return true
241+
if (!item.locales || !Array.isArray(item.locales)) return true
242+
243+
const invalidLocales = item.locales.filter(
244+
(locale: string) => !i18n.locales.includes(locale)
245+
)
246+
247+
if (invalidLocales.length > 0) {
248+
console.error(
249+
`i18n.domains item "${
250+
item.domain
251+
}" has the following locales (${invalidLocales.join(
252+
', '
253+
)}) that aren't provided in the main i18n.locales. Add them to the i18n.locales list or remove them from the domains item locales to continue.\n`
254+
)
255+
return true
256+
}
257+
return false
258+
})
259+
260+
if (invalidDomainItems.length > 0) {
261+
throw new Error(
262+
`Invalid i18n.domains values:\n${invalidDomainItems
263+
.map((item: any) => JSON.stringify(item))
264+
.join(
265+
'\n'
266+
)}\n\ndomains value must follow format { domain: 'example.fr', defaultLocale: 'fr', locales: ['fr'] }`
267+
)
268+
}
269+
}
270+
230271
if (!Array.isArray(i18n.locales)) {
231272
throw new Error(
232273
`Specified i18n.locales must be an array of locale strings e.g. ["en-US", "nl-NL"] received ${typeof i18n.locales}`

0 commit comments

Comments
 (0)