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


119

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

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

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

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

这是我可以考虑的可能解决方案:

每个组件都独立处理翻译

这意味着每个组件在其旁边都有例如一组en.json,fr.json等文件以及转换后的字符串。还有一个帮助程序功能,可帮助您根据所选语言从这些值中读取值。

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

每个组成部分都通过道具接收翻译

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

  • 优点:由于这些字符串是“自上而下”的,因此可以将它们集中在某个位置
  • 缺点:现在每个组件都已绑定到翻译系统中,您不能只是重复使用一个组件,而是每次都需要指定正确的字符串

您绕过道具了一下,可能是使用情境啄向下传递当前语言

  • 优点:几乎是透明的,不必始终通过道具传递当前语言和/或翻译
  • 缺点:使用起来很麻烦

如果您还有其他想法,请说!

你怎么做呢?


2
我更喜欢将带有翻译字符串的键对象作为prop传递的想法,而不必将每个字符串分别作为prop传递。在最高级别更改此设置将触发重新渲染。我认为使用上下文不是一个好主意,每个有权访问翻译文件的组件都会使它们“笨拙”且实际上可移植为imo(并且更难使应用程序在语言更改时重新呈现)。
多米尼克

1
实际上,根据facebook.github.io/react/docs/context.html,使用上下文共享当前语言是合法的用例之一。我现在尝试的方法是使用此方法加上一个高阶组件来处理为该特定组件提取字符串的逻辑(可能基于某个键)
Antoine Jaussoin 2015年

1
也许您也可以看看Instant。他们以完全不同的方式处理此问题,方法是在前端ala优化地解决该问题(也就是在加载时更改DOM)。
马塞尔·潘斯

1
一点也不差!这确实是完全不同的野兽(它将您与网站成长时可能需要付费的服务联系在一起),但是我喜欢这个主意,对于一个小型网站,您需要快速运行就值得了!
Antoine Jaussoin

4
另外,您可能想提及自己是Instant的共同创始人,而不是说“他们”就好像您与他们没有任何关系:)
Antoine Jaussoin

Answers:


110

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

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

组件

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

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

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早期版本的mixins)。

第一个参数是一个密钥,将用于在翻译文件中查找翻译(我在这里使用了组件的名称,但是可以是任何东西)。第二个(注意,该函数已进行咖喱化,以允许使用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;
    };
}

这不是魔术:它只会从上下文中读取当前语言(并且该上下文不会在整个代码库中泛滥,只是在此包装器中使用),然后从加载的文件中获取相关的字符串对象。在此示例中,此逻辑很幼稚,可以按照您真正想要的方式完成。

重要的一点是,在提供了键的情况下,它将上下文中的当前语言转换为字符串。

在层次结构的最顶层

在根组件上,您只需要从当前状态设置当前语言即可。以下示例将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按自己的意愿提供的人重用。

[Edit:31/03/2016]:我最近在一个Reactspective Board(用于敏捷回顾展)上工作,该会议由React&Redux构建,并且是多语言的。由于很多人在评论中要求提供真实的示例,因此它是:

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


这是一个很酷的解决方案。.想知道几个月后您是否仍然对此感兴趣?在此在线模式的建议方面,我还没有找到太多建议
Damon

2
实际上,我发现它可以完美工作(满足我的需求)。默认情况下,它使组件无需翻译即可工作,并且翻译就位于其之上,而组件却不知道它
Antoine Jaussoin 2016年

1
@ l.cetinsoy,您可以使用该dangerouslySetInnerHTML道具,只需注意其含义(手动清理输入内容)。参见facebook.github.io/react/tips/dangerously-set-inner-html.html
Teodor Sandu

6
为什么您没有尝试react-intl是否有原因?
SureshCS'8

1
真的很喜欢这种解决方案。我要补充一件事,我们发现它对于保持一致性和节省时间非常有用,那就是,如果您有许多包含通用字符串的组件,则可以利用变量并扩展到对象上,例如const formStrings = { cancel, create, required }; export default { fooForm: { ...formStrings, foo: 'foo' }, barForm: { ...formStrings, bar: 'bar' } }
Huw Davies

18

根据我的经验,出于多种原因,最好的方法是创建一个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-以人为格式的此模块将允许您将数字转换为人类可读的字符串,例如:

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


这种方法是否适用于两种以上的语言?考虑目录的设置
tempranova

下投。这不能回答问题。OP要求一个架构思想,而不是任何i18n库的建议或比较。
TrungDQ

9
我建议i18n目录为redux状态,看来您不了解redux
Fareed Alnamrouti

5

Antoine的解决方案效果很好,但有一些警告:

  • 它直接使用React上下文,当我已经使用Redux时,我倾向于避免使用它
  • 它直接从文件中导入短语,如果要在运行时,客户端获取所需的语言,可能会出现问题。
  • 它不使用任何轻量级的i18n库,但无法访问方便的翻译功能,例如复数和插值

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

它提供 :

  • 用来在Redux商店中存储语言和相应消息的reducer。您可以通过以下两种方式提供:
    • 您可以将其配置为捕获特定操作,扣除当前语言并获取/获取关联消息的中间件。
    • 直接派遣 setLanguage(lang, messages)
  • getP(state)选择器,该选择器检索P公开4种方法的对象:
    • t(key):原始的多语言T函数
    • tc(key):大写翻译
    • tu(key):大写翻译
    • tm(morphism)(key):自定义变形翻译
  • 一个getLocale(state)选择器来获得当前的语言
  • 一个translate高阶组件,以提高你的反应的组分通过注入p道具对象

简单用法示例:

派出新语言:

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

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


1
更好的原始短语要翻译。并制作一个分析所有组件_()功能的工具,例如获取所有这些字符串。因此,您可以在语言文件中更轻松地进行翻译,而不会陷入疯狂的变量。在某些情况下,登录页面需要布局的特定部分以不同的方式显示。因此,还应该提供一些如何选择默认值和其他可能选项的智能功能。
罗曼·科斯

@Jalil,您好,哪里有中间件的完整示例?
ArkadyB

@ArkadyB,您好:我们在一些非开源项目中将其用于生产。您可以在模块README上找到更多信息: npmjs.com/package/redux-polyglot 您对使用它有任何疑问/困难吗?
加里尔

我和polyglot.js的主要问题是,它完全是在重新发明轮子,而不是在PO文件的基础上构建。这个替代库看起来很有前途npmjs.com/package/redux-i18n。我认为这样做并没有太大不同-它只是提供了一个额外的层来与PO文件进行相互转换。
icc97 '17

2

从我对此的研究来看,似乎有两种在JavaScript中使用i18n的主要方法,即ICUgettext

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

令我惊讶的是这种支持多么可怜。我来自PHP世界,无论是CakePHP还是WordPress。在这两种情况下,这都是一个基本标准,所有的字符串都简单地用括起来__(''),然后再进一步轻松地使用PO文件进行翻译。

文字

您对sprintf格式字符串很熟悉,PO文件将被成千上万个不同的机构轻松转换。

有两种流行的选择:

  1. i18next,其用法由arkency.com博客文章描述
  2. Jed,使用了sendry.io和本React + Redux帖子中描述的用法,

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

i18next具有自己开发的React扩展。杰德没有。Sentry.io似乎使用了Jed与React的自定义集成。该阵营+终极版后,建议使用

工具:jed + po2json + jsxgettext

但是,Jed似乎更着重于gettext的实现-这是它的明确意图,因为i18next只是将其作为选项。

重症监护病房

这为围绕翻译的边缘案例提供了更多支持,例如,处理性别问题。我认为,如果您要翻译更复杂的语言,将会从中受益。

一个流行的选项是messageformat.js。在此sentry.io博客教程中进行了简要讨论。messageformat.js实际上是由编写Jed的同一人开发的。他对使用ICU提出了非常严格的要求

我认为Jed功能齐全。我很乐意修复错误,但通常不希望向库中添加更多内容。

我还维护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(我从阅读指南中得出的最佳猜测):

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

下投。这不能回答问题。OP要求一个架构思想,而不是任何i18n库的建议或比较。
TrungDQ

@TrungDQ这就是OP所问的问题“我的问题不只是技术上的问题,而是有关人们在生产中实际使用的架构和模式来解决此问题的方法。” 。这是生产中使用的两种模式。
icc97

在我看来,此答案未提供我(和其他人)正在寻找的信息。您提供的信息很有帮助,但可能还有其他问题。我只想贡献我的一票,使正确的答案弹出顶部(我希望如此)。
TrungDQ

@TrungDQ如果它不是你要找的内容,那么就给予好评你没有使用一个而忽略其他,而不是downvoting没有问题的特定部分匹配pefectly有效回答您感兴趣的问题。
icc97

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>

随附以下样品:

  • 网络包
  • cra
  • expo.js
  • next.js
  • 故事书整合
  • azz
  • t
  • ...

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

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


这不能回答问题。OP要求一个架构思想,而不是任何i18n库的建议或比较。
TrungDQ

@TrungDQ以及您对我投票否决的答案的评论-OP要求生产中使用的当前解决方案。但是,在2月的回答中就建议了i18next
icc97

0

我想提出一个使用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文件的更整体的方法:

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

它不是仅在编译时有效吗?用户无法即时更改语言吗?那将是一个不同的用例。
Antoine Jaussoin '18年

该应用程序将针对所需的每种语言进行编译。Web服务器将自动提供正确的版本,具体取决于“接受语言”标头,或者由用户即时设置的Cookie。这样,可以将整个翻译逻辑移出应用程序。
伊戈尔·
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.