/nextjs-i18n-example

I18n for Next.js with Typescript support, SEO, Automatic static optimizations, RTL, static export, static and dynamic translations and more!

Primary LanguageTypeScriptMIT LicenseMIT

Next.JS i18n Example

Demo

https://nextjs-i18n-example.vercel.app

Features

  • In the spirit of React, instead of declaring all translations in a global locales folder, each component and page has its own set of translations. Each translation dir is placed under: /public/translations/ by default. That way, translations for components/Title component, are found under: /public/translations/components/Title, and for components/Nested/NestedComponent are found under /public/translations/Nested/NestedComponent and pages/[language]/ssr are found under public/translations/pages/[language]/ssr. I found that an even better way is to place the translations under their respective components e.g. for components/Title, their translations should be found under components/Title/translations. and for pages/[language]/ssr should be found under pagesTranslations/[language]/ssr. I'm just too lazy to change them here, feel free to make a PR.

  • SEO as a top priority

    • Adds hreflang tags for you
    • Sets HTML lang attribute for you on both the server and client.
  • Doesn't block Next.js Automatic Static Optimizations.

  • RTL and custom direction

  • Works with next export. (You'll only have to remove the getServerSideProps function in pages/index.js and rely only on the client side redirect or create one yourself on your HTTP server)

  • Support for both: server loaded translations and dynamic translations

  • Automatic language detection

  • Save user's preferred language in a cookie on every language transition.

  • No heavy I18n lib dependency e.g. i18next.

Don't use if you

  • Don't want to maintain the all the i18n logic yourself.

  • Don't want to write a fair amount of boilerplate

  • Don't want to use HTTP subdirectory based i18n. e.g. https://example.com/en/about, and want to use a subdomain or TLD based i18n strategy instead.

Files

0. i18n.config.ts

Example config:

const allLanguages: Config = {
  en: {
    name: 'English',
    prefix: 'en',
  },
  ar: {
    name: 'العربية',
    prefix: 'ar',
    direction: 'rtl',
  },
};

const defaultLanguage: Language = allLanguages.en;

// used for setting the `hreflang` alternate tag.
// Read the withI18n section for more info.
const domains: Domains = {
  development: 'http://localhost:3000',
  production: 'https://next-i18n-dynamic.netlify.app',
};

export default {
  allLanguages,
  defaultLanguage,
  domains
}

1. pages/_document.tsx

Is only there to add the lang property in the html tag. Note: due to a limitation of Next.JS, the _document page won't change the lang property after client side language transitions because the _document page is only rendered on the server. For client side html lang transitions, check: changeDocumentLanguage and how it's used in _app.tsx.

2. pages/_app.tsx

Is there to:

  1. add a div with a dir property. e.g. rtl and ltr. You should define the direction of the languages you specify in i18n.config.ts.

  2. Set the preferred-language cookie with the language found in context (Defaults to a 3 year cookie).

  3. Makes sure the <html> lang property is changed if the language changes.

3. utils/i18n.tsx

Has:

getI18nStaticPaths

Enumerates getStaticPaths with all the lanugage prefixes that you define in your config.

getI18nProps

Should be used for: getStaticProps and getServerSideProps.

A simple function that given the language param code generated by: getI18nStaticPaths and the paths of the translation files needed, loads:

  • The language prefix. Returns the default language if no language prefix was given (Shouldn't normally happen).

  • The translation namespaces specified, from public/translations into memory. (You can change the directory by passing a custom translationsDir)

withI18n

Should wrap all the pages that use the getI18nProps method.

It accepts the props passed by getI18nProps and sets them in the i18nContext so that all your components can access them using the useI18n hook.

This HOC also adds an hreflang alternate tag given the page route, e.g.

If you pass the page: pages/ssr.tsx to the withI18n HOC like this:

export default withI18n(Page, '/ssr');

It will add the following links to your header.

  <Head>
    <link
      rel="alternate"
      href={`http://localhost:3000/en${pathname}`}  // pathname is '/ssr' in that case
      hrefLang="en"
    />

    <link
      rel="alternate"
      href={`http://localhost:3000/ar${pathname}`}  // pathname is '/ssr' in that case
      hrefLang="ar"
    />
  </Head>

This tag is used to tell search engines which pages are translations of which pages. This is needed because Google claims that the Google crawler doesn't understand path-directories/sub-domains in regards to how they relate to a page's language. (Anyway, stop lying to us Google. We all know that your bots have super intellegence and will be ruling us in a matter of years).

useI18n

A hook that given a translation path, returns the translations JSON needed along with:

  • the current language prefix
  • the current language configs

useDynamicI18n

Asynchronously loads the translations by doing an ajax call. Check: components/DynamicTranslations for an example.

withPrefetchDynamicTranslations

To be used with useDynamicI18n

A HOC to wrap components that use the useDynamicI18n hook. It adds a prefetch header so that the user doesn't have to wait for the component to mount in order to download the translations. This however doesn't entirely solve the latency issue because your page won't show the translations until the components actually mount. That's because SWR can only access the prefetched version when the component mounts and the all the JS has been loaded. This can be useful for large translations and large XHR requests in general. But given the atomicity of the translations in this example, it probably won't make much difference in the latency anyway, because the major latency bottleneck is React, not the translations. NOTE: link prefetch is neither supported on Safari nor Safari IOS.

Link

Wraps next/link and accepts these extra props:

  • language: string of which language you want to redirect to.
  • href is optional. If you only pass a language without an href, this link will only switch to the language you chose.

changeDocumentLanguage

Given a language prefix on the browser, sets it to <html>'s lang attribute.

I can't find a scenario where you'll need to manually call this, because our custom _app.tsx already sets the HTML lang for you on every page transition, and our custom _document.tsx automatically sets it on every prerender.

changeDocumentDirection

Changes the root dir of your document

setI18nCookie

Sets a cookie with a name of: preferred-language to your cookies. Only works in the browser. See pages/[language]/index.tsx::{ getServerSideProps } to see how this cookie is loaded on the server.

getI18nAgnosticPathname

Strips the language directory prefix from window.location.pathname or any pathname you pass it and returns it. You can skip passing a pathname on the browser.

getLanguageFromURL

Opposite of changeDocumentLanguage. Strips the language prefix from window.location.pathname or any pathname you pass it and returns it. You can skip passing a pathname on the browser.

Todo

  1. Configure rewrites so that all pages work without specifying a language prefix. I wrote this example a couple of days before the rewrite feature was released. After we're done with this todo, all pages should have an hrefLang="x-default" link tag (on top of the already existing hrefLang={allLanguages[language].prefix} tags). The x-default tag should point to the page without a language prefix. The page without a language prefix should always redirect to a language prefix.

  2. Move component translations to their respective folders. and move pages translations to a top level pageTranslations folder. This isn't necessary, I tried this method and prefer it more than having all the translations in the /public dir.

  3. Make the componentDidMount of _app.tsx run on every route change, even better on every language change. I tried to make it run by using: Router.events.on('routeChangeComplete', () => changeDocumentLang() but couldn't make it work, not sure why.

Pain points

  1. It's quite cumbersome to manually list the names of the translations needed for each page. Since all pages already knows which components they will mount, we should expect them to know which translations to load without needing to specify them.

  2. Redirecting from '/' to '/ar' or '/en' is easy. But what if the user goes to '/ssr' and not '/[language]/ssr'. It should redirect to /[language]/ssr and not return a 404. Todo number 1. should fix this. Also checkout issue #1.

SEO resources

This example builds heavily on the work done by