未验证 提交 e554a1fb 编写于 作者: J JJ Kasper 提交者: GitHub

Add locale prop for transitioning locales client side (#17898)

This adds the `locale` prop for `next/link` to allow transitioning between locales client-side and also allows passing the locale to `router.push/replace` via the transition options similar to `shallow` e.g. `router.push('/another', '/another, { locale: 'nl' })`

x-ref: https://github.com/vercel/next.js/pull/17370
上级 245499a0
......@@ -558,7 +558,7 @@ const nextServerlessLoader: loader.Loader = function () {
isDataReq: _nextData,
locale: detectedLocale,
locales,
defaultLocale,
defaultLocale: i18n.defaultLocale,
},
options,
)
......
......@@ -26,6 +26,7 @@ export type LinkProps = {
shallow?: boolean
passHref?: boolean
prefetch?: boolean
locale?: string
}
type LinkPropsRequired = RequiredKeys<LinkProps>
type LinkPropsOptional = OptionalKeys<LinkProps>
......@@ -125,7 +126,8 @@ function linkClicked(
as: string,
replace?: boolean,
shallow?: boolean,
scroll?: boolean
scroll?: boolean,
locale?: string
): void {
const { nodeName } = e.currentTarget
......@@ -142,7 +144,7 @@ function linkClicked(
}
// replace state instead of push if prop is present
router[replace ? 'replace' : 'push'](href, as, { shallow }).then(
router[replace ? 'replace' : 'push'](href, as, { shallow, locale }).then(
(success: boolean) => {
if (!success) return
if (scroll) {
......@@ -202,21 +204,28 @@ function Link(props: React.PropsWithChildren<LinkProps>) {
shallow: true,
passHref: true,
prefetch: true,
locale: true,
} as const
const optionalProps: LinkPropsOptional[] = Object.keys(
optionalPropsGuard
) as LinkPropsOptional[]
optionalProps.forEach((key: LinkPropsOptional) => {
const valType = typeof props[key]
if (key === 'as') {
if (
props[key] &&
typeof props[key] !== 'string' &&
typeof props[key] !== 'object'
) {
if (props[key] && valType !== 'string' && valType !== 'object') {
throw createPropError({
key,
expected: '`string` or `object`',
actual: typeof props[key],
actual: valType,
})
}
} else if (key === 'locale') {
if (props[key] && valType !== 'string') {
throw createPropError({
key,
expected: '`string`',
actual: valType,
})
}
} else if (
......@@ -226,11 +235,11 @@ function Link(props: React.PropsWithChildren<LinkProps>) {
key === 'passHref' ||
key === 'prefetch'
) {
if (props[key] != null && typeof props[key] !== 'boolean') {
if (props[key] != null && valType !== 'boolean') {
throw createPropError({
key,
expected: '`boolean`',
actual: typeof props[key],
actual: valType,
})
}
} else {
......@@ -285,7 +294,7 @@ function Link(props: React.PropsWithChildren<LinkProps>) {
}
}, [p, childElm, href, as, router])
let { children, replace, shallow, scroll } = props
let { children, replace, shallow, scroll, locale } = props
// Deprecated. Warning shown by propType check. If the children provided is a string (<Link>example</Link>) we wrap it in an <a> tag
if (typeof children === 'string') {
children = <a>{children}</a>
......@@ -314,7 +323,7 @@ function Link(props: React.PropsWithChildren<LinkProps>) {
child.props.onClick(e)
}
if (!e.defaultPrevented) {
linkClicked(e, router, href, as, replace, shallow, scroll)
linkClicked(e, router, href, as, replace, shallow, scroll, locale)
}
},
}
......@@ -333,7 +342,11 @@ function Link(props: React.PropsWithChildren<LinkProps>) {
// 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, router && router.defaultLocale)
addLocale(
as,
locale || (router && router.locale),
router && router.defaultLocale
)
)
}
......
......@@ -29,6 +29,7 @@ import escapePathDelimiters from './utils/escape-path-delimiters'
interface TransitionOptions {
shallow?: boolean
locale?: string
}
interface NextHistoryState {
......@@ -592,6 +593,7 @@ export default class Router implements BaseRouter {
window.location.href = url
return false
}
this.locale = options.locale || this.locale
if (!(options as any)._h) {
this.isSsr = false
......
......@@ -197,6 +197,7 @@ export default class Server {
? requireFontManifest(this.distDir, this._isLikeServerless)
: null,
optimizeImages: this.nextConfig.experimental.optimizeImages,
defaultLocale: this.nextConfig.experimental.i18n?.defaultLocale,
}
// Only the `publicRuntimeConfig` key is exposed to the client side
......
import Link from 'next/link'
import { useRouter } from 'next/router'
export default function Page(props) {
const router = useRouter()
const { nextLocale } = router.query
return (
<>
<p id="links">links page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/another" locale={nextLocale}>
<a id="to-another">to /another</a>
</Link>
<br />
<Link href="/gsp" locale={nextLocale}>
<a id="to-gsp">to /gsp</a>
</Link>
<br />
<Link href="/gsp/fallback/first" locale={nextLocale}>
<a id="to-fallback-first">to /gsp/fallback/first</a>
</Link>
<br />
<Link href="/gsp/fallback/hello" locale={nextLocale}>
<a id="to-fallback-hello">to /gsp/fallback/hello</a>
</Link>
<br />
<Link href="/gsp/no-fallback/first" locale={nextLocale}>
<a id="to-no-fallback-first">to /gsp/no-fallback/first</a>
</Link>
<br />
<Link href="/gssp" locale={nextLocale}>
<a id="to-gssp">to /gssp</a>
</Link>
<br />
<Link href="/gssp/first" locale={nextLocale}>
<a id="to-gssp-slug">to /gssp/first</a>
</Link>
<br />
</>
)
}
// make SSR page so we have query values immediately
export const getServerSideProps = () => {
return {
props: {},
}
}
......@@ -26,7 +26,170 @@ let appPort
const locales = ['en-US', 'nl-NL', 'nl-BE', 'nl', 'fr-BE', 'fr', 'en']
async function addDefaultLocaleCookie(browser) {
// make sure default locale is used in case browser isn't set to
// favor en-US by default, (we use all caps to ensure it's case-insensitive)
await browser.manage().addCookie({ name: 'NEXT_LOCALE', value: 'EN-US' })
await browser.get(browser.initUrl)
}
function runTests(isDev) {
it('should navigate with locale prop correctly', async () => {
const browser = await webdriver(appPort, '/links?nextLocale=fr')
await addDefaultLocaleCookie(browser)
expect(await browser.elementByCss('#router-pathname').text()).toBe('/links')
expect(await browser.elementByCss('#router-as-path').text()).toBe(
'/links?nextLocale=fr'
)
expect(await browser.elementByCss('#router-locale').text()).toBe('en-US')
expect(
JSON.parse(await browser.elementByCss('#router-locales').text())
).toEqual(locales)
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({ nextLocale: 'fr' })
await browser.elementByCss('#to-another').click()
await browser.waitForElementByCss('#another')
expect(await browser.elementByCss('#router-pathname').text()).toBe(
'/another'
)
expect(await browser.elementByCss('#router-as-path').text()).toBe(
'/another'
)
expect(await browser.elementByCss('#router-locale').text()).toBe('fr')
expect(
JSON.parse(await browser.elementByCss('#router-locales').text())
).toEqual(locales)
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({})
let parsedUrl = url.parse(await browser.eval('window.location.href'), true)
expect(parsedUrl.pathname).toBe('/fr/another')
expect(parsedUrl.query).toEqual({})
await browser.eval('window.history.back()')
await browser.waitForElementByCss('#links')
expect(await browser.elementByCss('#router-pathname').text()).toBe('/links')
expect(await browser.elementByCss('#router-as-path').text()).toBe(
'/links?nextLocale=fr'
)
expect(await browser.elementByCss('#router-locale').text()).toBe('fr')
expect(
JSON.parse(await browser.elementByCss('#router-locales').text())
).toEqual(locales)
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({ nextLocale: 'fr' })
parsedUrl = url.parse(await browser.eval('window.location.href'), true)
expect(parsedUrl.pathname).toBe('/fr/links')
expect(parsedUrl.query).toEqual({ nextLocale: 'fr' })
await browser.eval('window.history.forward()')
await browser.waitForElementByCss('#another')
expect(await browser.elementByCss('#router-pathname').text()).toBe(
'/another'
)
expect(await browser.elementByCss('#router-as-path').text()).toBe(
'/another'
)
expect(await browser.elementByCss('#router-locale').text()).toBe('fr')
expect(
JSON.parse(await browser.elementByCss('#router-locales').text())
).toEqual(locales)
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({})
parsedUrl = url.parse(await browser.eval('window.location.href'), true)
expect(parsedUrl.pathname).toBe('/fr/another')
expect(parsedUrl.query).toEqual({})
})
it('should navigate with locale prop correctly GSP', async () => {
const browser = await webdriver(appPort, '/links?nextLocale=nl')
await addDefaultLocaleCookie(browser)
expect(await browser.elementByCss('#router-pathname').text()).toBe('/links')
expect(await browser.elementByCss('#router-as-path').text()).toBe(
'/links?nextLocale=nl'
)
expect(await browser.elementByCss('#router-locale').text()).toBe('en-US')
expect(
JSON.parse(await browser.elementByCss('#router-locales').text())
).toEqual(locales)
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({ nextLocale: 'nl' })
await browser.elementByCss('#to-fallback-first').click()
await browser.waitForElementByCss('#gsp')
expect(await browser.elementByCss('#router-pathname').text()).toBe(
'/gsp/fallback/[slug]'
)
expect(await browser.elementByCss('#router-as-path').text()).toBe(
'/gsp/fallback/first'
)
expect(await browser.elementByCss('#router-locale').text()).toBe('nl')
expect(
JSON.parse(await browser.elementByCss('#router-locales').text())
).toEqual(locales)
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({ slug: 'first' })
let parsedUrl = url.parse(await browser.eval('window.location.href'), true)
expect(parsedUrl.pathname).toBe('/nl/gsp/fallback/first')
expect(parsedUrl.query).toEqual({})
await browser.eval('window.history.back()')
await browser.waitForElementByCss('#links')
expect(await browser.elementByCss('#router-pathname').text()).toBe('/links')
expect(await browser.elementByCss('#router-as-path').text()).toBe(
'/links?nextLocale=nl'
)
expect(await browser.elementByCss('#router-locale').text()).toBe('nl')
expect(
JSON.parse(await browser.elementByCss('#router-locales').text())
).toEqual(locales)
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({ nextLocale: 'nl' })
parsedUrl = url.parse(await browser.eval('window.location.href'), true)
expect(parsedUrl.pathname).toBe('/nl/links')
expect(parsedUrl.query).toEqual({ nextLocale: 'nl' })
await browser.eval('window.history.forward()')
await browser.waitForElementByCss('#gsp')
expect(await browser.elementByCss('#router-pathname').text()).toBe(
'/gsp/fallback/[slug]'
)
expect(await browser.elementByCss('#router-as-path').text()).toBe(
'/gsp/fallback/first'
)
expect(await browser.elementByCss('#router-locale').text()).toBe('nl')
expect(
JSON.parse(await browser.elementByCss('#router-locales').text())
).toEqual(locales)
expect(
JSON.parse(await browser.elementByCss('#router-query').text())
).toEqual({ slug: 'first' })
parsedUrl = url.parse(await browser.eval('window.location.href'), true)
expect(parsedUrl.pathname).toBe('/nl/gsp/fallback/first')
expect(parsedUrl.query).toEqual({})
})
it('should update asPath on the client correctly', async () => {
for (const check of ['en', 'En']) {
const browser = await webdriver(appPort, `/${check}`)
......@@ -509,10 +672,7 @@ function runTests(isDev) {
it('should navigate client side for default locale with no prefix', async () => {
const browser = await webdriver(appPort, '/')
// make sure default locale is used in case browser isn't set to
// favor en-US by default, (we use all caps to ensure it's case-insensitive)
await browser.manage().addCookie({ name: 'NEXT_LOCALE', value: 'EN-US' })
await browser.get(browser.initUrl)
await addDefaultLocaleCookie(browser)
const checkIndexValues = async () => {
expect(await browser.elementByCss('#router-locale').text()).toBe('en-US')
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册