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

React / Redux and Multilingual (Internationalization) Apps - Architecture

我正在构建一个需要以多种语言和区域设置提供的应用程序。

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

以下是我的要求(真的是"standard"):

以下是我能想到的可能的解决方案:

每个组件单独处理翻译

这意味着每个组件都有例如一组 en.json、fr.json 等文件以及已翻译的字符串。以及一个辅助函数,可帮助根据所选语言从那些值中读取值。

每个组件通过 props 接收翻译

所以他们不知道当前的语言,他们只是将字符串列表作为恰好与当前语言匹配的 props

你稍微绕过道具,可能会使用 context 东西来传递当前的语言

如果您有任何其他想法,请尽管说!

你是怎么做到的?

在尝试了很多解决方案之后,我认为我找到了一个效果很好的解决方案,它应该是 React 0.14 的惯用解决方案(即它不使用 mixin,而是使用高阶组件)(编辑:当然 React 15 也完全没问题!)。

这里是解决方案,从底部开始(各个组件):

组件

您的组件唯一需要的(按照惯例)是 strings 道具。 它应该是一个包含您的组件所需的各种字符串的对象,但实际上它的形状取决于您。

它确实包含默认翻译,因此您可以在其他地方使用该组件而无需提供任何翻译(它可以直接使用默认语言,在本例中为英语)

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

高阶分量

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

translate 在这种情况下是一个高阶组件,它包装了您的组件,并提供了一些额外的功能(这种结构取代了以前版本的 React 的混入)。

第一个参数是用于在翻译文件中查找翻译的键(我在这里使用了组件的名称,但它可以是任何名称)。第二个(注意函数是柯里化的,以允许 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;
    };
}

这不是魔法:它只会从上下文中读取当前语言(并且该上下文不会在整个代码库中流血,只是在这个包装器中使用),然后从 loaded 中获取相关的字符串对象文件。在这个例子中这段逻辑很幼稚,完全可以按照你想要的方式完成。

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

在层次结构的最顶端

在根组件上,您只需要从当前状态设置当前语言即可。下面的示例使用 Redux 作为类似 Flux 的实现,但它可以很容易地转换为任何其他 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 不需要由 translate() 包装并且可以是独立的,允许任何其他希望以自己的方式提供 strings 的人重用它。

[编辑:2016 年 3 月 31 日]:我最近在一个回顾委员会(用于敏捷回顾)工作,它是用 React 和 Redux 构建的,并且是多语言的。 由于很多人在评论中要求一个真实的例子,这里是:

您可以在此处找到代码:https://github.com/antoinejaussoin/retro-board/tree/master

Antoine 的解决方案工作正常,但有一些注意事项:

  • 它直接使用 React 上下文,我在使用 Redux 时倾向于避免这种情况
  • 它直接从文件中导入短语,如果您想在客户端运行时获取所需的语言,这可能会有问题
  • 它不使用任何轻量级的 i18n 库,但不会让您访问复数和插值等方便的翻译功能

这就是我们构建 redux-polyglot on top of both Redux and AirBNB's Polyglot 的原因。
(我是作者之一)

它提供:

  • 一个 reducer,用于在你的 Redux store 中存储语言和相应的消息。您可以通过以下任一方式提供:
    • 一个中间件,您可以配置它来捕获特定操作、扣除当前语言和 get/fetch 相关消息。
    • 直接派送 setLanguage(lang, messages)
  • 一个 getP(state) 选择器,它检索一个 P 公开 4 种方法的对象:
    • t(key):原始多语言 T 函数
    • tc(key): 大写翻译
    • tu(key): 大写翻译
    • tm(morphism)(key):自定义变形翻译
  • 一个getLocale(state)获取当前语言的选择器
  • 一个 translate 高阶组件,通过在 props
  • 中注入 p 对象来增强你的 React 组件

简单用法示例:

发送新语言:

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

有的话请告诉我question/suggestion!

根据我对此的研究,在 JavaScript、ICU and gettext.

中似乎有两种主要的国际化方法

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

令我吃惊的是支持如此之差。我来自 PHP 世界,不是 CakePHP 就是 WordPress。在这两种情况下,基本标准是所有字符串都简单地用 __('') 包围,然后再往下走,您可以很容易地使用 PO 文件获得翻译。

获取文本

您熟悉用于格式化字符串的 sprintf,并且 PO 文件将被成千上万的不同机构轻松翻译。

有两个常用选项:

  1. i18next, with usage described by this arkency.com blog post
  2. Jed, with usage described by the sentry.io post and this React+Redux post,

两者都支持 gettext 样式、字符串的 sprintf 样式格式化以及导入/导出到 PO 文件。

i18next有个React extension developed by themselves. Jed doesn't. Sentry.io appear to use a custom integration of Jed with React. The React+Redux post,建议用

Tools: jed + po2json + jsxgettext

然而,Jed 似乎是一个更注重 gettext 的实现 - 也就是说它表达了意图,而 i18next 只是将它作为一个选项。

重症监护病房

这对翻译的边缘情况有更多支持,例如用于处理性别。如果您要翻译成更复杂的语言,我想您会看到这样做的好处。

一个流行的选择是 messageformat.js. Discussed briefly in this sentry.io blog tutorial. messageformat.js is actually developed by the same person that wrote Jed. He makes quite stong claims for using ICU:

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

I also maintain messageformat.js. If you don't specifically need a gettext implementation, I might suggest using MessageFormat instead, as it has better support for plurals/gender and has built-in locale data.

粗略比较

使用 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'] }
);

messageformat.js(我阅读 guide 的最佳猜测):

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 redux state 并使用它,原因有很多:

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

2- 当用户更改语言时,您可以更改整个应用程序语言,甚至无需刷新 UI。

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

中检索新语言

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

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

6- 您可以将后端和前端字符串放在一个地方,例如在我的例子中,我使用 i18n-node 进行本地化,当用户更改 UI 语言时,我只做一个正常 API 调用,在后端,我只是 return i18n.getCatalog(req) 这将 return 所有用户字符串仅用于当前语言

我对 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- human-format 此模块允许您将数字 to/from 转换为人类可读的字符串,例如:

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

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

import moment from "moment";

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

更新 (14/06/2019)

目前有很多框架使用react context实现相同的概念API(没有redux),我个人推荐I18next

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

您的代码将类似于:

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

附带样品:

  • webpack
  • 疯狂
  • expo.js
  • next.js
  • 故事书集成
  • 大放异彩
  • 数据
  • ...

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

除此之外,您还应该考虑开发期间的工作流程以及之后的翻译人员 -> https://www.youtube.com/watch?v=9NOzJhgmyQE

我想提出一个使用 create-react-app 的简单解决方案。

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

Web 服务器将根据 Accept-Language header 自动提供正确的语言,或者通过设置 cookie.

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

翻译数据放在同一个组件文件中,使用它,连同样式,html 和代码。

这里我们有完全独立的组件,负责自己的状态、视图、翻译:

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/shared/Types.tsx

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

/src/store/actions/actionTypes.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;

第 3 部分: JSON 个文件。在这个例子中,只是一个带有几种语言的测试值:

/src/locales/en/translation.json

{
    "message": "Welcome"
}

/src/locales/ca/translation.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;

抱歉 post 扩展,但我试图展示完整的设置以澄清所有疑问。完成此操作后,可以非常快速和灵活地在任何地方添加语言和使用描述。