quebone
quebone

Reputation: 51

How to integrate next-i18next, nextjs & locize

I need to integrate i18 localisation from locize in a nextjs project. I found that react-i18next works well with i18 & locize, but doesn't integrate with nextjs. On the other hand, next-i18next works well with nextjs & local i18 files, but doesn't seem to work with locize (almost there are no examples). Is there another solution to work with? Could this be done with next-i18next?

Thks.

Upvotes: 3

Views: 7521

Answers (3)

Vadorequest
Vadorequest

Reputation: 18079

Edit March 2020: Check https://github.com/UnlyEd/next-right-now boilerplate, which uses the below configuration and provide a real use-case example with Next.js 9 (serverless) + i18next/react-i18next + Locize. (Disclaimer: I'm the author)


Thank you @quebone for your auto-answer. I used it to improve my own configuration, which is using TypeScript with Next.js, but I'm not using next-i18next like you because it's not compatible yet with the Next serverless mode.

So, if you're using Next in serverless mode (with Zeit now, for instance), rather follow the following configuration.

utils/i18nextLocize.ts

import { isBrowser } from '@unly/utils';
import { createLogger } from '@unly/utils-simple-logger';
import i18next from 'i18next';
import map from 'lodash.map';
import { initReactI18next } from 'react-i18next';

import { LOCALE_EN, LOCALE_FR } from './locale';

const logger = createLogger({
  label: 'utils/i18nextLocize',
});

/**
 * Common options shared between all locize/i18next plugins
 *
 * @see https://github.com/locize/i18next-node-locize-backend#backend-options
 * @see https://github.com/locize/i18next-locize-backend#backend-options
 * @see https://github.com/locize/locize-node-lastused#options
 * @see https://github.com/locize/locize-editor#initialize-with-optional-options
 */
export const locizeOptions = {
  projectId: '7867a172-62dc-4f47-b33c-1785c4701b12',
  apiKey: isBrowser() ? null : process.env.LOCIZE_API_KEY, // XXX Only define the API key on the server, for all environments (allows to use saveMissing)
  version: process.env.APP_STAGE === 'production' ? 'production' : 'latest', // XXX On production, use a dedicated production version
  referenceLng: 'fr',
};

/**
 * Specific options for the selected Locize backend.
 *
 * There are different backends for locize, depending on the runtime (browser or node).
 * But each backend shares a common API.
 *
 * @see https://github.com/locize/i18next-node-locize-backend#backend-options
 * @see https://github.com/locize/i18next-locize-backend#backend-options
 */
export const locizeBackendOptions = {
  ...locizeOptions,
  loadPath: 'https://api.locize.io/{{projectId}}/{{version}}/{{lng}}/{{ns}}',
  addPath: 'https://api.locize.io/missing/{{projectId}}/{{version}}/{{lng}}/{{ns}}',
  allowedAddOrUpdateHosts: [
    'localhost',
  ],
};

/**
 * Configure i18next with Locize backend.
 *
 * - Initialized with pre-defined "lang" (to make sure GraphCMS and Locize are configured with the same language)
 * - Initialized with pre-fetched "defaultLocales" (for SSR compatibility)
 * - Fetches translations from Locize backend
 * - Automates the creation of missing translations using "saveMissing: true"
 * - Display Locize "in-context" Editor when appending "/?locize=true" to the url (e.g http://localhost:8888/?locize=true)
 * - Automatically "touches" translations so it's easier to know when they've been used for the last time,
 *    helping translators figuring out which translations are not used anymore so they can delete them
 *
 * XXX We don't rely on https://github.com/i18next/i18next-browser-languageDetector because we have our own way of resolving the language to use, using utils/locale
 *
 * @param lang
 * @param defaultLocales
 */
const i18nextLocize = (lang, defaultLocales): void => {
  logger.info(JSON.stringify(defaultLocales, null, 2), 'defaultLocales');

  // Plugins will be dynamically added at runtime, depending on the runtime (node or browser)
  const plugins = [ // XXX Only plugins that are common to all runtimes should be defined by default
    initReactI18next, // passes i18next down to react-i18next
  ];

  // Dynamically load different modules depending on whether we're running node or browser engine
  if (!isBrowser()) {
    // XXX Use "__non_webpack_require__" on the server
    // loads translations, saves new keys to it (saveMissing: true)
    // https://github.com/locize/i18next-node-locize-backend
    const i18nextNodeLocizeBackend = __non_webpack_require__('i18next-node-locize-backend');
    plugins.push(i18nextNodeLocizeBackend);

    // sets a timestamp of last access on every translation segment on locize
    // -> safely remove the ones not being touched for weeks/months
    // https://github.com/locize/locize-node-lastused
    const locizeNodeLastUsed = __non_webpack_require__('locize-node-lastused');
    plugins.push(locizeNodeLastUsed);

  } else {
    // XXX Use "require" on the browser, always take the "default" export specifically
    // loads translations, saves new keys to it (saveMissing: true)
    // https://github.com/locize/i18next-locize-backend
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const i18nextLocizeBackend = require('i18next-locize-backend').default;
    plugins.push(i18nextLocizeBackend);

    // InContext Editor of locize ?locize=true to show it
    // https://github.com/locize/locize-editor
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const locizeEditor = require('locize-editor').default;
    plugins.push(locizeEditor);
  }

  const i18n = i18next;
  map(plugins, (plugin) => i18n.use(plugin));
  i18n.init({ // XXX See https://www.i18next.com/overview/configuration-options
    resources: defaultLocales,
    debug: process.env.APP_STAGE !== 'production',
    saveMissing: true,
    lng: lang, // XXX We don't use the built-in i18next-browser-languageDetector because we have our own way of detecting language, which must behave identically for both GraphCMS I18n and react-I18n
    fallbackLng: lang === LOCALE_FR ? LOCALE_EN : LOCALE_FR,
    ns: 'common',
    defaultNS: 'common',
    interpolation: {
      escapeValue: false, // not needed for react as it escapes by default
    },
    backend: locizeBackendOptions,
    locizeLastUsed: locizeOptions,
    editor: {
      ...locizeOptions,
      onEditorSaved: async (lng, ns): Promise<void> => {
        // reload that namespace in given language
        await i18next.reloadResources(lng, ns);
        // trigger an event on i18n which triggers a rerender
        // based on bindI18n below in react options
        i18next.emit('editorSaved');
      },
    },
    react: {
      bindI18n: 'languageChanged editorSaved',
      useSuspense: false, // Not compatible with SSR
    },
    load: 'languageOnly', // Remove if you want to use localization (en-US, en-GB)
  });
};

export default i18nextLocize;



Also, because Next will render (SSR) early (it won't wait for i18next to load the initial translations), this will cause the translations not to be fetched by the server and the page served by SSR will not contain the right sentences.

To avoid that, you need to manually pre-fetch all the namespaces you rely on for the current language. This step must be done in pages/_app.tsx in the getInitialProps function. (I'm using TSX, but you can use jsx, js, etc.)

pages/_app.tsx

import { ApolloProvider } from '@apollo/react-hooks';
import fetch from 'isomorphic-unfetch';
import get from 'lodash.get';
import { NextPageContext } from 'next';
import NextApp from 'next/app';
import React from 'react';

import Layout from '../components/Layout';
import withData from '../hoc/withData';
import i18nextLocize, { backendOptions } from '../utils/i18nextLocize';
import { LOCALE_FR, resolveBestCountryCodes, resolveBrowserBestCountryCodes } from '../utils/locale';


class App extends NextApp {
  /**
   * Initialise the application
   *
   * XXX Executed on the server-side only
   *
   * @param props
   * @see https://github.com/zeit/next.js/#fetching-data-and-component-lifecycle
   */
  static async getInitialProps(props): Promise<any> {
    const { ctx } = props;
    const { req, res }: NextPageContext = ctx;
    let publicHeaders = {};
    let bestCountryCodes;

    if (req) {
      bestCountryCodes = resolveBestCountryCodes(req, LOCALE_FR);
      const { headers } = req;
      publicHeaders = {
        'accept-language': get(headers, 'accept-language'),
        'user-agent': get(headers, 'user-agent'),
        'host': get(headers, 'host'),
      };

    } else {
      bestCountryCodes = resolveBrowserBestCountryCodes();
    }
    const lang = get(bestCountryCodes, '[0]', 'en').toLowerCase(); // TODO Should return a locale, not a lang. i.e: fr-FR instead of fr

    // calls page's `getInitialProps` and fills `appProps.pageProps` - XXX See https://nextjs.org/docs#custom-app
    const appProps = await NextApp.getInitialProps(props);

    // Pre-fetching locales for i18next, for the "common" namespace
    // XXX We do that because if we don't, then the SSR fails at fetching those locales using the i18next "backend" and renders too early
    //  This hack helps fix the SSR issue
    //  On the other hand, it seems that once the i18next "resources" are set, they don't change for that language
    //  so this workaround could cause sync issue if we were using multiple namespaces, but we aren't and probably won't
    const defaultLocalesResponse = await fetch(
      backendOptions
        .loadPath
        .replace('{{projectId}}', backendOptions.projectId)
        .replace('{{version}}', backendOptions.version)
        .replace('{{lng}}', lang)
        .replace('{{ns}}', 'common'));
    const defaultLocales = {
      [lang]: {
        common: await defaultLocalesResponse.json(),
      }
    };

    appProps.pageProps = {
      ...appProps.pageProps,
      bestCountryCodes, // i.e: ['EN', 'FR']
      lang, // i.e: 'en'
      defaultLocales: defaultLocales,
    };

    return { ...appProps };
  }

  render() {
    const { Component, pageProps, apollo }: any = this.props;
    i18nextLocize(pageProps.lang, pageProps.defaultLocales); // Apply i18next configuration with Locize backend

    // Workaround for https://github.com/zeit/next.js/issues/8592
    const { err }: any = this.props;
    const modifiedPageProps = { ...pageProps, err };

      return (
        <ApolloProvider client={apollo}>
          <Layout {...modifiedPageProps}>
            <Component {...modifiedPageProps} />
          </Layout>
        </ApolloProvider>
      );
  }

  componentDidCatch(error, errorInfo) {
    // This is needed to render errors correctly in development / production
    super.componentDidCatch(error, errorInfo);
  }
}

// Wraps all components in the tree with the data provider
export default withData(App);

Then, you can use either HOC or Hook to use translation within your pages/components. Here is an example using HOC with my index page:

pages/index.tsx

import React from 'react';
import { withTranslation } from 'react-i18next';
import { compose } from 'recompose';

import Head from '../components/Head';

const Home = (props: any) => {
  const { organisationName, bestCountryCodes, t } = props;

  return (
    <div>
      <Head />

      <div className="hero">
        <div>{t('welcome', 'Bonjour auto')}</div>
        <div>{t('missingShouldBeAdded', 'Missing sentence, should be added automatically')}</div>
        <div>{t('missingShouldBeAdded2', 'Missing sentence, should be added automatically')}</div>
      </div>
    </div>
  );

};

export default compose(
  withTranslation(['common']),
)(Home);

See official documentation for more examples:


Note that auto adding of missing sentence doesn't work for me on localhost, but works fine online.

Edit: Made it work in localhost eventually, not sure how.


Note that you'll need to install additional dependencies to have this working:


Also note that I use a custom locale detector, but you would probably want to use the recommended one at https://github.com/i18next/i18next-browser-languageDetector

Upvotes: 8

quebone
quebone

Reputation: 51

I got the answer from Locize. Thanks a lot!

const isNode = require("detect-node");
const i18nextLocizeBackend = require("i18next-locize-backend");
const { localeSubpaths } = require("next/config").default().publicRuntimeConfig;
const NextI18Next = require("next-i18next/dist/commonjs");

const use = [];

if (isNode) {
  const i18nextNodeLocizeBackend = eval(
    "require('i18next-node-locize-backend')"
  );
  use.push(i18nextNodeLocizeBackend);
} else {
  use.push(i18nextLocizeBackend.default);
}

module.exports = new NextI18Next({
  otherLanguages: ["de"],
  localeSubpaths,
  use,
  saveMissing: true,
  backend: {
    loadPath: "https://api.locize.io/{{projectId}}/{{version}}/{{lng}}/{{ns}}",
    addPath: "https://api.locize.io/missing/{{projectId}}/{{version}}/{{lng}}/{{ns}}",
    referenceLng: "en",
    projectId: "9dc2239d-a752-4973-a6e7-f622b2b76508",
    apiKey: "9f019666-2e71-4c58-9648-e6a4ed1e15ae",
    version: "latest"
  }
});

Upvotes: 2

Related Questions