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 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
3 changes: 3 additions & 0 deletions packages/next/build/entries.ts
Expand Up @@ -97,6 +97,9 @@ export function createEntrypoints(
loadedEnvFiles: Buffer.from(JSON.stringify(loadedEnvFiles)).toString(
'base64'
),
i18n: config.experimental.i18n
? JSON.stringify(config.experimental.i18n)
: '',
}

Object.keys(pages).forEach((page) => {
Expand Down
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
3 changes: 3 additions & 0 deletions packages/next/build/webpack-config.ts
Expand Up @@ -986,6 +986,9 @@ export default async function getBaseWebpackConfig(
),
'process.env.__NEXT_ROUTER_BASEPATH': JSON.stringify(config.basePath),
'process.env.__NEXT_HAS_REWRITES': JSON.stringify(hasRewrites),
'process.env.__NEXT_i18n_SUPPORT': JSON.stringify(
!!config.experimental.i18n
),
...(isServer
? {
// Fix bad-actors in the npm ecosystem (e.g. `node-formidable`)
Expand Down
64 changes: 64 additions & 0 deletions packages/next/build/webpack/loaders/next-serverless-loader.ts
Expand Up @@ -28,6 +28,7 @@ export type ServerlessLoaderQuery = {
runtimeConfig: string
previewProps: string
loadedEnvFiles: string
i18n: string
}

const vercelHeader = 'x-vercel-id'
Expand All @@ -49,6 +50,7 @@ const nextServerlessLoader: loader.Loader = function () {
runtimeConfig,
previewProps,
loadedEnvFiles,
i18n,
}: ServerlessLoaderQuery =
typeof this.query === 'string' ? parse(this.query.substr(1)) : this.query

Expand All @@ -66,6 +68,8 @@ const nextServerlessLoader: loader.Loader = function () {
JSON.parse(previewProps) as __ApiPreviewProps
)

const i18nEnabled = !!i18n

const defaultRouteRegex = pageIsDynamicRoute
? `
const defaultRouteRegex = getRouteRegex("${page}")
Expand Down Expand Up @@ -212,6 +216,58 @@ const nextServerlessLoader: loader.Loader = function () {
`
: ''

const handleLocale = i18nEnabled
? `
// get pathname from URL with basePath stripped for locale detection
const i18n = ${i18n}
const accept = require('@hapi/accept')
const { detectLocaleCookie } = require('next/dist/next-server/lib/i18n/detect-locale-cookie')
const { normalizeLocalePath } = require('next/dist/next-server/lib/i18n/normalize-locale-path')
let detectedLocale = detectLocaleCookie(req, i18n.locales)

if (!detectedLocale) {
detectedLocale = accept.language(
req.headers['accept-language'],
i18n.locales
) || i18n.defaultLocale
}

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

// TODO: domain based locales (domain to locale mapping needs to be provided in next.config.js)
const localePathResult = normalizeLocalePath(parsedUrl.pathname, i18n.locales)

if (localePathResult.detectedLocale) {
detectedLocale = localePathResult.detectedLocale
req.url = formatUrl({
...parsedUrl,
pathname: localePathResult.pathname,
})
parsedUrl.pathname = localePathResult.pathname
}

detectedLocale = detectedLocale || i18n.defaultLocale
`
: `
const i18n = {}
const detectedLocale = undefined
`

if (page.match(API_ROUTE)) {
return `
import initServer from 'next-plugin-loader?middleware=on-init-server!'
Expand Down Expand Up @@ -305,6 +361,7 @@ const nextServerlessLoader: loader.Loader = function () {
const { renderToHTML } = require('next/dist/next-server/server/render');
const { tryGetPreviewData } = require('next/dist/next-server/server/api-utils');
const { denormalizePagePath } = require('next/dist/next-server/server/denormalize-page-path')
const { setLazyProp, getCookieParser } = require('next/dist/next-server/server/api-utils')
const {sendPayload} = require('next/dist/next-server/server/send-payload');
const buildManifest = require('${buildManifest}');
const reactLoadableManifest = require('${reactLoadableManifest}');
Expand Down Expand Up @@ -338,6 +395,9 @@ const nextServerlessLoader: loader.Loader = function () {
export const _app = App
export async function renderReqToHTML(req, res, renderMode, _renderOpts, _params) {
const fromExport = renderMode === 'export' || renderMode === true;
const nextStartMode = renderMode === 'passthrough'

setLazyProp({ req }, 'cookies', getCookieParser(req))

const options = {
App,
Expand Down Expand Up @@ -388,12 +448,16 @@ const nextServerlessLoader: loader.Loader = function () {
routeNoAssetPath = parsedUrl.pathname
}

${handleLocale}

const renderOpts = Object.assign(
{
Component,
pageConfig: config,
nextExport: fromExport,
isDataReq: _nextData,
locale: detectedLocale,
locales: i18n.locales,
},
options,
)
Expand Down
28 changes: 27 additions & 1 deletion packages/next/client/index.tsx
Expand Up @@ -11,7 +11,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 +64,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 +87,23 @@ if (hasBasePath(asPath)) {
asPath = delBasePath(asPath)
}

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

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

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 +315,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 @@ -298,6 +298,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