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

时间:2015-10-29 12:11:22

标签: architecture internationalization reactjs translation redux

我正在构建一个需要以多种语言和语言环境提供的应用。

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

以下是我的要求(他们真的是"标准"):

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

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

每个组件都单独处理翻译

这意味着每个组件都有一组en.json,fr.json等文件,旁边是翻译后的字符串。还有一个辅助函数可以帮助读取取决于所选语言的值。

  • 专业:更尊重React的理念,每个组件都是独立的#34;
  • 缺点:您无法将所有翻译集中在一个文件中(例如让其他人添加新语言)
  • 缺点:您仍然需要将当前语言作为道具传递给每个血腥组件及其子女

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

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

  • 亲:因为这些字符串来自顶部"它们可以集中在某处
  • 缺点:现在每个组件都绑定到翻译系统中,您只能重复使用一个组件,每次都需要指定正确的字符串

您稍微绕过道具并可能使用context内容传递当前语言

  • 亲:它大部分是透明的,不必一直通过道具传递当前的语言和/或翻译
  • 缺点:使用起来很麻烦

如果您有任何其他想法,请说出来!

你是怎么做到的?

7 个答案:

答案 0 :(得分:101)

在尝试了很多解决方案之后,我想我找到了一个效果很好的应该是React 0.14的惯用解决方案(即它不使用mixins,而是使用高阶组件)(编辑 :当然还可以使用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的混合)。

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

以下是翻译组件的代码:

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

这不是魔术:它只是从上下文中读取当前语言(并且该上下文不会在整个代码库中流失,仅在此包装器中使用),然后从加载的文件中获取相关的字符串对象。这个逻辑在这个例子中非常天真,可以按你想要的方式完成。

重要的一点是它从上下文中获取当前语言,并在给定密钥的情况下将其转换为字符串。

位于层次结构的最顶层

在根组件上,您只需要从当前状态设置当前语言。以下示例使用Redux作为类似Flux的实现,但可以使用任何其他框架/模式/库轻松转换。

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的人重用它。

[编辑:31/03/2016]:我最近参与了一个Retrospective Board(敏捷回顾),用React&amp; amp; Redux,并且是多语言的。 由于很多人在评论中要求提供一个真实的例子,所以这里是:

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

答案 1 :(得分:15)

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

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

2-当用户更改语言时,您甚至无需刷新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- human-format此模块允许您将数字转换为人类可读字符串,例如:

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库,你可以翻译片刻,但它已经有一个内置的翻译,只需你传递当前的状态语言,例如:

import moment from "moment";

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

答案 2 :(得分:5)

Antoine的解决方案运行良好,但有一些注意事项:

  • 它直接使用React上下文,我在使用Redux
  • 时往往会避免使用它
  • 它直接导入文件中的短语,如果您想在运行时获取所需的语言,客户端
  • 会出现问题
  • 它不使用任何i18n库,它是轻量级的,但不能让您访问多元化和插值等便捷的翻译功能

这就是我们在Redux和AirBNB redux-polyglot之上构建Polyglot的原因。 (我是其中一位作者)

它提供:

  • 用于在Redux商店中存储语言和相应消息的reducer。您可以通过以下两种方式提供:
    • 一个中间件,您可以配置该中间件以捕获特定操作,扣除当前语言并获取/获取相关消息。
    • 直接发送setLanguage(lang, messages)
  • 一个getP(state)选择器,用于检索公开4种方法的P对象:
    • 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);

如果您有任何问题/建议,请告诉我!

答案 3 :(得分:2)

根据我对此的研究,似乎有两种主要方法用于i18n在JavaScript中,ICUgettext

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

让我感到惊讶的是,支持是多么糟糕。我来自PHP世界,CakePHP或WordPress。在这两种情况下,它都是一个基本标准,所有字符串都只是被__('')包围,然后您可以更轻松地使用PO文件进行翻译。

的gettext

您熟悉sprintf格式化字符串,PO文件将被数千个不同的代理商轻松翻译。

有两个流行的选择:

  1. i18next,使用此arkency.com blog post
  2. 描述的用法
  3. Jed,其使用情况由sentry.io post和此React+Redux post描述,
  4. 两者都有gettext样式支持,字符串的sprintf样式格式以及导入/导出到PO文件。

    i18next自己开发了React extension。杰德没有。 Sentry.io似乎使用Jed与React的自定义集成。 React+Redux post,建议使用

      

    工具:jed + po2json + jsxgettext

    然而,Jed看起来更像是一个以gettext为重点的实现 - 这就是它表达的意图,因为i18next只是将它作为选项。

    ICU

    这有更多支持翻译的边缘情况,例如:处理性别问题。如果你有更复杂的语言要翻译,我想你会看到这样做的好处。

    一个受欢迎的选项是messageformat.js。在sentry.io blog tutorial中简要讨论。 messageformat.js实际上是由写Jed的同一个人开发的。 He makes quite stong claims for using ICU

      

    杰德在我看来是完整的功能。我很乐意修复错误,但通常对添加更多内容不感兴趣。

         

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

    粗略比较

    使用sprintf的gettext:

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

答案 4 :(得分:1)

如果还没看过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
  • CRA
  • expo.js
  • next.js
  • 故事书整合
  • 拉扎勒
  • DAT
  • ...

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

除此之外,您还应该考虑开发过程中的工作流程以及稍后为翻译人员考虑的工作流程 - &gt; https://www.youtube.com/watch?v=9NOzJhgmyQE

答案 5 :(得分:1)

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

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

Web服务器将自动提供正确的语言,具体取决于 Accept-Language 标头,或通过设置 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文件的更多单片方法:

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

答案 6 :(得分:0)

还有另一个(轻型)提案,该提案以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;

很抱歉,该帖子的扩展名是我,但我尝试显示完整的设置以澄清所有疑问。完成此操作后,可以在任何地方添加语言和使用描述,非常快捷,灵活。