Translated routing and more for Next /pages
router using Next regular file-base routing system
\_app
component with the withTranslateRoutes
hocnext-translate-routes/link
instead of next/link
next-translate-routes/router instead
of next/router
for singleton router (default export)/contact-us
and /fr/contactez-nous
./:id{-:slug}?/
/:id(\\d+)/
/fr/english/path
redirects to /fr/french/path
See it in action: https://codesandbox.io/s/github/hozana/next-translate-routes/tree/master
/app
router is not supported: with /app
router, you can use next-roots.To build a fully internationalized website, one need to translate url segments: it is important for UX and SEO. For now, Next only ship with locale prefixes (see Next.js internationalized routing doc).
The next-routes package allow fully internationalized routing but it is no longer maintained, and it is designed without the latest Next api, such as internationalized routing, new data fetching methods, automatic static optimization.
To be able to use theses, a new approach is needed. Next.js provides it with Redirects and Rewrites, but:
/${localePrefix}/$(filePath)
to /${localePrefix}/$(localePath)
, but it creates a lot more complexity to write by hand and maintain.Check the example folder to see next-translate-routes in action. Some advanced techniques are shown there too: they may seem complicated but those 4 steps should cover most of the cases.
Import the withTranslateRoutes
from next-translate-routes/plugin
.
// next.config.js
const withTranslateRoutes = require('next-translate-routes/plugin')
module.exports = withTranslateRoutes({
// Next i18n config (mandatory): https://nextjs.org/docs/advanced-features/i18n-routing
i18n: {
locales: ['en', 'fr', 'es', 'pl'],
defaultLocale: 'pl',
},
// ...Remaining next config
})
You can add a _routes.json
, or a _routes.yaml
, file in the pages
folder, and in the every subfolder where you want to define routes.
Given a folder structure like so, you can add a _routes.json
in /pages/
and in /pages/section/
.
/pages/
├ section/
| ├ page1.tsx
| ├ page2.tsx
| └ _routes.json
├ somewhere/
| └ else.json
├ _app.tsx
├ about.tsx
├ contact.tsx
└ _routes.json
In /pages/section/
, the _routes.json
file could look like this.
// `/pages/section/_routes.json`
{
"/": {
"es": "seccion" // Folder path in es
},
"page1": {
"default": "article", // Overwrite the default page path (fallback)
"es": "articulo"
},
"page2": "definition" // Overwrite the page path for all language
}
The "/": { ... }
part define the folder paths, each other section define the paths of a page file in this folder:
/seccion/articulo
in es
, and at /section/article
in other languages,/seccion/definition
in es
, and at /section/definition
in other languages.You don't need a _routes.json
file in folder where you don't customize anything. If it is empty, then delete it.
Here, the /somewhere/
subfolder does not have any _routes.json
file.
Then, in /pages/
, the _routes.json
file could look like this.
// `/pages/_routes.json`
{
"/": {
"pt": "blog" // As we are in the root pages folder, this will add a "blog" path prefix for all pages in pt
},
"contact": {
"es": "contactar",
"pt": "contatar"
}
}
In the root pages folder, the "/": { ... }
part of the root _routes.json
allows to add a different basePath
for each language or only some language, like "blog" in pt
here:
/blog/about
in pt
and at /about
in other languages,/blog/contatar
in pt
, at /contactar
in es
and at /contact
in other languages,/blog/section/article
in pt
,/blog/section/definition
in pt
.\_app
component with the withTranslateRoutes
hoc// `/pages/_app.js`
import { withTranslateRoutes } from 'next-translate-routes'
import { App } from 'next/app'
export default withTranslateRoutes(App)
Or:
// `/pages/_app.js`
import { withTranslateRoutes } from 'next-translate-routes'
const App = ({ Component, pageProps }) => {
// Custom code...
return <Component {...pageProps} />
}
export default withTranslateRoutes(App)
next-translate-routes/link
instead of next/link
next-translate-routes extends Next Link to translate routes automatically: import it from 'next-translate-routes/link' instead of 'next/link' and use as you ever did.
import Link from 'next-translate-routes/link'
import React, { useEffect, useState } from 'react'
const MyLinks = (props) => {
const { locales } = useRouter()
return (
<>
<Link href="https://github.com/hozana/next-translate-routes/blob/master/file/path/to/page">Current locale</Link>
{locales.map((locale) => (
<Link
href={{ pathname: '/file/path/to/[dynamic]/page', query: { dynamic: props.param, otherQueryParam: 'foo' } }}
locale={locale}
key={locale}
>
{locale}
</Link>
))}
</>
)
}
next-translate-routes/router instead
of next/router
for singleton router (default export)You can use next-translate-routes/router
everywhere instead of next/router
but it is only necessary for the singleton router (which is rarely used).
import singletonRouter from 'next-translate-routes/router'
// Indead of:
import singletonRouter from 'next/router'
If en
is the default locale for example, you probably will want to:
/en
to /
and /en/anywhere
to /anywhere
,/
to /en
and /anywhere
to /en/anywhere
.This is complex: next-translate-routes cannot handle this without creating a looping redirect. The only way to do this seems to be using the middleware, as stated here.
Check the example folder to see some advanced techniques in action.
Next-translate-routes is configurable by adding a translateRoutes
key in Next config that accept an object of the following NTRConfig
type.
type NTRConfig = {
debug?: boolean
routesDataFileName?: string
routesTree?: TRouteBranch<L>
pagesDirectory?: string
}
translateRoutes: {
debug: true,
routesDataFileName: 'routesData',
}
When debug
is set to true, you will get some logs, both in the server terminal and in the browser console. By default, you will get some logs for each router.push
and router.replace
, but not router.prefetch
. To enable logs for router.prefetch
too, you can set debug to withPrefetch
.
If routesDataFileName
is defined, to 'routesData'
for example, next-translate-routes will look in the pages
folder for files named routesData.json
or routesData.yaml
instead of the default _routes.json
or _routes.yaml
.
If routesTree
is defined, next-translate-routes won't parse the pages
folder and will use the given object as the routes tree. If you uses it, beware of building correctly the routes tree to avoid bugs.
You can see and edit these while your app is running to debug things, using __NEXT_TRANSLATE_ROUTES_DATA
in the browser console. For exemple, executing __NEXT_TRANSLATE_ROUTES_DATA.debug = true
will activate the logs on router.push
and router.replace
.
Two helpers are exposed to translate/untranslate urls:
fileUrlToUrl
transforms a file url into a translated urlurlToFileUrl
transforms a translated url into a file urlBoth of them take 2 arguments: an url and a locale. fileUrlToUrl
can take an extra option argument to prevent it to throw and return undefined instead if the file url is not found: { throwOnError: false }
.
You will probably want to indicate alternate pages for SEO optimization. Here is how you can do that:
const { pathname, query, locale, locale } = useRouter()
return (
<Head>
{locales.map((l) => l !== locale && <link rel="alternate" hrefLang={l} href={fileUrlToUrl({ pathname, query }, l)} />)}
</Head>
)
You can do it in the _app
component if you are sure to do that for all your pages. You can also use a dedicated package, like next-seo.
See this article about alternate and canonical pages
See @JacbSoderblom's suggestion
// `/pages/blog/[id]/_routes.json`
{
"/": ":id(\\d+)", // Constrain a dynamic folder segment (to be a number here)
"[slug]": ":slug(\\w+)", // Constrain a dynamic page segment (to be letters here)
}
For a catch all route: "[...path]": ":path*"
.
You can use a constrained dynamic path segment in the root of your application too.
/pages/
├ [side]/
| ├ index.tsx
| └ _routes.yml
├ somewhere.tsx
└ _app.tsx
# /pages/[side]/_routes.yml
---
"/":
default: :side(heads|tails) # Important!
en: :side(heads|tails)
fr: :side(pile|face)
es: :side(cara|cruz)
This will ignore the blog
path segment:
// `/pages/blog/_routes.json`
{
"/": "."
}
It can be done for some lang only and not others.
// `/pages/blog/_routes.json`
{
"/": {
fr: "."
}
}
⚠️ Ignoring a path segment can cause troubles with the redirections.
Ex: given the /a/[b]/[c]
and /a/[b]/[c]/d
file paths where [b]
is ignored and the b param is merged with the c param: :b-:c
.
/a/:b/:c
=> /a/:b-:c
and /a/:b/:c/d
=> /a/:b-:c/d
Then /a/bb/11
will be redirected to /a/bb-11
and /a/bb/11/d
to /a/bb-11/d
and that is fine.
But then /a/bb-11/d
will match /a/:b-:c
and be redirected to /a/bb-11-d
and that is not fine!
To handle this case, one can add a path-to-regex pattern to the default ignore token: .(\\d+)
, or .(\[\^-\]+)
, or .(what|ever)
.
This path-to-regex pattern will be added after the segment name in the redirect.
/a/:b(\[\^-\]+)/:c
=> /a/:b-:c
and /a/:b(\[\^-\]+)/:c/d
=> /a/:b-:c/d
Then /a/bb-11/d
will no more match /a/[b]/[c]
(/a/:b(\[\^-\]+)/:c
). #ignorePattern
⚠️ This is only handled in default paths (i.e. "/": ".(\\d+)"
or "/": { "default": ".(\\d+)" }
), not in lang-specific paths.
// `/pages/blog/[id]/_routes.json`
{
"/": "article{-:id}?-view", // Add prefix, optional prefix, suffix
}
It is also possible to create a path segment with 2 dynamic parameters. Ex: /articles/:id{-:slug}?
.
First, create a path segment for each dynamic parameter: `/articles/[id]/[slug].
Then:
// `/articles/[id]/_routes.json`
{
"/": ".", // Ignore the [id] segment
"[slug]": ":id{-:slug}?" // Give the 2 params to the 2nd segment
}
If you want to avoid seeding _routes.json
files in your /pages
folder,
you can directly create a routesTree object, and inject it in the next config as follow.
// next.config.js
const withTranslateRoutes = require('next-translate-routes')
const getRoutesTree = require('./getRoutesTree')
const routesTree = getRoutesTree()
module.exports = withTranslateRoutes({
// Next i18n config (mandatory): https://nextjs.org/docs/advanced-features/i18n-routing
i18n: {
locales: ['en', 'fr', 'es', 'pl'],
defaultLocale: 'pl',
},
translateRoutes: {
routesTree,
},
// ...Remaining next config
})
routesTree
must be of type TRouteBranch
:
type TRouteBranch<Locale extends string> = {
name: string
paths: { default: string } & Partial<Record<Locale, string>>
children?: TRouteBranch[]
}
You might need to mock next-translate-routes outside Next, for example for testing or in Storybook.
First, you need to create next-translate-routes data. You can do it using the createNtrData
helper, but it only works in node environment. It takes the next config as first parameter. The second parameter is optional and allows to use a custom pages folder: if omitted, createNtrData
will look for you next pages
folder.
import { createNtrData } from 'next-translate-routes/plugin`
import nextConfig from '../next.config.js'
const ntrData = createNtrData(
nextConfig,
path.resolve(process.cwd(), './fixtures/pages'),
)
Then, if you want to render you app, you need to inject the router context, then (and only then) inject next-translate-routes. You can do it manually, or using next-tranlate-routes/loader
with Webpack.
You will have to execute createNtrData in a node script and store the result somewhere that can be imported.
// nextRouterMock.ts
import withTranslateRoutes from 'next-translate-routes'
import { RouterContext } from 'next/dist/shared/lib/router-context'
import ntrData from 'path/to/your/ntrData'
//[...]
const RouteTranslationsProvider = withTranslateRoutes(ntrData, ({ children }) => <>{children}</>)
const TranslatedRoutesProvider = ({ children }) => (
<RouterContext.Provider value={routerMock}>
<RouteTranslationsProvider router={routerMock}>{children}</RouteTranslationsProvider>
</RouterContext.Provider>
)
// [...]
For Storybook, this piece of code can be used to create a decorator function:
export const WithNextRouter: DecoratorFn = (Story, context): JSX.Element => (
<TranslatedRoutesProvider routerMock={createRouterFromContext(context)}>
<Story />
</TranslatedRoutesProvider>
)
next-translate-routes/loader
for Webpacknext-translate-routes/loader
allows to create next-translate-routes data at build time. So you can do exactly the same as described in the Manually paragraph above, but you don't need to create and add ntrData
as an argument to withTranslateRoutes
.
// nextRouterMock.ts
import withTranslateRoutes from 'next-translate-routes'
import { RouterContext } from 'next/dist/shared/lib/router-context'
//[...]
const RouteTranslationsProvider = withTranslateRoutes(({ children }) => <>{children}</>)
// TranslatedRoutesProvider is the same as in the manually paragraph above
// [...]
Then all you have to do is to add this rule in your webpack config:
// storybook/config/webpack.config.js for exemple
const { createNtrData } = require('next-translate-routes/plugin')
const nextConfig = require('../../next.config') // Your project nextConfig
module.exports = ({ config }) => {
// [...]
config.module.rules.push({
test: /path\/to\/nextRouterMock/, // ⚠️ Warning! This test should only match the file where withTranslateRoutes is used! If you cannot, set the mustMatch option to false.
use: {
loader: 'next-translate-routes/loader',
options: { data: createNtrData(nextConfig) },
},
})
return config
}
⚠️ Warning! The rule
test
should only match the file wherewithTranslateRoutes
is used! If you cannot, then set themustMatch
loader option tofalse
.
You can define fallback languages in next-translate-routes config as you would in i18next, using fallbackLng
, that can take either a string (ex: 'fr'
), an array (ex: ['fr', 'en']
), or an object (ex: { default: ['en'], 'de-CH': ['fr'] }
), but unlike i18next, fallbackLng
cannot be a function.
// next.config.js
const withTranslateRoutes = require('next-translate-routes')
module.exports = withTranslateRoutes({
// Next i18n config (mandatory): https://nextjs.org/docs/advanced-features/i18n-routing
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr', 'de', 'de-AT', 'de-DE', 'de-CH'],
},
translateRoutes: {
fallbackLng: {
default: ['en'],
'de-AT': ['de'],
'de-DE': ['de'],
'de-CH': ['de', 'fr'],
},
},
// ...Remaining next config
})
It can avoid having routes.json
files looking like:
{
"/": {
"de": "produkt",
"de-AT": "produkt",
"de-DE": "produkt",
"de-CH": "produkt"
}
}
Unfortunately, Next new middleware syntax (stable) has a bug when using a "matcher" and rewrites. You can keep track of this issue:
So if you want to use a middleware with Next >= 12.2.0, you need to remove any watcher option and filter from within the middleware using conditional statements.
Another issue in Next.js messed some optional catch all routes when they are rewritten: it has been fixed starting from 13.0.4.
@sentry/nextjs inject a webpack loader that replaces all pages content with a proxy, including _app. If it does it before next-translate-routes loader execution, the latter won't be able to do its job.
So the wrapping order in next.config.js is important!
Works:
module.exports = withTranslateRoutes(withSentryConfig(nextConfig, sentryWebpackPluginOptions))
Does NOT work:
module.exports = withSentryConfig(withTranslateRoutes(nextConfig), sentryWebpackPluginOptions)
_routes.json
files.withTranslateRoutes
high order component that wrap the app.withTranslateRoutes
makes this data available as a global variable, __NEXT_TRANSLATE_ROUTES_DATA
.translateUrl
function uses this data to translate routes.next-translate-routes/link
leverages the translateUrl
function to set the as
prop of next/link
to the translated url so that the link is aware of the true url destination (which is then available on hover, or on right-click - copy link for example).withTranslateRoutes
enhance the router by overriding the router context, to give translation skills to the router.push (which is used on click on a next/link
), router.replace, and router.prefetch functions, using the translateUrl
function too.