DEV Community

Cover image for All side optimized Next.js translations
Adriano Raiano
Adriano Raiano

Posted on • Edited on

All side optimized Next.js translations

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?

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:

.
└── public
    └── locales
        ├── en
        |   └── common.json
        └── de
            └── common.json
Enter fullscreen mode Exit fullscreen mode

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:

// 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] : []
}
Enter fullscreen mode Exit fullscreen mode

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

//
// 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'])
//   }
// }
Enter fullscreen mode Exit fullscreen mode

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.

// 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] : []
}
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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:

// 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] : []
}
Enter fullscreen mode Exit fullscreen mode

Extend the makeStaticProps function with options (emptyI18nStoreStore):

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

...and use it accordingly:

const getStaticProps = makeStaticProps(['common', 'footer'], { emptyI18nStoreStore: true })
export { getStaticPaths, getStaticProps }
Enter fullscreen mode Exit fullscreen mode

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:

// 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
}
Enter fullscreen mode Exit fullscreen mode

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

<div>{t('new.key', 'this will be added automatically')}</div>
Enter fullscreen mode Exit fullscreen mode

will result in locize like this:

👀 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:

// 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
}
Enter fullscreen mode Exit fullscreen mode

Automatic machine translation:

Last used translations filter:

InContext Editor:

📦 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:

// 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
}
Enter fullscreen mode Exit fullscreen mode

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:

Merging versions:

🧑‍💻 The complete code can be found here.

Check also the code integration part in this YouTube 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.

👍

Top comments (10)

Collapse
 
bbrizzi profile image
Benjamin Brizzi

Hey, thanks for the article, very clear :)

We have a problem with this setup : if a page is not loaded in our dev env before release (for dynamic catalog pages for instance) the translations will never show up in production.

is there a way to load every page once at build time to trigger the saveMissing and make sure all strings are sent to Locize before going to the production version with saveMissing disabled ?

Collapse
 
adrai profile image
Adriano Raiano

no, but alternatively you can try something like i18next-extract, like here (locize.com/blog/gatsby-i18n/#extract) and use the locize cli to sync?

Collapse
 
bbrizzi profile image
Benjamin Brizzi

Thank you for the answer, I'll take a look at that and let you know what I figure out.

Collapse
 
eliozashvili profile image
Giorgi Eliozashvili • Edited

Hey there! Thanks for your post. But I have a few questions:

  1. Where did you get project id from?

  2. Am I able to use next export while Client Side Rendering? (In my project I don't use getServerSideProps/getStaticProps, I use useEffect() hook for fetching data)

Collapse
 
adrai profile image
Adriano Raiano
  1. From the settings page of your locize project.
  2. yes, should work
Collapse
 
eliozashvili profile image
Giorgi Eliozashvili

Thanks

Collapse
 
jxhnxllxn profile image
John Allen de Chavez

How about dynamic route? I am getting this error please help

Error: A required parameter (slug) was not provided as a string in getStaticPaths for /[locale]/transactions/[slug]

Collapse
 
adrai profile image
Adriano Raiano

You need to provide all path parameters, like here: github.com/adrai/next-static-i18n-...

Collapse
 
sjiirfan profile image
Mohammed Irfan

Hi, what if I use Next build and with regular translation approach loads the, that has server, and dynamic details page. Will it be well search engine optimized. If I also don't want to use Locize.

Collapse
 
adrai profile image
Adriano Raiano

Sorry, I don’t understand what you’re asking for.
You may try to ask at stackoverflow.
But generally, if the html returned by the server includes the translations it is search engine optimized.