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

Update to have default locale matched on root #17669

Merged
merged 19 commits into from Oct 8, 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
17 changes: 14 additions & 3 deletions packages/next/build/webpack/loaders/next-serverless-loader.ts
Expand Up @@ -229,24 +229,34 @@ const nextServerlessLoader: loader.Loader = function () {
detectedLocale = accept.language(
req.headers['accept-language'],
i18n.locales
) || i18n.defaultLocale
)
}

const denormalizedPagePath = denormalizePagePath(parsedUrl.pathname || '/')
const detectedDefaultLocale = detectedLocale === i18n.defaultLocale
const shouldStripDefaultLocale =
detectedDefaultLocale &&
denormalizedPagePath === \`/\${i18n.defaultLocale}\`
const shouldAddLocalePrefix =
!detectedDefaultLocale && denormalizedPagePath === '/'
detectedLocale = detectedLocale || i18n.defaultLocale

if (
!nextStartMode &&
i18n.localeDetection !== false &&
denormalizePagePath(parsedUrl.pathname || '/') === '/'
(shouldAddLocalePrefix || shouldStripDefaultLocale)
) {
res.setHeader(
'Location',
formatUrl({
// make sure to include any query values when redirecting
...parsedUrl,
pathname: \`/\${detectedLocale}\`,
pathname: shouldStripDefaultLocale ? '/' : \`/\${detectedLocale}\`,
})
)
res.statusCode = 307
res.end()
return
}

// TODO: domain based locales (domain to locale mapping needs to be provided in next.config.js)
Expand Down Expand Up @@ -458,6 +468,7 @@ const nextServerlessLoader: loader.Loader = function () {
isDataReq: _nextData,
locale: detectedLocale,
locales: i18n.locales,
defaultLocale: i18n.defaultLocale,
},
options,
)
Expand Down
2 changes: 2 additions & 0 deletions packages/next/client/index.tsx
Expand Up @@ -65,6 +65,7 @@ const {
isFallback,
head: initialHeadData,
locales,
defaultLocale,
} = data

let { locale } = data
Expand Down Expand Up @@ -317,6 +318,7 @@ export default async (opts: { webpackHMR?: any } = {}) => {
render({ App, Component, styleSheets, props, err }),
locale,
locales,
defaultLocale,
})

// call init-client middleware
Expand Down
4 changes: 3 additions & 1 deletion packages/next/client/link.tsx
Expand Up @@ -332,7 +332,9 @@ function Link(props: React.PropsWithChildren<LinkProps>) {
// If child is an <a> tag and doesn't have a href attribute, or if the 'passHref' property is
// defined, we specify the current 'href', so that repetition is not needed by the user
if (props.passHref || (child.type === 'a' && !('href' in child.props))) {
childProps.href = addBasePath(addLocale(as, router && router.locale))
childProps.href = addBasePath(
addLocale(as, router && router.locale, router && router.defaultLocale)
)
}

return React.cloneElement(child, childProps)
Expand Down
29 changes: 25 additions & 4 deletions packages/next/client/page-loader.ts
Expand Up @@ -203,13 +203,23 @@ 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) {
getDataHref(
href: string,
asPath: string,
ssg: boolean,
locale?: string,
defaultLocale?: 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)
const dataRoute = addLocale(
getAssetPathFromRoute(path, '.json'),
locale,
defaultLocale
)
return addBasePath(
`/_next/data/${this.buildId}${dataRoute}${ssg ? '' : search}`
)
Expand All @@ -229,15 +239,26 @@ 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) {
prefetchData(
href: string,
asPath: string,
locale?: string,
defaultLocale?: 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)) &&
(_dataHref = this.getDataHref(
href,
asPath,
true,
locale,
defaultLocale
)) &&
// noop when data has already been prefetched (dedupe)
!document.querySelector(
`link[rel="${relPrefetch}"][href^="${_dataHref}"]`
Expand Down
1 change: 1 addition & 0 deletions packages/next/client/router.ts
Expand Up @@ -39,6 +39,7 @@ const urlPropertyFields = [
'basePath',
'locale',
'locales',
'defaultLocale',
]
const routerEvents = [
'routeChangeStart',
Expand Down
7 changes: 5 additions & 2 deletions packages/next/export/index.ts
Expand Up @@ -283,6 +283,8 @@ export default async function exportApp(
}
}

const { i18n } = nextConfig.experimental

// Start the rendering process
const renderOpts = {
dir,
Expand All @@ -298,8 +300,9 @@ export default async function exportApp(
ampValidatorPath: nextConfig.experimental.amp?.validator || undefined,
ampSkipValidation: nextConfig.experimental.amp?.skipValidation || false,
ampOptimizerConfig: nextConfig.experimental.amp?.optimizer || undefined,
locales: nextConfig.experimental.i18n?.locales,
locale: nextConfig.experimental.i18n?.defaultLocale,
locales: i18n?.locales,
locale: i18n.defaultLocale,
defaultLocale: i18n.defaultLocale,
}

const { serverRuntimeConfig, publicRuntimeConfig } = nextConfig
Expand Down
8 changes: 6 additions & 2 deletions packages/next/next-server/lib/i18n/normalize-locale-path.ts
Expand Up @@ -6,10 +6,14 @@ export function normalizeLocalePath(
pathname: string
} {
let detectedLocale: string | undefined
// first item will be empty string from splitting at first char
const pathnameParts = pathname.split('/')

;(locales || []).some((locale) => {
if (pathname.startsWith(`/${locale}`)) {
if (pathnameParts[1] === locale) {
detectedLocale = locale
pathname = pathname.replace(new RegExp(`^/${locale}`), '') || '/'
pathnameParts.splice(1, 1)
pathname = pathnameParts.join('/') || '/'
return true
}
return false
Expand Down
32 changes: 26 additions & 6 deletions packages/next/next-server/lib/router/router.ts
Expand Up @@ -55,9 +55,13 @@ function addPathPrefix(path: string, prefix?: string) {
: path
}

export function addLocale(path: string, locale?: string) {
export function addLocale(
path: string,
locale?: string,
defaultLocale?: string
) {
if (process.env.__NEXT_i18n_SUPPORT) {
return locale && !path.startsWith('/' + locale)
return locale && locale !== defaultLocale && !path.startsWith('/' + locale)
? addPathPrefix(path, '/' + locale)
: path
}
Expand Down Expand Up @@ -246,6 +250,7 @@ export type BaseRouter = {
basePath: string
locale?: string
locales?: string[]
defaultLocale?: string
}

export type NextRouter = BaseRouter &
Expand Down Expand Up @@ -356,6 +361,7 @@ export default class Router implements BaseRouter {
_shallow?: boolean
locale?: string
locales?: string[]
defaultLocale?: string

static events: MittEmitter = mitt()

Expand All @@ -375,6 +381,7 @@ export default class Router implements BaseRouter {
isFallback,
locale,
locales,
defaultLocale,
}: {
subscription: Subscription
initialProps: any
Expand All @@ -387,6 +394,7 @@ export default class Router implements BaseRouter {
isFallback: boolean
locale?: string
locales?: string[]
defaultLocale?: string
}
) {
// represents the current component key
Expand Down Expand Up @@ -440,6 +448,7 @@ export default class Router implements BaseRouter {
if (process.env.__NEXT_i18n_SUPPORT) {
this.locale = locale
this.locales = locales
this.defaultLocale = defaultLocale
}

if (typeof window !== 'undefined') {
Expand Down Expand Up @@ -596,7 +605,7 @@ export default class Router implements BaseRouter {
this.abortComponentLoad(this._inFlightRoute)
}

as = addLocale(as, this.locale)
as = addLocale(as, this.locale, this.defaultLocale)
const cleanedAs = delLocale(
hasBasePath(as) ? delBasePath(as) : as,
this.locale
Expand Down Expand Up @@ -790,7 +799,12 @@ export default class Router implements BaseRouter {
}

Router.events.emit('beforeHistoryChange', as)
this.changeState(method, url, addLocale(as, this.locale), options)
this.changeState(
method,
url,
addLocale(as, this.locale, this.defaultLocale),
options
)

if (process.env.NODE_ENV !== 'production') {
const appComp: any = this.components['/_app'].Component
Expand Down Expand Up @@ -960,7 +974,8 @@ export default class Router implements BaseRouter {
formatWithValidation({ pathname, query }),
delBasePath(as),
__N_SSG,
this.locale
this.locale,
this.defaultLocale
)
}

Expand Down Expand Up @@ -1117,7 +1132,12 @@ export default class Router implements BaseRouter {

const route = removePathTrailingSlash(pathname)
await Promise.all([
this.pageLoader.prefetchData(url, asPath),
this.pageLoader.prefetchData(
url,
asPath,
this.locale,
this.defaultLocale
),
this.pageLoader[options.priority ? 'loadPage' : 'prefetch'](route),
])
}
Expand Down
1 change: 1 addition & 0 deletions packages/next/next-server/lib/utils.ts
Expand Up @@ -103,6 +103,7 @@ export type NEXT_DATA = {
head: HeadEntry[]
locale?: string
locales?: string[]
defaultLocale?: string
}

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

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}`
)
}

const invalidLocales = i18n.locales.filter(
(locale: any) => typeof locale !== 'string'
)

if (invalidLocales.length > 0) {
throw new Error(
`Specified i18n.locales contains invalid values, locales must be valid locale tags provided as strings e.g. "en-US".\n` +
`See here for list of valid language sub-tags: http://www.iana.org/assignments/language-subtag-registry/language-subtag-registry`
)
}

if (!i18n.locales.includes(i18n.defaultLocale)) {
throw new Error(
`Specified i18n.defaultLocale should be included in i18n.locales`
)
}

// make sure default Locale is at the front
i18n.locales = [
i18n.defaultLocale,
...i18n.locales.filter((locale: string) => locale !== i18n.defaultLocale),
]

const localeDetectionType = typeof i18n.locales.localeDetection

if (
Expand Down