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 initial changes for i18n support #17370

Merged
merged 16 commits into from Oct 7, 2020
Merged
Show file tree
Hide file tree
Changes from 9 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
4 changes: 3 additions & 1 deletion packages/next/build/index.ts
Expand Up @@ -563,7 +563,9 @@ export default async function build(
let workerResult = await staticCheckWorkers.isPageStatic(
page,
serverBundle,
runtimeEnvConfig
runtimeEnvConfig,
config.experimental.i18n?.locales,
config.experimental.i18n?.defaultLocale
)

if (workerResult.isHybridAmp) {
Expand Down
42 changes: 36 additions & 6 deletions packages/next/build/utils.ts
Expand Up @@ -27,6 +27,7 @@ import { denormalizePagePath } from '../next-server/server/normalize-page-path'
import { BuildManifest } from '../next-server/server/get-page-files'
import { removePathTrailingSlash } from '../client/normalize-trailing-slash'
import type { UnwrapPromise } from '../lib/coalesced-function'
import { normalizeLocalePath } from '../next-server/lib/i18n/normalize-locale-path'

const fileGzipStats: { [k: string]: Promise<number> } = {}
const fsStatGzip = (file: string) => {
Expand Down Expand Up @@ -530,7 +531,9 @@ export async function getJsPageSizeInKb(

export async function buildStaticPaths(
page: string,
getStaticPaths: GetStaticPaths
getStaticPaths: GetStaticPaths,
locales?: string[],
defaultLocale?: string
): Promise<
Omit<UnwrapPromise<ReturnType<GetStaticPaths>>, 'paths'> & { paths: string[] }
> {
Expand Down Expand Up @@ -595,7 +598,17 @@ export async function buildStaticPaths(
// route.
if (typeof entry === 'string') {
entry = removePathTrailingSlash(entry)
const result = _routeMatcher(entry)

const localePathResult = normalizeLocalePath(entry, locales)
let cleanedEntry = entry

if (localePathResult.detectedLocale) {
cleanedEntry = entry.substr(localePathResult.detectedLocale.length + 1)
} else if (defaultLocale) {
entry = `/${defaultLocale}${entry}`
}

const result = _routeMatcher(cleanedEntry)
if (!result) {
throw new Error(
`The provided path \`${entry}\` does not match the page: \`${page}\`.`
Expand All @@ -607,7 +620,10 @@ export async function buildStaticPaths(
// For the object-provided path, we must make sure it specifies all
// required keys.
else {
const invalidKeys = Object.keys(entry).filter((key) => key !== 'params')
const invalidKeys = Object.keys(entry).filter(
(key) => key !== 'params' && key !== 'locale'
)

if (invalidKeys.length) {
throw new Error(
`Additional keys were returned from \`getStaticPaths\` in page "${page}". ` +
Expand Down Expand Up @@ -657,7 +673,14 @@ export async function buildStaticPaths(
.replace(/(?!^)\/$/, '')
})

prerenderPaths?.add(builtPage)
if (entry.locale && !locales?.includes(entry.locale)) {
throw new Error(
`Invalid locale returned from getStaticPaths for ${page}, the locale ${entry.locale} is not specified in next.config.js`
)
}
const curLocale = entry.locale || defaultLocale || ''

prerenderPaths?.add(`${curLocale ? `/${curLocale}` : ''}${builtPage}`)
}
})

Expand All @@ -667,7 +690,9 @@ export async function buildStaticPaths(
export async function isPageStatic(
page: string,
serverBundle: string,
runtimeEnvConfig: any
runtimeEnvConfig: any,
locales?: string[],
defaultLocale?: string
): Promise<{
isStatic?: boolean
isAmpOnly?: boolean
Expand Down Expand Up @@ -755,7 +780,12 @@ export async function isPageStatic(
;({
paths: prerenderRoutes,
fallback: prerenderFallback,
} = await buildStaticPaths(page, mod.getStaticPaths))
} = await buildStaticPaths(
page,
mod.getStaticPaths,
locales,
defaultLocale
))
}

const config = mod.config || {}
Expand Down
23 changes: 22 additions & 1 deletion packages/next/client/index.tsx
Expand Up @@ -3,6 +3,7 @@ import '@next/polyfill-module'
import React from 'react'
import ReactDOM from 'react-dom'
import { HeadManagerContext } from '../next-server/lib/head-manager-context'
import { normalizeLocalePath } from '../next-server/lib/i18n/normalize-locale-path'
import mitt from '../next-server/lib/mitt'
import { RouterContext } from '../next-server/lib/router-context'
import type Router from '../next-server/lib/router/router'
Expand All @@ -11,7 +12,11 @@ import type {
AppProps,
PrivateRouteInfo,
} from '../next-server/lib/router/router'
import { delBasePath, hasBasePath } from '../next-server/lib/router/router'
import {
delBasePath,
hasBasePath,
delLocale,
} 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 @@ -60,8 +65,11 @@ const {
dynamicIds,
isFallback,
head: initialHeadData,
locales,
} = data

let { locale } = data

const prefix = assetPrefix || ''

// With dynamic assetPrefix it's no longer possible to set assetPrefix at the build time
Expand All @@ -80,6 +88,17 @@ if (hasBasePath(asPath)) {
asPath = delBasePath(asPath)
}

asPath = delLocale(asPath, locale)
timneutkens marked this conversation as resolved.
Show resolved Hide resolved

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

if (localePathResult.detectedLocale) {
asPath = asPath.substr(localePathResult.detectedLocale.length + 1)
locale = localePathResult.detectedLocale
}
}

type RegisterFn = (input: [string, () => void]) => void

const pageLoader = new PageLoader(buildId, prefix, page)
Expand Down Expand Up @@ -291,6 +310,8 @@ export default async (opts: { webpackHMR?: any } = {}) => {
isFallback: Boolean(isFallback),
subscription: ({ Component, styleSheets, props, err }, App) =>
render({ App, Component, styleSheets, props, err }),
locale,
locales,
})

// call init-client middleware
Expand Down
3 changes: 2 additions & 1 deletion packages/next/client/link.tsx
Expand Up @@ -2,6 +2,7 @@ import React, { Children } from 'react'
import { UrlObject } from 'url'
import {
addBasePath,
addLocale,
isLocalURL,
NextRouter,
PrefetchOptions,
Expand Down Expand Up @@ -331,7 +332,7 @@ 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(as)
childProps.href = addBasePath(addLocale(as, router && router.locale))
}

return React.cloneElement(child, childProps)
Expand Down
5 changes: 3 additions & 2 deletions packages/next/client/page-loader.ts
Expand Up @@ -7,6 +7,7 @@ import {
addBasePath,
markLoadingError,
interpolateAs,
addLocale,
} from '../next-server/lib/router/router'

import getAssetPathFromRoute from '../next-server/lib/router/utils/get-asset-path-from-route'
Expand Down Expand Up @@ -202,13 +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) {
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 = getAssetPathFromRoute(path, '.json')
const dataRoute = addLocale(getAssetPathFromRoute(path, '.json'), locale)
return addBasePath(
`/_next/data/${this.buildId}${dataRoute}${ssg ? '' : search}`
)
Expand Down
7 changes: 6 additions & 1 deletion packages/next/client/router.ts
Expand Up @@ -37,6 +37,8 @@ const urlPropertyFields = [
'components',
'isFallback',
'basePath',
'locale',
'locales',
]
const routerEvents = [
'routeChangeStart',
Expand Down Expand Up @@ -144,7 +146,10 @@ export function makePublicRouterInstance(router: Router): NextRouter {

for (const property of urlPropertyFields) {
if (typeof _router[property] === 'object') {
instance[property] = Object.assign({}, _router[property]) // makes sure query is not stateful
instance[property] = Object.assign(
Array.isArray(_router[property]) ? [] : {},
_router[property]
) // makes sure query is not stateful
continue
}

Expand Down
2 changes: 2 additions & 0 deletions packages/next/export/index.ts
Expand Up @@ -281,6 +281,8 @@ 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,
}

const { serverRuntimeConfig, publicRuntimeConfig } = nextConfig
Expand Down
10 changes: 10 additions & 0 deletions packages/next/export/worker.ts
Expand Up @@ -15,6 +15,7 @@ import { ComponentType } from 'react'
import { GetStaticProps } from '../types'
import { requireFontManifest } from '../next-server/server/require'
import { FontManifest } from '../next-server/server/font-utils'
import { normalizeLocalePath } from '../next-server/lib/i18n/normalize-locale-path'

const envConfig = require('../next-server/lib/runtime-config')

Expand Down Expand Up @@ -67,6 +68,8 @@ interface RenderOpts {
optimizeFonts?: boolean
optimizeImages?: boolean
fontManifest?: FontManifest
locales?: string[]
locale?: string
}

type ComponentModule = ComponentType<{}> & {
Expand Down Expand Up @@ -100,6 +103,13 @@ export default async function exportPage({
let query = { ...originalQuery }
let params: { [key: string]: string | string[] } | undefined

const localePathResult = normalizeLocalePath(path, renderOpts.locales)

if (localePathResult.detectedLocale) {
path = localePathResult.pathname
renderOpts.locale = localePathResult.detectedLocale
}

// We need to show a warning if they try to provide query values
// for an auto-exported page since they won't be available
const hasOrigQueryValues = Object.keys(originalQuery).length > 0
Expand Down
19 changes: 19 additions & 0 deletions packages/next/next-server/lib/i18n/detect-locale-cookie.ts
@@ -0,0 +1,19 @@
import { IncomingMessage } from 'http'
import cookie from 'next/dist/compiled/cookie'

export function detectLocaleCookie(req: IncomingMessage, locales: string[]) {
let detectedLocale: string | undefined

if (req.headers.cookie && req.headers.cookie.includes('NEXT_LOCALE')) {
const header = req.headers.cookie
const { NEXT_LOCALE } = cookie.parse(
ijjk marked this conversation as resolved.
Show resolved Hide resolved
Array.isArray(header) ? header.join(';') : header
)

if (locales.some((locale: string) => NEXT_LOCALE === locale)) {
detectedLocale = NEXT_LOCALE
}
}

return detectedLocale
}
22 changes: 22 additions & 0 deletions packages/next/next-server/lib/i18n/normalize-locale-path.ts
@@ -0,0 +1,22 @@
export function normalizeLocalePath(
pathname: string,
locales?: string[]
): {
detectedLocale?: string
pathname: string
} {
let detectedLocale: string | undefined
;(locales || []).some((locale) => {
if (pathname.startsWith(`/${locale}`)) {
detectedLocale = locale
pathname = pathname.replace(new RegExp(`^/${locale}`), '') || '/'
return true
}
return false
})

return {
pathname,
detectedLocale,
}
}