用i18n实现React国际化

2,116 阅读10分钟

当我的上一个客户向我询问React的国际化问题时,我费尽心思为他们准备了一个演示。在这个React教程中,我想向你展示我所学到的关于翻译React应用程序的要领。

React的国际化我应该使用哪个库?

现在有两个流行的React国际化库:react-intlreact-i18next。虽然考虑到统计数据,react-intl是最受欢迎的,但大多数React开发者似乎喜欢react-i18next。

以下是我从我的追随者那里听到的react-i18next比react-intl的三个优势:

  • 当涉及到新的React功能(如React Hooks)时,采用率快
  • 高度有效的API
  • 不受React约束的i18n生态系统

在缩小了一些优势、劣势和差异之后,我决定使用react-i18next进行进一步研究。这不仅是因为我以前使用过这个库作为我的i18n的首选库,而且还因为普遍的意见似乎都指向这个库。

值得一提的是,还有两个新兴的React国际化库在那里。LinguiJSFBT。我没有尝试它们,但它们看起来很有趣。

React与react-i18next:i18n设置

在我们开始翻译React应用程序之前,我们需要安装其库:

npm install i18next react-i18next i18next-xhr-backend

我们将使用i18next核心库进行设置,并使用react-i18next库将其国际化功能连接到React。src/i18n.js中的一个i18n设置文件的例子可能看起来如下:

import i18n from 'i18next';import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-xhr-backend';
i18n  // learn more: https://github.com/i18next/i18next-xhr-backend  .use(Backend)  // connect with React  .use(initReactI18next)  // for all options read: https://www.i18next.com/overview/configuration-options  .init({    debug: true,
    lng: 'en',    fallbackLng: 'en',    whitelist: ['en', 'de'],
    interpolation: {      escapeValue: false, // not needed for react as it escapes by default    },  });
export default i18n;

默认的i18n后端希望所有的翻译文件都能从网络服务器上提供。如果你使用create-react-app,你的*public/文件夹就足够了。如果你使用自定义Webpack与React设置,你需要自己设置这个public/*文件夹。

翻译文件的默认文件夹结构看起来像下面这样。

- public/--- locales/----- de------- translation.json----- en------- translation.json

两个翻译文件都可以有以下JSON内容,以便在React中开始使用i18n。

// de/translation.json
{  "welcome": "Hallo React"}
// en/translation.json
{  "welcome": "Hello React"}

回到你的src/i18n.js文件中,你可以为你的后台配置定义翻译文件的路径。但这不是必须的,因为无论如何它都是默认的。

import i18n from 'i18next';import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-xhr-backend';
i18n  // learn more: https://github.com/i18next/i18next-xhr-backend  .use(Backend)  // connect with React  .use(initReactI18next)  // for all options read: https://www.i18next.com/overview/configuration-options  .init({    debug: true,
    lng: 'en',    fallbackLng: 'en',    whitelist: ['en', 'de'],
    interpolation: {      escapeValue: false, // not needed for react as it escapes by default    },
    backend: {      loadPath: '/locales/{{lng}}/{{ns}}.json',    },  });
export default i18n;

在经历了i18n设置文件和翻译文件之后,让我们把国际化连接到React。在你的src/index.js文件,或者你设置React的地方,用React的Suspense组件将i18n连接到你的React应用程序。

import React, { Suspense } from 'react';import ReactDOM from 'react-dom';
import './index.css';import App from './App';
import './i18n';
ReactDOM.render(  <Suspense fallback={null}>    <App />  </Suspense>,  document.getElementById('root'));

所有的翻译文件都会异步加载到你的React应用程序。在这个例子中,当我们等待翻译文件时,我们什么都不渲染。如果你想提供一个回退组件,例如一个加载指示器,使用Suspense组件的回退属性。

最后,你可以在你的React组件中使用你的翻译文件。例如,在你的src/App.js中,一个文本的翻译可能看起来像下面这样。

import React from 'react';import { useTranslation } from 'react-i18next';
const App = () => {  const { t } = useTranslation();
  return (    <div>      <p>{t('welcome', 'Hello there')}</p>    </div>  );};
export default App;

React Hook给了我们一个叫做t 的函数,用于翻译我们React组件中的文本。它的第一个强制参数是翻译键(见public/locales/en/translation.json),第二个可选参数是所谓的工作文本。只要没有翻译,它就会默认为工作文本,或者默认为翻译键,如果首先没有工作文本的话。

React与react-i18next。多个文件(命名空间)

如果你想在一种语言中把你的翻译分成多个文件,可以用命名空间来实现。在这个例子中,我们将在每种语言中再添加一个翻译文件。

- public/--- locales/----- de------- translation.json------- welcome.json----- en------- translation.json------- welcome.json

所有的翻译文件可以有以下内容。

// de/translation.json
{  "de": "Deutsch",  "en": "Englisch"}
// en/translation.json
{  "de": "German",  "en": "English"}
// de/welcome.json
{  "title": "Hallo React",  "content": {    "text": "Willkommen bei uns."  }}
// en/welcome.json
{  "title": "Hello React",  "content": {    "text": "Welcome at our place."  }}

在我们的React组件中,通过i18n useTranslation Hook,我们可以加载这两个命名空间,并用命名空间分隔符(:)独立使用。我们也可以在JSON中接下来翻译,并用嵌套分隔符(.)引用它们。

import React from 'react';import { useTranslation } from 'react-i18next';
const App = () => {  const { t } = useTranslation(['translation', 'welcome']);
  return (    <div>      <button type="button">{t('translation:de')}</button>
      <button type="button">{t('translation:en')}</button>
      <h1>{t('welcome:title', 'Hello there.')}</h1>
      <p>{t('welcome:content.text', 'Welcome here.')}</p>    </div>  );};
export default App;

本质上这就是你如何将你的语言分割成多个文件(命名空间)。而translation.json文件是用于整个应用程序的通用翻译的地方,所有其他文件可能是特定领域的翻译。这样,在某些页面,你可以只加载某些命名空间。

React与react-i18next。Trans组件

Trans组件可以作为useTranslation钩子的替代品。

import React from 'react';import { useTranslation, Trans } from 'react-i18next';
const App = () => {  const { t } = useTranslation(['translation', 'welcome']);
  return (    <div>      <button type="button">{t('translation:de')}</button>
      <button type="button">{t('translation:en')}</button>
      <h1>{t('welcome:title', 'Hello there.')}</h1>
      <p>        <Trans i18nKey="welcome:content.text">          Welcome at <strong>our place</strong>.        </Trans>      </p>    </div>  );};
export default App;

在你的翻译文件(例如public/locales/en/welcome.json)中,你可以用占位符引用内部HTML元素,例如strong标签。

{  "title": "Hello React",  "content": {    "text": "Welcome at <1>our place</1>."  }}

与useTranslation钩子相比,Trans组件帮助你对内部HTML元素进行插值。然而,大多数时候,翻译钩子应该足以满足你的需要。

React与react-i18next。改变语言

如果你想让你的用户选择切换语言,可以再次使用国际化挂钩。

import React from 'react';import { useTranslation } from 'react-i18next';
const App = () => {  const { t, i18n } = useTranslation(['translation', 'welcome']);
  const changeLanguage = code => {    i18n.changeLanguage(code);  };
  return (    <div>      <button type="button" onClick={() => changeLanguage('de')}>        {t('translation:de')}      </button>
      <button type="button" onClick={() => changeLanguage('en')}>        {t('translation:en')}      </button>
      <h1>{t('welcome:title', 'Hello there.')}</h1>
      <p>{t('welcome:content.text', 'Welcome here.')}</p>    </div>  );};
export default App;

所有的命名空间文件都为当前选择的语言加载。

从React中提取翻译

到目前为止,你的代码中的每一个翻译键都需要在你的翻译文件(命名空间)中的所有语言中找到相应的翻译。作为一个开发人员,手动添加这些翻译键可能是一项繁琐的工作。毕竟,这些文件应该有一套完整的翻译键,以便最终将它们交给翻译人员。幸运的是,有一些选项可以从你的React应用程序中自动提取翻译。

自定义翻译后端

前面的设置使用我们的网络应用程序的公共文件系统来提供所有的翻译。该设置可以通过报告缺失翻译的功能进行扩展。

import i18n from 'i18next';import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-xhr-backend';
i18n  // learn more: https://github.com/i18next/i18next-xhr-backend  .use(Backend)  // connect with React  .use(initReactI18next)  // for all options read: https://www.i18next.com/overview/configuration-options  .init({    debug: true,
    lng: 'en',    fallbackLng: 'en',    whitelist: ['en', 'de'],
    interpolation: {      escapeValue: false, // not needed for react as it escapes by default    },
    saveMissing: true,    saveMissingTo: 'all',
    backend: {      loadPath: '/locales/{{lng}}/{{ns}}.json',      addPath: '/locales/add/{{lng}}/{{ns}}',    },  });
export default i18n;

然而,这可能会导致授权错误,因为我们可能不被允许写到这些文件。另一个选择是有一个自定义的后端应用程序,为我们的翻译提供服务,同时也接收关于丢失的翻译键的信息。在这个例子中,我展示了如何将缺失的翻译密钥信息发送到一个自定义的后端,但没有展示如何首先提供翻译。首先,在你的i18n设置文件中定义API端点。

import i18n from 'i18next';import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-xhr-backend';
i18n  // learn more: https://github.com/i18next/i18next-xhr-backend  .use(Backend)  // connect with React  .use(initReactI18next)  // for all options read: https://www.i18next.com/overview/configuration-options  .init({    debug: true,
    lng: 'en',    fallbackLng: 'en',    whitelist: ['en', 'de'],
    interpolation: {      escapeValue: false, // not needed for react as it escapes by default    },
    saveMissing: true,    saveMissingTo: 'all',
    backend: {      loadPath: '/locales/{{lng}}/{{ns}}.json',      addPath: 'http://localhost:8000/locales/add/{{lng}}/{{ns}}',    },  });
export default i18n;

其次,创建一个自定义的后端,可以是一个普通的Express服务器,接收丢失的翻译密钥。

import express from 'express';import cors from 'cors';import bodyParser from 'body-parser';
const app = express();
app.use(cors());app.use(bodyParser.json());app.use(bodyParser.urlencoded({ extended: true }));
app.post('/locales/add/:lng/:ns', (req, res) => {  const { lng, ns } = req.params;
  console.log(req.body);  console.log(lng, ns);
  res.sendStatus(200);});
app.listen(8000, () =>  console.log(`Listening!`),);

但这有个前提,即所有缺失的翻译键只有在代码中使用了这个翻译键后才会报告给后端。因此,例如,如果一个具有特定翻译的React组件没有被渲染,它不会被报告给后端。

语言提取脚本

自定义国际化后端的另一个选择是一个脚本,从你的代码中提取所有的翻译。一旦你运行这个脚本,它就会从你的应用程序中提取所有的翻译键,并将它们与你的翻译文件相匹配。让我们使用其中的一个脚本。首先,在命令行中安装它。

npm install --save-dev i18next-parser

第二,在你的package.json文件中引入一个新的npm脚本来使用这个脚本。

{  ...  "scripts": {    ...    "extract": "i18next --config i18next-parser.config.js"  },  ...}

第三,创建一个i18next-parser.config.js配置文件用于提取。

module.exports = {  createOldCatalogs: true,  indentation: 2,  lexers: {    js: ['JsxLexer'],    ts: ['JsxLexer'],    jsx: ['JsxLexer'],    tsx: ['JsxLexer'],
    default: ['JsxLexer'],  },  locales: ['en', 'de'],  output: 'public/locales/$LOCALE/$NAMESPACE.json',  input: ['src/**/*.{js,jsx,ts,tsx}'],  verbose: true,};

最后,用npm run extract 执行该脚本,并验证所有的键被添加到你的翻译文件中。与定制的后台解决方案相比,脚本提取收集了所有缺失的翻译键,而不使用实际的应用程序。

用Locize进行提取和所见即所得

然后是react-i18next的企业工具。Locize。你可以通过npm在命令行上安装它。

npm install i18next-locize-backend

接下来在他们的网站上注册,在那里为你的应用程序创建一个项目。项目创建成功后,你应该得到一个项目ID和一个API密钥,可以在你的src/i18n.js设置中使用。

import i18n from 'i18next';import { initReactI18next } from 'react-i18next';
import LocizeBackend from 'i18next-locize-backend';
i18n  .use(LocizeBackend)  // connect with React  .use(initReactI18next)  // for all options read: https://www.i18next.com/overview/configuration-options  .init({    debug: true,
    lng: 'en',    fallbackLng: 'en',    whitelist: ['en', 'de'],
    interpolation: {      escapeValue: false, // not needed for react as it escapes by default    },
    // ** Enterprise https://locize.com **
    saveMissing: true,    saveMissingTo: 'all',
    backend: {      projectId: 'xxx',      apiKey: 'yyy',      referenceLng: 'en',    },  });
export default i18n;

之后,所有缺失的翻译密钥将被转移到Locize后台。你的项目的Locize仪表板应该显示所有缺失的密钥,在那里也可以为你的项目添加更多的语言。从那里,开始为翻译键插入所有的翻译,或把项目交给你的翻译员。每次你在项目的仪表板上添加一个翻译,你应该在刷新页面后在你的实际应用中看到它。

此外,Locize还带有一个所见即所得的编辑器。先通过命令安装它。

npm install locize-editor

然后在你的i18n设置中使用它。

import i18n from 'i18next';import { initReactI18next } from 'react-i18next';
import LocizeBackend from 'i18next-locize-backend';import LocizeEditor from 'locize-editor';
i18n  .use(LocizeBackend)  .use(LocizeEditor)  // connect with React  .use(initReactI18next)  // for all options read: https://www.i18next.com/overview/configuration-options  .init({    debug: true,
    lng: 'en',    fallbackLng: 'en',    whitelist: ['en', 'de'],
    interpolation: {      escapeValue: false, // not needed for react as it escapes by default    },
    // ** Enterprise https://locize.com **
    saveMissing: true,    saveMissingTo: 'all',
    backend: {      projectId: 'xxx',      apiKey: 'yyy',      referenceLng: 'en',    },  });
export default i18n;

最后,用以下查询扩展打开你的React应用程序:http://localhost:3000/?locize=true 。你应该看到一个所见即所得的打开方式,使你能够调整你的翻译。你也可以在你的React应用程序中点击文本,所见即所得编辑器将显示它的正确翻译。


在本教程中,你已经了解了React的不同国际化库。它还教你如何设置 react-i18next 库,如何在多语言和命名空间中使用它,以及如何以各种方式从你的 React 应用程序中自动提取翻译。这里展示的一切都可以在这个GitHub仓库中以代码的形式体验。