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 locale prop for transitioning locales client side #17898

Merged
merged 1 commit into from Oct 15, 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
Expand Up @@ -558,7 +558,7 @@ const nextServerlessLoader: loader.Loader = function () {
isDataReq: _nextData,
locale: detectedLocale,
locales,
defaultLocale,
defaultLocale: i18n.defaultLocale,
},
options,
)
Expand Down
39 changes: 26 additions & 13 deletions packages/next/client/link.tsx
Expand Up @@ -26,6 +26,7 @@ export type LinkProps = {
shallow?: boolean
passHref?: boolean
prefetch?: boolean
locale?: string
}
type LinkPropsRequired = RequiredKeys<LinkProps>
type LinkPropsOptional = OptionalKeys<LinkProps>
Expand Down Expand Up @@ -125,7 +126,8 @@ function linkClicked(
as: string,
replace?: boolean,
shallow?: boolean,
scroll?: boolean
scroll?: boolean,
locale?: string
): void {
const { nodeName } = e.currentTarget

Expand All @@ -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) {
Expand Down Expand Up @@ -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 (
Expand All @@ -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 {
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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)
}
},
}
Expand All @@ -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
)
)
}

Expand Down
2 changes: 2 additions & 0 deletions packages/next/next-server/lib/router/router.ts
Expand Up @@ -29,6 +29,7 @@ import escapePathDelimiters from './utils/escape-path-delimiters'

interface TransitionOptions {
shallow?: boolean
locale?: string
}

interface NextHistoryState {
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/next/next-server/server/next-server.ts
Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions test/integration/i18n-support/pages/links.js
@@ -0,0 +1,54 @@
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: {},
}
}
168 changes: 164 additions & 4 deletions test/integration/i18n-support/test/index.test.js
Expand Up @@ -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}`)
Expand Down Expand Up @@ -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')
Expand Down