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

Add handling for domain to locale mapping #17771

Merged
merged 3 commits into from Oct 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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