Skip to content

Commit

Permalink
Update to have default locale matched on root (#17669)
Browse files Browse the repository at this point in the history
Follow-up PR to #17370 when the path is not prefixed with a locale and the default locale is the detected locale it doesn't redirect to locale prefixed variant. If the default locale path is visited and the default locale is visited this also redirects to the root removing the un-necessary locale in the URL. 

This also exposes the `defaultLocale` on the router since the RFC mentions `Setting a defaultLocale is required in every i18n library so it'd be useful for Next.js to provide it to the application.` although doesn't explicitly spec where we want to expose it. If we want to expose it differently this can be updated.
  • Loading branch information
ijjk committed Oct 8, 2020
1 parent a79bcfb commit bbc1a21
Show file tree
Hide file tree
Showing 15 changed files with 331 additions and 51 deletions.
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

0 comments on commit bbc1a21

Please sign in to comment.