Skip to content

Commit

Permalink
Add handling for domain to locale mapping (#17771)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ijjk committed Oct 10, 2020
1 parent e334c4e commit 5cab03f
Show file tree
Hide file tree
Showing 11 changed files with 310 additions and 80 deletions.
14 changes: 5 additions & 9 deletions packages/next/build/index.ts
Expand Up @@ -780,7 +780,6 @@ export default async function build(
const isFallback = isSsg && ssgStaticFallbackPages.has(page)

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

if (i18n) {
if (additionalSsgFile) return

for (const locale of i18n.locales) {
// auto-export default locale files exist at root
// TODO: should these always be prefixed with locale
// similar to SSG prerender/fallback files?
if (!isSsg && locale === i18n.defaultLocale) {
continue
}

const localeExt = page === '/' ? path.extname(file) : ''
const relativeDestNoPages = relativeDest.substr('pages/'.length)

Expand Down
28 changes: 19 additions & 9 deletions packages/next/build/webpack/loaders/next-serverless-loader.ts
Expand Up @@ -222,24 +222,33 @@ const nextServerlessLoader: loader.Loader = function () {
const i18n = ${i18n}
const accept = require('@hapi/accept')
const { detectLocaleCookie } = require('next/dist/next-server/lib/i18n/detect-locale-cookie')
const { detectDomainLocales } = require('next/dist/next-server/lib/i18n/detect-domain-locales')
const { normalizeLocalePath } = require('next/dist/next-server/lib/i18n/normalize-locale-path')
let detectedLocale = detectLocaleCookie(req, i18n.locales)
const { defaultLocale, locales } = detectDomainLocales(
req,
i18n.domains,
i18n.locales,
i18n.defaultLocale,
)
if (!detectedLocale) {
detectedLocale = accept.language(
req.headers['accept-language'],
i18n.locales
locales
)
}
const denormalizedPagePath = denormalizePagePath(parsedUrl.pathname || '/')
const detectedDefaultLocale = detectedLocale === i18n.defaultLocale
const detectedDefaultLocale = !detectedLocale || detectedLocale === defaultLocale
const shouldStripDefaultLocale =
detectedDefaultLocale &&
denormalizedPagePath === \`/\${i18n.defaultLocale}\`
denormalizedPagePath === \`/\${defaultLocale}\`
const shouldAddLocalePrefix =
!detectedDefaultLocale && denormalizedPagePath === '/'
detectedLocale = detectedLocale || i18n.defaultLocale
detectedLocale = detectedLocale || defaultLocale
if (
!fromExport &&
Expand All @@ -260,8 +269,7 @@ const nextServerlessLoader: loader.Loader = function () {
return
}
// TODO: domain based locales (domain to locale mapping needs to be provided in next.config.js)
const localePathResult = normalizeLocalePath(parsedUrl.pathname, i18n.locales)
const localePathResult = normalizeLocalePath(parsedUrl.pathname, locales)
if (localePathResult.detectedLocale) {
detectedLocale = localePathResult.detectedLocale
Expand All @@ -272,11 +280,13 @@ const nextServerlessLoader: loader.Loader = function () {
parsedUrl.pathname = localePathResult.pathname
}
detectedLocale = detectedLocale || i18n.defaultLocale
detectedLocale = detectedLocale || defaultLocale
`
: `
const i18n = {}
const detectedLocale = undefined
const defaultLocale = undefined
const locales = undefined
`

if (page.match(API_ROUTE)) {
Expand Down Expand Up @@ -468,8 +478,8 @@ const nextServerlessLoader: loader.Loader = function () {
nextExport: fromExport,
isDataReq: _nextData,
locale: detectedLocale,
locales: i18n.locales,
defaultLocale: i18n.defaultLocale,
locales,
defaultLocale,
},
options,
)
Expand Down
19 changes: 8 additions & 11 deletions packages/next/client/index.tsx
Expand Up @@ -11,11 +11,7 @@ import type {
AppProps,
PrivateRouteInfo,
} from '../next-server/lib/router/router'
import {
delBasePath,
hasBasePath,
delLocale,
} from '../next-server/lib/router/router'
import { delBasePath, hasBasePath } from '../next-server/lib/router/router'
import { isDynamicRoute } from '../next-server/lib/router/utils/is-dynamic'
import * as querystring from '../next-server/lib/router/utils/querystring'
import * as envConfig from '../next-server/lib/runtime-config'
Expand Down Expand Up @@ -65,10 +61,9 @@ const {
isFallback,
head: initialHeadData,
locales,
defaultLocale,
} = data

let { locale } = data
let { locale, defaultLocale } = data

const prefix = assetPrefix || ''

Expand All @@ -88,19 +83,21 @@ if (hasBasePath(asPath)) {
asPath = delBasePath(asPath)
}

asPath = delLocale(asPath, locale)

if (process.env.__NEXT_i18n_SUPPORT) {
const {
normalizeLocalePath,
} = require('../next-server/lib/i18n/normalize-locale-path')

if (isFallback && locales) {
if (locales) {
const localePathResult = normalizeLocalePath(asPath, locales)

if (localePathResult.detectedLocale) {
asPath = asPath.substr(localePathResult.detectedLocale.length + 1)
locale = localePathResult.detectedLocale
} else {
// derive the default locale if it wasn't detected in the asPath
// since we don't prerender static pages with all possible default
// locales
defaultLocale = locale
}
}
}
Expand Down
29 changes: 4 additions & 25 deletions packages/next/client/page-loader.ts
Expand Up @@ -203,23 +203,13 @@ export default class PageLoader {
* @param {string} href the route href (file-system path)
* @param {string} asPath the URL as shown in browser (virtual path); used for dynamic routes
*/
getDataHref(
href: string,
asPath: string,
ssg: boolean,
locale?: string,
defaultLocale?: string
) {
getDataHref(href: string, asPath: string, ssg: boolean, locale?: string) {
const { pathname: hrefPathname, query, search } = parseRelativeUrl(href)
const { pathname: asPathname } = parseRelativeUrl(asPath)
const route = normalizeRoute(hrefPathname)

const getHrefForSlug = (path: string) => {
const dataRoute = addLocale(
getAssetPathFromRoute(path, '.json'),
locale,
defaultLocale
)
const dataRoute = addLocale(getAssetPathFromRoute(path, '.json'), locale)
return addBasePath(
`/_next/data/${this.buildId}${dataRoute}${ssg ? '' : search}`
)
Expand All @@ -239,26 +229,15 @@ export default class PageLoader {
* @param {string} href the route href (file-system path)
* @param {string} asPath the URL as shown in browser (virtual path); used for dynamic routes
*/
prefetchData(
href: string,
asPath: string,
locale?: string,
defaultLocale?: string
) {
prefetchData(href: string, asPath: string, locale?: string) {
const { pathname: hrefPathname } = parseRelativeUrl(href)
const route = normalizeRoute(hrefPathname)
return this.promisedSsgManifest!.then(
(s: ClientSsgManifest, _dataHref?: string) =>
// Check if the route requires a data file
s.has(route) &&
// Try to generate data href, noop when falsy
(_dataHref = this.getDataHref(
href,
asPath,
true,
locale,
defaultLocale
)) &&
(_dataHref = this.getDataHref(href, asPath, true, locale)) &&
// noop when data has already been prefetched (dedupe)
!document.querySelector(
`link[rel="${relPrefetch}"][href^="${_dataHref}"]`
Expand Down
37 changes: 37 additions & 0 deletions packages/next/next-server/lib/i18n/detect-domain-locales.ts
@@ -0,0 +1,37 @@
import { IncomingMessage } from 'http'

export function detectDomainLocales(
req: IncomingMessage,
domainItems:
| Array<{
domain: string
locales: string[]
defaultLocale: string
}>
| undefined,
locales: string[],
defaultLocale: string
) {
let curDefaultLocale = defaultLocale
let curLocales = locales

const { host } = req.headers

if (host && domainItems) {
// remove port from host and remove port if present
const hostname = host.split(':')[0].toLowerCase()

for (const item of domainItems) {
if (hostname === item.domain.toLowerCase()) {
curDefaultLocale = item.defaultLocale
curLocales = item.locales
break
}
}
}

return {
defaultLocale: curDefaultLocale,
locales: curLocales,
}
}
3 changes: 1 addition & 2 deletions packages/next/next-server/lib/router/router.ts
Expand Up @@ -974,8 +974,7 @@ export default class Router implements BaseRouter {
formatWithValidation({ pathname, query }),
delBasePath(as),
__N_SSG,
this.locale,
this.defaultLocale
this.locale
)
}

Expand Down
41 changes: 41 additions & 0 deletions packages/next/next-server/server/config.ts
Expand Up @@ -227,6 +227,47 @@ function assignDefaults(userConfig: { [key: string]: any }) {
throw new Error(`Specified i18n.defaultLocale should be a string`)
}

if (typeof i18n.domains !== 'undefined' && !Array.isArray(i18n.domains)) {
throw new Error(
`Specified i18n.domains must be an array of domain objects e.g. [ { domain: 'example.fr', defaultLocale: 'fr', locales: ['fr'] } ] received ${typeof i18n.domains}`
)
}

if (i18n.domains) {
const invalidDomainItems = i18n.domains.filter((item: any) => {
if (!item || typeof item !== 'object') return true
if (!item.defaultLocale) return true
if (!item.domain || typeof item.domain !== 'string') return true
if (!item.locales || !Array.isArray(item.locales)) return true

const invalidLocales = item.locales.filter(
(locale: string) => !i18n.locales.includes(locale)
)

if (invalidLocales.length > 0) {
console.error(
`i18n.domains item "${
item.domain
}" has the following locales (${invalidLocales.join(
', '
)}) 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`
)
return true
}
return false
})

if (invalidDomainItems.length > 0) {
throw new Error(
`Invalid i18n.domains values:\n${invalidDomainItems
.map((item: any) => JSON.stringify(item))
.join(
'\n'
)}\n\ndomains value must follow format { domain: 'example.fr', defaultLocale: 'fr', locales: ['fr'] }`
)
}
}

if (!Array.isArray(i18n.locales)) {
throw new Error(
`Specified i18n.locales must be an array of locale strings e.g. ["en-US", "nl-NL"] received ${typeof i18n.locales}`
Expand Down

0 comments on commit 5cab03f

Please sign in to comment.