?? Static HTML Export with i18n compatibility in Next.js ??
Adriano Raiano
Bridging the gap between developers and translators... i18next.com ?? locize.com | Always in search for #innovative and #disruptive stuff | Founder, CTO, Software/System Architect
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...
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:
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 know 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:
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.