All side optimized Next.js translations (a next-i18next guide)
de

Writing Next.js code blurs the lines between client side and server side.
The code is written once and depending on your needs it is then executed as SSG (static-site generation), SSR (server-side rendering) or CSR (client-side rendering), etc.

So also the internationalization, right?

How to optimize Next.js apps to best work with translations on server side and on client side with next-i18next?

Let's take the example of next-i18next. While next-i18next uses i18next and react-i18next under the hood, users of next-i18next simply need to include their translation content as JSON files and don't have to worry about much else.

By default, there is one next-i18next configuration that loads the translations from the local directory structure and renders the pages on server side.
This is ok, it works and is optimized for SEO etc. but there is more we could do.

What if we could power up the seo optimized website with always up-to-date translations without the need to redeploy your app?

We will discuss 2 different setups: One with an active backend and another one completely statically generated.

The basic target is always the same: We want everything to be SEO optimized in all languages and serve always the newest translations to our users.

Example with a backend server

Having a backend server does not mean you are forced to run your own server. It can also be a PaaS or serverless solution, like Vercel or Netlify, etc.

Ok, let's start with the default:
You followed the normal next-i18next setup guide and now your translations are organized more or less as such:

1
2
3
4
5
6
7
.
└── public
└── locales
├── en
| └── common.json
└── de
└── common.json

Now let's 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.

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.

But you were talking about having always up-to-date translations without the need to redeploy your app?

Yes, let's adapt for that:

We will use the i18next-locize-backend plugin, but only on client side.

Together with some other i18next dependencies:

npm install i18next-locize-backend i18next-chained-backend i18next-localstorage-backend

And we adapt the next-i18next.config.js file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// next-i18next.config.js
const LocizeBackend = require('i18next-locize-backend/cjs')
const ChainedBackend= require('i18next-chained-backend').default
const LocalStorageBackend = require('i18next-localstorage-backend').default

const isBrowser = typeof window !== 'undefined'

module.exports = {
// debug: true,
i18n: {
defaultLocale: 'en',
locales: ['en', 'de', 'it'],
},
backend: {
backendOptions: [{
expirationTime: 60 * 60 * 1000 // 1 hour
}, {
projectId: 'd3b405cf-2532-46ae-adb8-99e88d876733',
version: 'latest'
}],
backends: isBrowser ? [LocalStorageBackend, LocizeBackend] : [],
},
serializeConfig: false,
use: isBrowser ? [ChainedBackend] : []
}

And then remove the serverSideTranslation to getStaticProps or getServerSideProps (depending on your case) in the page-level components.

1
2
3
4
5
6
7
8
9
//
// Without the getStaticProps or getServerSideProps function,
// the translsations are loaded via configured i18next backend.
//
// export const getStaticProps = async ({ locale }) => {
// return {
// props: await serverSideTranslations(locale, ['common', 'footer'])
// }
// }

That's it! Let's check the result:

The HTML returned from the server looks correctly translated. So this is well optimized for search engines.

And on client side, the up-to-date translations are directly fetched from the locize CDN.

🙀 This means you can fix translations without having to change your code or redeploy your app. 🤩

🧑‍💻 The code can be found here.

Additional hint:

If you've configured caching for your locize version, you may not need the i18next-localstorage-backend and i18next-chained-backend plugin.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// next-i18next.config.js
const LocizeBackend = require('i18next-locize-backend/cjs')

const isBrowser = typeof window !== 'undefined'

module.exports = {
// debug: true,
i18n: {
defaultLocale: 'en',
locales: ['en', 'de', 'it'],
},
backend: isBrowser ? {
projectId: 'd3b405cf-2532-46ae-adb8-99e88d876733',
version: 'production'
} : undefined,
serializeConfig: false,
use: isBrowser ? [LocizeBackend] : []
}

Alternative usage:

In case you're using the ready flag and are seeing a warning like this: Expected server HTML to contain a matching text node for... this is because of the following reason:

The server rendered the correct translation text, but the client still needs to lazy load the translations and will show a different UI. This means there's hydration mismatch.

This can be prevented by keeping the getServerSideProps or getStaticProps function but making use of the reloadResources functionality of i18next.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const LazyReloadPage = () => {

const { t, i18n } = useTranslation(['lazy-reload-page', 'footer'], { bindI18n: 'languageChanged loaded' })
// bindI18n: loaded is needed because of the reloadResources call
// if all pages use the reloadResources mechanism, the bindI18n option can also be defined in next-i18next.config.js
useEffect(() => {
i18n.reloadResources(i18n.resolvedLanguage, ['lazy-reload-page', 'footer'])
}, [])

return (
<>
<main>
<Header heading={t('h1')} title={t('title')} />
<Link href='/'>
<button
type='button'
>
{t('back-to-home')}
</button>
</Link>
</main>
<Footer />
</>
)
}

export const getStaticProps = async ({ locale }) => ({
props: {
...await serverSideTranslations(locale, ['lazy-reload-page', 'footer']),
},
})

export default LazyReloadPage

This way the ready check is also not necessary anymore, because the translations served directly by the server are used. And as soon the translations are reloaded, new translations are shown.

Static Website example

With this example, we just need a static webserver, like GitHub Pages or similar.

It's pretty much the same as with above example, but there are some little things we need to additionally consider.

To work with static-site generation (SSG) we need to use the next export command, but...

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.

There is a dedicated article with a solution to that Next.js problem. Follow that guide first!

Done so? Then let's continue here:

It's the same next-i18next.config.js config like in the previous example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// next-i18next.config.js
const LocizeBackend = require('i18next-locize-backend/cjs')
const ChainedBackend= require('i18next-chained-backend').default
const LocalStorageBackend = require('i18next-localstorage-backend').default

// If you've configured caching for your locize version, you may not need the i18next-localstorage-backend and i18next-chained-backend plugin.
// https://docs.locize.com/more/caching

const isBrowser = typeof window !== 'undefined'

module.exports = {
// debug: true,
i18n: {
defaultLocale: 'en',
locales: ['en', 'de', 'it'],
},
backend: {
backendOptions: [{
expirationTime: 60 * 60 * 1000 // 1 hour
}, {
projectId: 'd3b405cf-2532-46ae-adb8-99e88d876733',
version: 'latest'
}],
backends: isBrowser ? [LocalStorageBackend, LocizeBackend] : [],
},
serializeConfig: false,
use: isBrowser ? [ChainedBackend] : []
}

Extend the makeStaticProps function with options (emptyI18nStoreStore):

1
2
3
4
5
6
7
8
9
10
11
12
export function makeStaticProps(ns = [], opt = {}) {
return async function getStaticProps(ctx) {
const props = await getI18nProps(ctx, ns)
if (opt.emptyI18nStoreStore) {
// let the client fetch the translations
props._nextI18Next.initialI18nStore = null
}
return {
props
}
}
}

...and use it accordingly:

1
2
const getStaticProps = makeStaticProps(['common', 'footer'], { emptyI18nStoreStore: true })
export { getStaticPaths, getStaticProps }

That's it! Let's check the result:

The generated static HTML looks correctly translated. So this is well optimized for search engines.

And on client side, the up-to-date translations are directly fetched from the locize CDN.

🙀 This means you can fix translations without having to change your code or redeploy your app. And without owning an active server. 🤩

🧑‍💻 The code can be found here.

Continuous Localization

Since we're now "connected" to as smart translation management system, we can try to make use of its full potential.

save missing translations

I wish newly added keys in the code, would automatically be saved to locize.

Your wish is my command!

Extend the next-i18next config with the locize api-key and set saveMissing: true:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// next-i18next.config.js
const LocizeBackend = require('i18next-locize-backend/cjs')

const isBrowser = typeof window !== 'undefined'

module.exports = {
// debug: true,
i18n: {
defaultLocale: 'en',
locales: ['en', 'de'],
},
backend: {
projectId: 'd3b405cf-2532-46ae-adb8-99e88d876733',
apiKey: '14bbe1fa-6ffc-40f5-9226-7462aa4a042f',
version: 'latest'
},
serializeConfig: false,
use: isBrowser ? [LocizeBackend] : [],
saveMissing: true // do not set saveMissing to true for production and also not when using the chained backend
}

Each time you'll use a new key, it will be sent to locize, i.e.:

1
<div>{t('new.key', 'this will be added automatically')}</div>

will result in locize like this:

missing key

👀 but there's more...

Thanks to the locize-lastused plugin, you'll be able to find and filter in locize which keys are used or not used anymore.

With the help of the locize plugin, you'll be able to use your app within the locize InContext Editor.

Lastly, with the help of the auto-machinetranslation workflow and the use of the saveMissing functionality, new keys not only gets added to locize automatically, while developing the app, but are also automatically translated into the target languages using machine translation.

Check out this video to see how the automatic machine translation workflow looks like!

npm install locize-lastused locize

use them like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// next-i18next.config.js
const LocizeBackend = require('i18next-locize-backend/cjs')

const isBrowser = typeof window !== 'undefined'

const locizeOptions = {
projectId: 'd3b405cf-2532-46ae-adb8-99e88d876733',
apiKey: '14bbe1fa-6ffc-40f5-9226-7462aa4a042f',
version: 'latest'
}

module.exports = {
// debug: true,
i18n: {
defaultLocale: 'en',
locales: ['en', 'de'],
},
backend: locizeOptions,
locizeLastUsed: locizeOptions,
serializeConfig: false,
use: isBrowser ? [LocizeBackend, require('locize').locizePlugin, require('locize-lastused/cjs')] : [], // do not use locize-lastused on production
saveMissing: true // do not set saveMissing to true for production and also not when using the chained backend
}

Automatic machine translation:

missing key auto

Last used translations filter:

i18next last used

InContext Editor:

i18next incontext

📦 Let's prepare for production 🚀

Now, we prepare the app for going to production.

First in locize, create a dedicated version for production. Do not enable auto publish for that version but publish manually or via API or via CLI. Lastly, enable Cache-Control max-age​ for that production version.

Let's adapt the next-i18next.config.js file once again:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// next-i18next.config.js
const LocizeBackend = require('i18next-locize-backend/cjs')

const isBrowser = typeof window !== 'undefined'

const locizeOptions = {
projectId: 'd3b405cf-2532-46ae-adb8-99e88d876733',
apiKey: '14bbe1fa-6ffc-40f5-9226-7462aa4a042f',
version: 'latest'
}

module.exports = {
// debug: true,
i18n: {
defaultLocale: 'en',
locales: ['en', 'de'],
},
backend: locizeOptions,
locizeLastUsed: locizeOptions,
serializeConfig: false,
use: isBrowser ? [LocizeBackend, require('locize').locizePlugin, require('locize-lastused/cjs')] : [], // do not use locize-lastused on production
saveMissing: true // do not set saveMissing to true for production and also not when using the chained backend
}

Now, during development, you'll continue to save missing keys and to make use of lastused feature. => npm run dev

And in production environment, saveMissing and lastused are disabled. => npm run build && npm start

Caching:

i18next caching

Merging versions:

overwrite version

🧑‍💻 The complete code can be found here.

Check also the code integration part in this YouTube video.

There's also an i18next crash course video.

🎉🥳 Congratulations 🎊🎁

Awesome! Thanks to next-i18next, i18next, react-i18next and locize your continuous localization workflow is ready to go.

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.

👍

Share