Blog
Next.js i18n-Compatible Static HTML Export

Next.js i18n-Compatible Static HTML Export

December 7, 2021
Blog Hero Image

You know Next.js, right? - If not, stop reading this article and make something else.

If you're using Next.js 13 with app directory, have a look at this blog post.

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:

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.

1- const { i18n } = require('./next-i18next.config')
2- 
3module.exports = {
4-   i18n,
5  trailingSlash: true,
6}

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.

1import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
2import i18nextConfig from '../next-i18next.config'
3
4export const getI18nPaths = () =>
5  i18nextConfig.i18n.locales.map((lng) => ({
6    params: {
7      locale: lng
8    }
9  }))
10
11export const getStaticPaths = () => ({
12  fallback: false,
13  paths: getI18nPaths()
14})
15
16export async function getI18nProps(ctx, ns = ['common']) {
17  const locale = ctx?.params?.locale
18  let props = {
19    ...(await serverSideTranslations(locale, ns))
20  }
21  return props
22}
23
24export function makeStaticProps(ns = {}) {
25  return async function getStaticProps(ctx) {
26    return {
27      props: await getI18nProps(ctx, ns)
28    }
29  }
30}

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.

1import languageDetector from 'next-language-detector'
2import i18nextConfig from '../next-i18next.config'
3
4export default languageDetector({
5  supportedLngs: i18nextConfig.i18n.locales,
6  fallbackLng: i18nextConfig.i18n.defaultLocale
7})

7. Create a redirect.js file and place it for example in the lib directory.

1import { useEffect } from 'react'
2import { useRouter } from 'next/router'
3import languageDetector from './languageDetector'
4
5export const useRedirect = (to) => {
6  const router = useRouter()
7  to = to || router.asPath
8
9  // language detection
10  useEffect(() => {
11    const detectedLng = languageDetector.detect()
12    if (to.startsWith('/' + detectedLng) && router.route === '/404') { // prevent endless loop
13      router.replace('/' + detectedLng + router.route)
14      return
15    }
16
17    languageDetector.cache(detectedLng)
18    router.replace('/' + detectedLng + to)
19  })
20
21  return <></>
22};
23
24export const Redirect = () => {
25  useRedirect()
26  return <></>
27}
28
29// eslint-disable-next-line react/display-name
30export const getRedirect = (to) => () => {
31  useRedirect(to)
32  return <></>
33}

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:

1import { Redirect } from '../lib/redirect'
2export default Redirect

9. Create a Link.js component and place it for example in the components directory.

1import React from 'react'
2import Link from 'next/link'
3import { useRouter } from 'next/router'
4
5const LinkComponent = ({ children, skipLocaleHandling, ...rest }) => {
6  const router = useRouter()
7  const locale = rest.locale || router.query.locale || ''
8
9  let href = rest.href || router.asPath
10  if (href.indexOf('http') === 0) skipLocaleHandling = true
11  if (locale && !skipLocaleHandling) {
12    href = href
13      ? `/${locale}${href}`
14      : router.pathname.replace('[locale]', locale)
15  }
16
17  return (
18    <>
19      <Link href={href}>
20        <a {...rest}>{children}</a>
21      </Link>
22    </>
23  )
24}
25
26export default LinkComponent

10. Replace al next/link Link imports with the appropriate ../components/Link Link import:

1- import Link from 'next/link'
2+ import Link from '../../components/Link'

11. Add or modify your _document.js file to set the correct html lang attribute:

1import Document, { Html, Head, Main, NextScript } from 'next/document'
2import i18nextConfig from '../next-i18next.config'
3
4class MyDocument extends Document {
5  render() {
6    const currentLocale = this.props.__NEXT_DATA__.query.locale || i18nextConfig.i18n.defaultLocale
7    return (
8      <Html lang={currentLocale}>
9        <Head />
10        <body>
11          <Main />
12          <NextScript />
13        </body>
14      </Html>
15    )
16  }
17}
18
19export default MyDocument

12. In case you have a language switcher, create or adapt it:

1// components/LanguageSwitchLink.js
2import languageDetector from '../lib/languageDetector'
3import { useRouter } from 'next/router'
4import Link from 'next/link'
5
6const LanguageSwitchLink = ({ locale, ...rest }) => {
7  const router = useRouter()
8
9  let href = rest.href || router.asPath
10  let pName = router.pathname
11  Object.keys(router.query).forEach((k) => {
12    if (k === 'locale') {
13      pName = pName.replace(`[${k}]`, locale)
14      return
15    }
16    pName = pName.replace(`[${k}]`, router.query[k])
17  })
18  if (locale) {
19    href = rest.href ? `/${locale}${rest.href}` : pName
20  }
21
22  return (
23    <Link
24      href={href}
25      onClick={() => languageDetector.cache(locale)}
26    >
27      <button style={{ fontSize: 'small' }}>{locale}</button>
28    </Link>
29  );
30};
31
32export default LanguageSwitchLink

1// components/Footer.js
2import { useTranslation } from 'next-i18next'
3import { useRouter } from 'next/router'
4import LanguageSwitchLink from './LanguageSwitchLink'
5import i18nextConfig from '../next-i18next.config'
6
7export const Footer = () => {
8  const router = useRouter()
9  const { t } = useTranslation('footer')
10  const currentLocale = router.query.locale || i18nextConfig.i18n.defaultLocale
11
12  return (
13    <footer>
14      <p>
15        <span style={{ lineHeight: '4.65em', fontSize: 'small' }}>{t('change-locale')}</span>
16        {i18nextConfig.i18n.locales.map((locale) => {
17          if (locale === currentLocale) return null
18          return (
19            <LanguageSwitchLink
20              locale={locale}
21              key={locale}
22            />
23          )
24        })}
25      </p>
26    </footer>
27  )
28}

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:

1●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)
2
3info  - using build directory: /Users/usr/projects/my-awesome-project/.next
4info  - Copying "static build" directory
5info  - No "exportPathMap" found in "/Users/usr/projects/my-awesome-project/next.config.js". Generating map from "./pages"
6info  - Launching 9 workers
7info  - Copying "public" directory
8info  - Exporting (3/3)
9Export 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 voluntary part

transform the localization process
transform the localization process

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:

  1. in locize: signup at https://locize.app/register and login
  2. in locize: create a new project
  3. in locize: add all your additional languages (this can also be done via API)
  4. 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.

🎉🥳 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 trying the localization management platform - locize.

The founders of locize are also the creators of i18next. So by using locize you directly support the future of i18next.

👍

Looking for an optimized Next.js translations setup?

next-i18next
next-i18next

Here you'll find a blog post on how to best use next-i18next with client side translation download and SEO optimization.

Share Now:
Follow Us: