【问题标题】:How to integrate next-i18next, nextjs & locize如何集成 next-i18next、nextjs & locize
【发布时间】:2019-09-23 11:24:43
【问题描述】:

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

谢谢。

【问题讨论】:

    标签: reactjs next.js i18next


    【解决方案1】:

    2020 年 3 月编辑:检查 https://github.com/UnlyEd/next-right-now 样板文件,它使用以下配置并提供 Next.js 9(无服务器)+ i18next/react-i18next + Locize 的真实用例示例。 (免责声明:我是作者)


    感谢@quebone 的自动回答。我用它来改进我自己的配置,即使用带有 Next.js 的 TypeScript,但我没有像你一样使用 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 提供​​的页面将不包含正确的句子。

    为避免这种情况,您需要手动预取当前语言所依赖的所有命名空间。此步骤必须在getInitialProps 函数中的pages/_app.tsx 中完成。 (我用的是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 中工作,不知道如何。


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


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

    【讨论】:

      【解决方案2】:

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

      【讨论】:

      • 你能解释一下,这个文件是什么? i18n.js?我也在尝试将 i18n 集成到 Next.js 应用程序中。
      • 对于 SSG,还有一个替代方案:dev.to/adrai/…
      【解决方案3】:
      猜你喜欢
      • 2020-06-07
      • 2022-01-16
      • 1970-01-01
      • 2021-08-09
      • 2020-11-04
      • 2021-06-16
      • 1970-01-01
      • 1970-01-01
      • 2021-07-20
      相关资源
      最近更新 更多