如何集成next-i18next,nextjs和locize

时间:2019-05-05 17:55:18

标签: reactjs next.js i18next

我需要将locize中的i18本地化集成到一个nextjs项目中。我发现react-i18next适用于i18和locize,但未与nextjs集成。另一方面,next-i18next与nextjs和本地i18文件一起很好地工作,但似乎不适用于locize(几乎没有示例)。 还有其他解决方案吗?可以用next-i18next完成吗?

谢谢。

3 个答案:

答案 0 :(得分:2)

谢谢@quebone的自动应答。我用它来改进自己的配置,该配置将TypeScript与Next.js结合使用,但是我没有像您一样使用next-i18next,因为它还与Next serverless模式不兼容。

因此,如果您在无服务器模式下使用Next(例如,现在使用Zeit),则请遵循以下配置。

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;



此外,由于Next会提早渲染(SSR)(它不会等待i18next加载初始翻译),因此这将导致服务器无法提取翻译,并且SSR服务的页面将不包含正确的句子。

为避免这种情况,您需要手动预取当前语言所依赖的所有名称空间。必须在pages/_app.tsx函数的getInitialProps中完成此步骤。 (我正在使用TSX,但是您可以使用jsx,js等)

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);

然后,您可以使用HOC或Hook在页面/组件中使用翻译。这是在我的索引页上使用HOC的示例:

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);

有关更多示例,请参见官方文档:


  

请注意,自动添加缺少的句子对我来说在localhost上不起作用,但可以在网上正常运行。

     

编辑:最终使其在localhost中工作,不确定如何使用。


  

请注意,您需要安装其他依赖项才能正常工作:

     

  

还请注意,我使用的是自定义区域设置检测器,但您可能想在https://github.com/i18next/i18next-browser-languageDetector上使用推荐的检测器

答案 1 :(得分:0)

我从Locize那里得到了答案。非常感谢!

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"
  }
});

答案 2 :(得分:0)