React/Redux 和多语言(国际化)应用程序-体系结构

我正在建立一个应用程序,将需要在多种语言和地区可用。

我的问题不是纯粹的技术问题,而是关于架构,以及人们在生产中实际使用的模式来解决这个问题。 我在任何地方都找不到这方面的“食谱”,所以我转向我最喜欢的问答网站:)

以下是我的要求(它们实际上是“标准”) :

  • 用户可以选择语言(琐碎的)
  • 更改语言后,界面应自动转换为新选定的语言
  • 目前我不太担心数字、日期等的格式,我想要一个简单的解决方案,只是转换字符串

Here are the possible solutions I could think off:

每个组件都独立地处理翻译

这意味着每个组件都有一组例如 en.json、 fr.json 等文件以及经过翻译的字符串。以及帮助从那些依赖于所选语言的值中读取值的助手函数。

  • Pro: more respectful of the React philosophy, each component is "standalone"
  • 缺点: 您不能将所有的翻译集中到一个文件中(例如,让其他人添加一种新语言)
  • 缺点: 你仍然需要通过当前的语言作为一个道具,在每一个血腥的组成部分和自己的孩子

每个组件通过道具接收翻译

所以他们不知道当前的语言,他们只是把一个字符串列表作为道具,恰好匹配当前的语言

  • 正方观点: 因为这些字符串是“从上面”来的,所以它们可以集中在某个地方
  • 缺点: 现在每个组件都绑定到翻译系统中,您不能只重用一个组件,每次都需要指定正确的字符串

你绕过道具一点,并可能使用 背景的东西,以传递当前的语言

  • 正方观点: 它基本上是透明的,不需要一直通过道具传递当前语言和/或翻译
  • 缺点: 它看起来使用起来很麻烦

如果你有任何其他的想法,请说!

你是怎么做到的?

59454 次浏览

After trying quite a few solutions, I think I found one that works well and should be an idiomatic solution for React 0.14 (i.e. it doesn't use mixins, but Higher Order Components) (edit: also perfectly fine with React 15 of course!).

So here the solution, starting by the bottom (the individual components):

组件

您的组件(按照惯例)唯一需要的是一个 strings道具。 It should be an object containing the various strings your Component needs, but really the shape of it is up to you.

It does contain the default translations, so you can use the component somewhere else without the need to provide any translation (it would work out of the box with the default language, english in this example)

import { default as React, PropTypes } from 'react';
import translate from './translate';


class MyComponent extends React.Component {
render() {


return (
<div>
{ this.props.strings.someTranslatedText }
</div>
);
}
}


MyComponent.propTypes = {
strings: PropTypes.object
};


MyComponent.defaultProps = {
strings: {
someTranslatedText: 'Hello World'
}
};


export default translate('MyComponent')(MyComponent);

The Higher Order Component

在前面的代码片段中,您可能在最后一行中注意到了这一点: translate('MyComponent')(MyComponent)

在本例中,translate是一个高阶组件,它包装您的组件,并提供一些额外的功能(此构造替换了以前版本的 React 的 mix)。

第一个参数是一个键,用于查找翻译文件中的翻译(我在这里使用了组件的名称,但它可以是任何名称)。第二个(注意,为了允许 ES7装饰器,函数是粗糙的)是要包装的组件本身。

下面是翻译组件的代码:

import { default as React } from 'react';
import en from '../i18n/en';
import fr from '../i18n/fr';


const languages = {
en,
fr
};


export default function translate(key) {
return Component => {
class TranslationComponent extends React.Component {
render() {
console.log('current language: ', this.context.currentLanguage);
var strings = languages[this.context.currentLanguage][key];
return <Component {...this.props} {...this.state} strings={strings} />;
}
}


TranslationComponent.contextTypes = {
currentLanguage: React.PropTypes.string
};


return TranslationComponent;
};
}

It's not magic: it will just read the current language from the context (and that context doesn't bleed all over the code base, just used here in this wrapper), and then get the relevant strings object from loaded files. This piece of logic is quite naïve in this example, could be done the way you want really.

重要的一点是,它从上下文中提取当前语言,并根据所提供的键将其转换为字符串。

At the very top of the hierarchy

On the root component, you just need to set the current language from your current state. The following example is using Redux as the Flux-like implementation, but it can easily be converted using any other framework/pattern/library.

import { default as React, PropTypes } from 'react';
import Menu from '../components/Menu';
import { connect } from 'react-redux';
import { changeLanguage } from '../state/lang';


class App extends React.Component {
render() {
return (
<div>
<Menu onLanguageChange={this.props.changeLanguage}/>
<div className="">
{this.props.children}
</div>


</div>


);
}


getChildContext() {
return {
currentLanguage: this.props.currentLanguage
};
}
}


App.propTypes = {
children: PropTypes.object.isRequired,
};


App.childContextTypes = {
currentLanguage: PropTypes.string.isRequired
};


function select(state){
return {user: state.auth.user, currentLanguage: state.lang.current};
}


function mapDispatchToProps(dispatch){
return {
changeLanguage: (lang) => dispatch(changeLanguage(lang))
};
}


export default connect(select, mapDispatchToProps)(App);

最后,翻译文件:

翻译文件

// en.js
export default {
MyComponent: {
someTranslatedText: 'Hello World'
},
SomeOtherComponent: {
foo: 'bar'
}
};


// fr.js
export default {
MyComponent: {
someTranslatedText: 'Salut le monde'
},
SomeOtherComponent: {
foo: 'bar mais en français'
}
};

你们觉得怎么样?

我认为它解决了我在问题中试图避免的所有问题: 翻译逻辑不会渗透到源代码中,它是相当孤立的,并且允许在没有它的情况下重用组件。

例如,MyComponent 不需要通过 trans ()进行包装,它可以是独立的,允许任何其他希望以自己的方式提供 strings的人重用它。

[Edit: 31/03/2016]: I recently worked on a Retrospective Board (for Agile Retrospectives), built with React & Redux, and is multilingual. 因为很多人在评论中要求一个现实生活中的例子,这里是:

你可以在这里找到代码: https://github.com/antoinejaussoin/retro-board/tree/master

安托万的解决方案行之有效,但有一些注意事项:

  • 它直接使用 React 上下文,在已经使用 Redux 时,我倾向于避免使用 React 上下文
  • It imports directly phrases from a file, which can be problematic if you want to fetch needed language at runtime, client-side
  • 它不使用任何 i18n 库,这是轻量级的,但不允许您访问方便的翻译功能,如多元化和插值

这就是为什么我们在 Redux 和 AirBNB 的 通晓多国语言之上构建了 通晓多种语言
(我是作者之一)

它规定:

  • a reducer to store language and corresponding messages in your Redux store. You can supply both by either :
    • a middleware that you can configure to catch specific action, deduct current language and get/fetch associated messages.
    • 直接派遣 setLanguage(lang, messages)
  • 检索公开4个方法的 P对象的 getP(state)选择器:
    • t(key): 原始多语言 T 功能
    • tc(key): capitalized translation
    • 大写翻译
    • 自定义变形翻译
  • 获取当前语言的 getLocale(state)选择器
  • 通过在道具中注入 p对象来增强反应组件的 translate高阶组件

简单使用例子:

发送新语言:

import setLanguage from 'redux-polyglot/setLanguage';


store.dispatch(setLanguage('en', {
common: { hello_world: 'Hello world' } } }
}));

组成部分:

import React, { PropTypes } from 'react';
import translate from 'redux-polyglot/translate';


const MyComponent = props => (
<div className='someId'>
{props.p.t('common.hello_world')}
</div>
);
MyComponent.propTypes = {
p: PropTypes.shape({t: PropTypes.func.isRequired}).isRequired,
}
export default translate(MyComponent);

Please tell me if you have any question/suggestion !

从我对此的研究来看,在 JavaScript 中 i18n 主要有两种方法: 重症监护室收到短信

我只用过 gettext,所以我有偏见。

令我惊讶的是,支持是如此之少。我来自 PHP 世界,不管是 CakePHP 还是 WordPress。在这两种情况下,所有字符串都简单地被 __('')包围是一个基本标准,然后进一步使用 PO 文件很容易得到翻译。

收到短信

您可以熟悉 sprintf 格式化字符串和 PO 文件,它们可以被成千上万个不同的机构轻松翻译。

有两种流行的选择:

  1. I18next ,其用法如下: arkency.com 博客文章
  2. Jed ,使用 哨兵,我在岗哨和这个 React + Redux post描述的用法,

它们都支持 gettext 样式、 sprintf 样式的字符串格式设置以及导入/导出到 PO 文件。

I18next 有一个自行开发的 反应延伸。杰德不知道。Io 似乎使用了 Jed 与 React 的自定义集成。React + Redux post建议使用

工具: jed + po2json + jsxgettext

然而 Jed 看起来更像是一个以 gettext 为中心的实现——也就是它所表达的意图,而 i18next 只是将其作为一个选项。

ICU

This has more support for the edge cases around translations, e.g. for dealing with gender. I think you will see the benefits from this if you have more complex languages to translate into.

一个流行的选择是 Messageformat. js。在此 Sentry.io 博客教程简要讨论。Js 实际上是由编写 Jed 的同一个人开发的。他强烈要求使用重症监护室:

Jed is feature complete in my opinion. I am happy to fix bugs, but generally am not interested in adding more to the library.

我还维护 messageformat. js。如果您不特别需要 gettext 实现,我可能建议使用 MessageFormat,因为它更好地支持复数/性别,并且具有内置的区域设置数据。

粗略的比较

用 sprintf 获取文本:

i18next.t('Hello world!');
i18next.t(
'The first 4 letters of the english alphabet are: %s, %s, %s and %s',
{ postProcess: 'sprintf', sprintf: ['a', 'b', 'c', 'd'] }
);

Js (通过阅读 向导我的最佳猜测) :

mf.compile('Hello world!')();
mf.compile(
'The first 4 letters of the english alphabet are: {s1}, {s2}, {s3} and {s4}'
)({ s1: 'a', s2: 'b', s3: 'c', s4: 'd' });

根据我的经验,最好的方法是创建一个 I18n 还原状态并使用它,原因有很多:

这将允许您从数据库、本地文件甚至从模板引擎(如 EJS 或 Jade)传递初始值

当用户改变语言时,你可以改变整个应用程序语言,甚至不需要刷新 UI。

3-当用户更改语言时,这也将允许您从 API、本地文件甚至常量中检索新语言

4-您还可以使用字符串保存其他重要内容,如时区、货币、方向(RTL/LTR)和可用语言列表

5-您可以将变更语言定义为一个正常的 redux 操作

6-你可以把你的后端和前端字符串放在一个地方,例如,在我的例子中,我使用 i18n-node进行本地化,当用户改变 UI 语言时,我只是做一个普通的 API 调用,在后端,我只返回 i18n.getCatalog(req),这将返回当前语言的所有用户字符串

我对 i18n 初始状态的建议是:

{
"language":"ar",
"availableLanguages":[
{"code":"en","name": "English"},
{"code":"ar","name":"عربي"}
],
"catalog":[
"Hello":"مرحباً",
"Thank You":"شكراً",
"You have {count} new messages":"لديك {count} رسائل جديدة"
],
"timezone":"",
"currency":"",
"direction":"rtl",
}

I18n 的额外有用模块:

1-String-template 字符串模板这将允许您在目录字符串之间注入值,例如:

import template from "string-template";
const count = 7;
//....
template(i18n.catalog["You have {count} new messages"],{count}) // لديك ٧ رسائل جديدة

2-人工格式这个模块允许你将一个数字转换成/从一个人类可读的字符串,例如:

import humanFormat from "human-format";
//...
humanFormat(1337); // => '1.34 k'
// you can pass your own translated scale, e.g: humanFormat(1337,MyScale)

3-瞬间最著名的日期和时间 npm 库,你可以翻译瞬间,但它已经有一个内置的翻译只是你需要通过当前的状态语言,例如:

import moment from "moment";


const umoment = moment().locale(i18n.language);
umoment.format('MMMM Do YYYY, h:mm:ss a'); // أيار مايو ٢ ٢٠١٧، ٥:١٩:٥٥ م

更新(14/06/2019)

目前,有许多框架使用反应上下文 API (没有还原)来实现相同的概念,我个人推荐使用 I18next

如果还没有看完 https://react.i18next.com/可能是一个不错的建议。它是基于 i18next: 学习一次-到处翻译。

Your code will look something like:

<div>{t('simpleContent')}</div>
<Trans i18nKey="userMessagesUnread" count={count}>
Hello <strong title={t('nameTitle')}>\{\{name}}</strong>, you have \{\{count}} unread message. <Link to="/msgs">Go to messages</Link>.
</Trans>

附带样品:

  • 网络包
  • Cra
  • Expo.js
  • 下一个 Js
  • 童话集成
  • razzle
  • 那个
  • ...

https://github.com/i18next/react-i18next/tree/master/example

除此之外,你还应该考虑开发过程中的工作流程,以及以后的翻译工作—— > https://www.youtube.com/watch?v=9NOzJhgmyQE

我想提出一个使用 创建-反应-应用程序的简单解决方案。

应用程序将为每种语言分别构建,因此整个翻译逻辑将从应用程序中移出。

Web 服务器将自动提供正确的语言,具体取决于 接受-语言标头,或者通过设置 饼干手动提供。

大多数情况下,我们不会改变语言超过一次,如果有的话)

将翻译数据放入使用它的同一个组件文件中,沿着样式、 html 和代码。

And here we have fully independent component that responsible for its own state, view, translation:

import React from 'react';
import {withStyles} from 'material-ui/styles';
import {languageForm} from './common-language';
const {REACT_APP_LANGUAGE: LANGUAGE} = process.env;
export let language; // define and export language if you wish
class Component extends React.Component {
render() {
return (
<div className={this.props.classes.someStyle}>
<h2>{language.title}</h2>
<p>{language.description}</p>
<p>{language.amount}</p>
<button>{languageForm.save}</button>
</div>
);
}
}
const styles = theme => ({
someStyle: {padding: 10},
});
export default withStyles(styles)(Component);
// sets laguage at build time
language = (
LANGUAGE === 'ru' ? { // Russian
title: 'Транзакции',
description: 'Описание',
amount: 'Сумма',
} :
LANGUAGE === 'ee' ? { // Estonian
title: 'Tehingud',
description: 'Kirjeldus',
amount: 'Summa',
} :
{ // default language // English
title: 'Transactions',
description: 'Description',
amount: 'Sum',
}
);

把语言环境变量加入你的 package.json

"start": "REACT_APP_LANGUAGE=ru npm-run-all -p watch-css start-js",
"build": "REACT_APP_LANGUAGE=ru npm-run-all build-css build-js",

够了!

我的原始答案还包括更多的单一方法,每个翻译都有一个 json 文件:

Lang/Ru.json

{"hello": "Привет"}

Lib/lang.js

export default require(`../lang/${process.env.REACT_APP_LANGUAGE}.json`);

Src/App.jsx

import lang from '../lib/lang.js';
console.log(lang.hello);

另一个在 Typescript 实现的(小)提案,基于 ES6 & Redux & Hooks & jSON,没有第三方依赖。

由于选定的语言是在 redux 状态下加载的,因此不需要呈现所有页面,只需要呈现受影响的文本,就可以快速更改语言。

第1部分: Redux 设置:

/src/share/Typees.tsx

export type Language = 'EN' | 'CA';

/src/store/actions/actionTypees.tsx

export const SET_LANGUAGE = 'SET_LANGUAGE';

/src/store/actions/language. tsx:

import * as actionTypes from './actionTypes';
import { Language } from '../../shared/Types';


export const setLanguage = (language: Language) => ({
type: actionTypes.SET_LANGUAGE,
language: language,
});

/src/store/reducers/language.tsx:

import * as actionTypes from '../action/actionTypes';
import { Language } from '../../shared/Types';
import { RootState } from './reducer';
import dataEN from '../../locales/en/translation.json';
import dataCA from '../../locales/ca/translation.json';


type rootState = RootState['language'];


interface State extends rootState { }
interface Action extends rootState {
type: string,
}


const initialState = {
language: 'EN' as Language,
data: dataEN,
};


const setLanguage = (state: State, action: Action) => {
let data;
switch (action.language) {
case 'EN':
data = dataEN;
break;
case 'CA':
data = dataCA;
break;
default:
break;
}
return {
...state,
...{ language: action.language,
data: data,
}
};
};


const reducer = (state = initialState, action: Action) => {
switch (action.type) {
case actionTypes.SET_LANGUAGE: return setLanguage(state, action);
default: return state;
}
};


export default reducer;

/src/store/reducers/reducer.tsx

import { useSelector, TypedUseSelectorHook } from 'react-redux';
import { Language } from '../../shared/Types';


export interface RootState {
language: {
language: Language,
data: any,
}
};


export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;

/src/App.tsx

import React from 'react';
import { Provider } from 'react-redux';
import { createStore, combineReducers } from 'redux';
import languageReducer from './store/reducers/language';
import styles from './App.module.css';


// Set global state variables through Redux
const rootReducer = combineReducers({
language: languageReducer,
});
const store = createStore(rootReducer);


const App = () => {


return (
<Provider store={store}>
<div className={styles.App}>
// Your components
</div>
</Provider>
);
}


export default App;

第2部分: 带语言的下拉菜单。在我的例子中,我将这个组件放在导航栏中,以便能够在任何屏幕上更改语言:

/src/components/Navigation/Language.tsx

import React from 'react';
import { useDispatch } from 'react-redux';
import { setLanguage } from '../../store/action/language';
import { useTypedSelector } from '../../store/reducers/reducer';
import { Language as Lang } from '../../shared/Types';
import styles from './Language.module.css';


const Language = () => {
const dispatch = useDispatch();
const language = useTypedSelector(state => state.language.language);
    

return (
<div>
<select
className={styles.Select}
value={language}
onChange={e => dispatch(setLanguage(e.currentTarget.value as Lang))}>
<option value="EN">EN</option>
<option value="CA">CA</option>
</select>
</div>
);
};


export default Language;

Part 3: JSON files. In this example, just a test value with a couple of languages:

/src/locales/en/transationation.json

{
"message": "Welcome"
}

/src/locales/ca/transationation.json

{
"message": "Benvinguts"
}

第4部分: 现在,在任何屏幕上,都可以用 redux 设置中选定的语言显示文本:

import React from 'react';
import { useTypedSelector } from '../../store/reducers/reducer';


const Test = () => {
const t = useTypedSelector(state => state.language.data);


return (
<div> {t.message} </div>
)
}


export default Test;

很抱歉后延期,但我试图显示完整的设置,以澄清所有的疑问。一旦完成这项工作,添加语言和在任何地方使用描述都是非常快速和灵活的。