You know Next.js, right? - If not, stop reading this article and make something else.
Next.js is awesome! It gives you the best developer experience with all the features you need...
TOC
- BUT, you may have heard about this
- So what can we do now?
- The recipe
- The outcome
- The voluntary part
- ππ₯³ Congratulations ππ
BUT, you may have heard about this:
Error: i18n support is not compatible with next export. See here for more info on deploying: https://nextjs.org/docs/deployment
This happens if you're using the internationalized routing feature and are trying to generate a static HTML export by executing next export
.
Well, this features requires a Node.js server, or dynamic logic that cannot be computed during the build process, that's why it is unsupported.
This is the case if you're using next-i18next for example.
So what can we do now?
An obvious option is, to renounce to the static HTML export and use a Node.js server or Vercel as deployment environment.
But sometimes, due to company or architectural guidelines it is mandatory to use a static web server.
Ok then renounce to i18n? - Not really, if we are here, it seems like to be a requirement.
So then do it without Next.js? - But this usually means to rewrite the whole project.
Executing next export
when not using i18n seems to work.
What if we do not try to use the internationalized routing feature and do the i18n routing on our own?
The recipe
To "cook" this recipe you will need the following ingredients:
- use the dynamic route segments feature
- willingness to change the structure of your project files
- willingness to adapt a bit of code
- a logic to detect the user language and redirect accordingly
Sounds feasible. Let's start!
1. Remove the i18n options from next.config.js
.
- const { i18n } = require('./next-i18next.config')
-
module.exports = {
- i18n,
trailingSlash: true,
}
2. Create a [locale]
folder inside your pages directory.
a) Move all your pages files to that folder (not _app.js
or _document.js
etc..).
b) Adapt your imports, if needed.
3. Create a getStatic.js
file and place it for example in a lib
directory.
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import i18nextConfig from '../next-i18next.config'
export const getI18nPaths = () =>
i18nextConfig.i18n.locales.map((lng) => ({
params: {
locale: lng
}
}))
export const getStaticPaths = () => ({
fallback: false,
paths: getI18nPaths()
})
export async function getI18nProps(ctx, ns = ['common']) {
const locale = ctx?.params?.locale
let props = {
...(await serverSideTranslations(locale, ns))
}
return props
}
export function makeStaticProps(ns = {}) {
return async function getStaticProps(ctx) {
return {
props: await getI18nProps(ctx, ns)
}
}
}
4. Use getStaticPaths
and makeStaticProps
in your pages, like this:
import { useTranslation } from 'next-i18next'
import { getStaticPaths, makeStaticProps } from '../../lib/getStatic'
import { Header } from '../../components/Header'
import { Footer } from '../../components/Footer'
import Link from '../../components/Link'
+ const getStaticProps = makeStaticProps(['common', 'footer'])
+ export { getStaticPaths, getStaticProps }
const Homepage = () => {
const { t } = useTranslation('common')
return (
<>
<main>
<Header heading={t('h1')} title={t('title')} />
<div>
<Link href='/second-page'><button type='button'>{t('to-second-page')}</button></Link>
</div>
</main>
<Footer />
</>
)
}
export default Homepage
5. Install next-language-detector.
npm i next-language-detector
6. Create a languageDetector.js
file and place it for example in the lib
directory.
import languageDetector from 'next-language-detector'
import i18nextConfig from '../next-i18next.config'
export default languageDetector({
supportedLngs: i18nextConfig.i18n.locales,
fallbackLng: i18nextConfig.i18n.defaultLocale
})
7. Create a redirect.js
file and place it for example in the lib
directory.
import { useEffect } from 'react'
import { useRouter } from 'next/router'
import languageDetector from './languageDetector'
export const useRedirect = (to) => {
const router = useRouter()
to = to || router.asPath
// language detection
useEffect(() => {
const detectedLng = languageDetector.detect()
if (to.startsWith('/' + detectedLng) && router.route === '/404') { // prevent endless loop
router.replace('/' + detectedLng + router.route)
return
}
languageDetector.cache(detectedLng)
router.replace('/' + detectedLng + to)
})
return <></>
};
export const Redirect = () => {
useRedirect()
return <></>
}
// eslint-disable-next-line react/display-name
export const getRedirect = (to) => () => {
useRedirect(to)
return <></>
}
8. For each of your pages files in your [locale]
directory, but especially for the index.js
file, create a file with the same name with this content:
import { Redirect } from '../lib/redirect'
export default Redirect
9. Create a Link.js
component and place it for example in the components
directory.
import React from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
const LinkComponent = ({ children, skipLocaleHandling, ...rest }) => {
const router = useRouter()
const locale = rest.locale || router.query.locale || ''
let href = rest.href || router.asPath
if (href.indexOf('http') === 0) skipLocaleHandling = true
if (locale && !skipLocaleHandling) {
href = href
? `/${locale}${href}`
: router.pathname.replace('[locale]', locale)
}
return (
<>
<Link href={href}>
<a {...rest}>{children}</a>
</Link>
</>
)
}
export default LinkComponent
10. Replace al next/link
Link
imports with the appropriate ../components/Link
Link
import:
- import Link from 'next/link'
+ import Link from '../../components/Link'
11. Add or modify your _document.js
file to set the correct html lang
attribute:
import Document, { Html, Head, Main, NextScript } from 'next/document'
import i18nextConfig from '../next-i18next.config'
class MyDocument extends Document {
render() {
const currentLocale = this.props.__NEXT_DATA__.query.locale || i18nextConfig.i18n.defaultLocale
return (
<Html lang={currentLocale}>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
12. In case you have a language switcher, create or adapt it:
// components/LanguageSwitchLink.js
import languageDetector from '../lib/languageDetector'
import { useRouter } from 'next/router'
import Link from 'next/link'
const LanguageSwitchLink = ({ locale, ...rest }) => {
const router = useRouter()
let href = rest.href || router.asPath
let pName = router.pathname
Object.keys(router.query).forEach((k) => {
if (k === 'locale') {
pName = pName.replace(`[${k}]`, locale)
return
}
pName = pName.replace(`[${k}]`, router.query[k])
})
if (locale) {
href = rest.href ? `/${locale}${rest.href}` : pName
}
return (
<Link
href={href}
onClick={() => languageDetector.cache(locale)}
>
<button style={{ fontSize: 'small' }}>{locale}</button>
</Link>
);
};
export default LanguageSwitchLink
// components/Footer.js
import { useTranslation } from 'next-i18next'
import { useRouter } from 'next/router'
import LanguageSwitchLink from './LanguageSwitchLink'
import i18nextConfig from '../next-i18next.config'
export const Footer = () => {
const router = useRouter()
const { t } = useTranslation('footer')
const currentLocale = router.query.locale || i18nextConfig.i18n.defaultLocale
return (
<footer>
<p>
<span style={{ lineHeight: '4.65em', fontSize: 'small' }}>{t('change-locale')}</span>
{i18nextConfig.i18n.locales.map((locale) => {
if (locale === currentLocale) return null
return (
<LanguageSwitchLink
locale={locale}
key={locale}
/>
)
})}
</p>
</footer>
)
}
The outcome
If you now start your project (next dev
) you should see, more or less, the same behaviour as before.
So what's the benefit?
Try: next build && next export
You should see something like this at the end:
β (SSG) automatically generated as static HTML + JSON (uses getStaticProps)
info - using build directory: /Users/usr/projects/my-awesome-project/.next
info - Copying "static build" directory
info - No "exportPathMap" found in "/Users/usr/projects/my-awesome-project/next.config.js". Generating map from "./pages"
info - Launching 9 workers
info - Copying "public" directory
info - Exporting (3/3)
Export successful. Files written to /Users/usr/projects/my-awesome-project/out
Yeah no i18n support is not compatible with next export
error anymore!!!
Congratulations! Now you can "deploy" the content of your out
directory to any static web server.
π§βπ» The complete code can be found here.
The voluntary part
Connect to an awesome translation management system and manage your translations outside of your code.
Let's synchronize the translation files with locize.
This can be done on-demand or on the CI-Server or before deploying the app.
What to do to reach this step:
- in locize: signup at https://locize.app/register and login
- in locize: create a new project
- in locize: add all your additional languages (this can also be done via API)
- install the locize-cli (
npm i locize-cli
)
Use the locize-cli
Use the locize sync
command to synchronize your local repository (public/locales
) with what is published on locize.
Alternatively, you can also use the locize download
command to always download the published locize translations to your local repository (public/locales
) before bundling your app.
ππ₯³ Congratulations ππ
I hope youβve learned a few new things about static site generation (SSG), Next.js, next-i18next, i18next and modern localization workflows.
So if you want to take your i18n topic to the next level, it's worth to try the localization management platform - locize.
The founders of locize are also the creators of i18next. So with using locize you directly support the future of i18next.
π
Looking for an optimized Next.js translations setup?
Here you'll find a blog post on how to best use next-i18next with client side translation download and SEO optimization.
Top comments (55)
Thank you so much, it worked for me, but I didn't understand anything :D
I implemented this to one of my own projects written in TypeScript, there is is some boilerplate you need to write but it works, even with dynamic routes, sitemap etc. Sure there is some code to adapt to your project but I think this is the way to go right now for next export. Only thing that I don't like this to have to create a second file per page, but I understand why this necessary and can't be avoided. Thanks again.
I think you should also add appWithTranslation github.com/adrai/next-language-det... section to initialize i18next, otherwise you're getting i18next is not initialized error.
That should already be covered with the default: github.com/isaachinman/next-i18nex...
@adrai Can you help me, why I'm getting a redirection in loop in production only?
I'm opening the site example.com. It redirects me with automatically detected language for example example.com/en. All ok, but if I reload the page, it redirects in loop and looks like something like this: example.com/en/en/en/en/en/en/en/en/...
But this problem in the production only. Locally it works without problems..
Help please..
if it is not reproducible locally, you may need to check on your prod system
if it is reproducible, please provide a minimal reproducible example repository
I need to create dynamic routes so created something like this in pages folder
pages/[locale]/gallery/[filter].tsx
In filter.tsx, I am catching the value of query.filter using useRouter hook. So after catching value of query i need to render some data from /en/common.json so i am trying to fetch it like
const data = t(
gallery.metadata.${query.filter})
As in first render query.filter returns undefined so it gives error if i reload or directly loads on /gallery/rooms or /gallery/services page , but it works fine if i simply navigate to these routes using some next Link component.
II tried many solutions , but got some issue in all the solutions i tried to avoid it from accessing t('gallery.metadata.undefined')..
kindly help me in this, thanks in advance
however it works fine if i am fetching data from some external url/source based upon value from query.filter
you may ask on stackoverflow with a more complete example
Thanks a lot for this. You are a life saviour π
Youβre welcomeβ¦
If you want you can share this to the word π
I am writing project with typescript, and cannot use type "any".
I want to ask you about getStatic file.
Can you please tell the type of ctx?
And also ns by default is array of strings, but in makeStaticProps you assign to ns an empty object, should I change empty object to an empty array?
this is JavaScript, not TypeScriptβ¦
ns can be a string or an array of strings
ctx is coming from Next.js
Made everything like in a tutorial, but now, when I try to run in development mode (also for build) I got this error:
error - ./node_modules/next-i18next/dist/commonjs/serverSideTranslations.js:78:0
Module not found: Can't resolve 'fs'
Can you help please how to resolve this issue
This seems to work: github.com/adrai/next-language-det...
provide a reproducible example and open a github issue
I'm not sure about what causes the "fs" issue but it seems related to using getStaticProps in client components - move it to server component and pass the translation to the component which causes the error
hey can you share some demo code of this setup, i also want to setup this in Ts
Thanks in advance
I switched to next-translate, because it's easier, you just keep small json config file and locales folder and that's it, nothing more
is next-translate compatible with next export?
Wow, this is great! Thanks a million!
Interesting approach. I wrote about my experience with next export an i18n a few months back: next.js: i18n with static sites and created this easy to integrate npm-package: next-export-i18n.
As far as is see we took a different route for something similar. Great article!
Cheers!
interestingβ¦ did not know itβ¦
I wanted to keep the i18next compatibilityβ¦
I followed the same step but for some reason.
const { t } = useTranslation("common");
is not loading the contents.
eg: {t("home1")} {t("home2")} would display home1 home2 in the page.
For context. I am using
"next": "12.3.0",
"next-i18next": "13.0.0",
"next-language-detector": "1.0.2",
How do I solve that?
Nvm I was missing.
export default appWithTranslation(MyApp)
THANK YOU!!!!!
I've been searching for a solution for quite a while now!
Great idea!
Just added this snippet to my _app.js to make it even nicer instead of step 8
How can i define dynamic routes, it gives error Id is not defined and also getStaticPaths is already occupied, please help on this urgently
dev.to/adrai/comment/1pphk
Hi, facing problem for [locale]/products/[productid].js
Here the productid will always be many, can be thousands, for that situation how can I handle it. Please share your knowledge on this, its urgent.
As with static path I had to pass a , but this situation I don't know which productid will be there.
Something like this:
params: {
locale: lng,
productid: product._id
}
If you donβt know all information on build time, then SSG is probably not suitable for you.
Is there no way on it.