React / Redux和多语言(国际化)应用程序 – 架构
我正在构build一个需要以多种语言和语言环境提供的应用程序。
我的问题不是单纯的技术性问题,而是纯粹的技术问题,而不是纯粹的技术问题,而是人们在生产中用来解决这个问题的模式。 我找不到任何“食谱”,所以我转向我最喜欢的Q / A网站:)
这是我的要求(他们真的是“标准”):
- 用户可以select语言(微不足道)
- 在改变语言时,界面应该自动翻译成新的选定语言
- 我现在不太担心格式化数字,date等,我想要一个简单的解决scheme来转换string
以下是我可以想到的可能解决scheme:
每个组件单独处理翻译
这意味着每个组件都有一个en.json,fr.json等文件和翻译后的string。 还有一个帮助函数,帮助读取取决于所选语言的值。
- Pro:更加尊重React哲学,每个组件都是“独立的”
- 缺点:你不能集中在一个文件中的所有翻译(例如让别人添加一个新的语言)
- 缺点:你仍然需要通过当前的语言作为道具,在每一个血腥的组件和他们的孩子
每个组件通过道具接收翻译
所以他们不知道当前的语言,他们只是把一个string列表当作正确匹配当前语言的道具
- Pro:由于这些string是“从顶部”来的,所以它们可以集中在某个地方
- 缺点:每个组件现在都绑定到翻译系统中,不能只重用一个,每次都需要指定正确的string
你绕过道具一点,可能使用上下文 thingy传递当前的语言
- Pro:大多数情况下是透明的,不必一直通过道具传递当前的语言和/或翻译
- 缺点:使用起来很麻烦
如果您有任何其他想法,请说!
你怎么做呢?
在尝试了一些解决scheme之后,我想我find了一个很好的解决scheme,应该是React 0.14的一个惯用的解决scheme(也就是说,它不使用mixins,而是高阶组件)。 )。
所以这里的解决scheme,从底部开始(单个组件):
组件
你的组件唯一需要的(按照惯例)是一个strings
道具。 它应该是一个包含你的组件需要的各种string的对象,但是它的形状真的取决于你。
它包含了默认的翻译,所以你可以在别的地方使用这个组件,而不需要提供任何翻译(在这个例子中,它可以使用默认的语言,英文)
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
是一个高级组件,它包装了你的组件,并提供了一些额外的function(这种构造取代了以前版本的React混合)。
第一个参数是一个将用于在翻译文件中查找翻译的键(我在这里使用了组件的名称,但它可以是任何东西)。 第二个(注意,该函数是curry,允许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; }; }
这不是魔术:它只会从上下文中读取当前的语言(并且上下文不会遍布整个代码库,只是在这个包装器中使用),然后从加载的文件中获取相关的string对象。 这个逻辑在这个例子中是相当天真的,可以以你想要的方式完成。
重要的一点是,它从上下文中获取当前的语言并将其转换为string,只要提供了密钥。
在层次结构的最顶端
在根组件上,您只需要从当前状态设置当前语言。 以下示例使用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' } };
你们有什么感想?
我认为是解决了所有的问题,我试图避免在我的问题:翻译逻辑不stream血的源代码,它是相当孤立的,并允许重新使用组件没有它。
例如,MyComponent不需要被translate()包装,并且可以是单独的,允许任何人按照自己的意思提供strings
。
[编辑:31/03/2016]:我最近在回溯董事会(敏捷回顾)工作,与React&Reduxbuild成,是多语言。 由于相当多的人在评论中要求一个真实的例子,这里是:
你可以在这里find代码: https : //github.com/antoinejaussoin/retro-board/tree/master
根据我的经验,最好的方法是创build一个i18n的redux状态并使用它,原因很多:
1-这将允许您从数据库,本地文件或甚至从模板引擎(如ejs或玉)传递初始值
2-当用户更改语言时,可以更改整个应用程序语言,而无需刷新UI。
3-当用户改变语言,这也将允许你从API,本地文件,甚至从常量检索新的语言
4-您还可以保存其他重要的东西,如时区,货币,方向(RTL / LTR)和可用语言列表
5-您可以将更改语言定义为正常的重复操作
6-你可以在一个地方有你的后端和前端string,例如在我的情况下,我使用i18n节点进行本地化,当用户改变ui语言,我只是做一个正常的API调用,在后端我只是返回i18n.getCatalog(req)
这将只返回当前语言的所有用户string
我对国际初始状态的build议是:
{ "language":"ar", "availableLanguages":[ {"code":"en","name": "English"}, {"code":"ar","name":"عربي"} ], "catalog":[ "Hello":"مرحباً", "Thank You":"شكراً", "You have {count} new messages":"لديك {count} رسائل جديدة" ], "timezone":"", "currency":"", "direction":"rtl", }
国际化的额外有用的模块:
1- string模板这将允许您在您的目录string之间插入值,例如:
import template from "string-template"; const count = 7; //.... template(i18n.catalog["You have {count} new messages"],{count}) // لديك ٧ رسائل جديدة
2- 人格式这个模块可以让你把一个数字转换成一个可读的string,例如:
import humanFormat from "human-format"; //... humanFormat(1337); // => '1.34 k' // you can pass your own translated scale, eg: humanFormat(1337,MyScale)
3- 时刻是最着名的date和时间npm库,你可以翻译的时刻,但它已经有一个内置的翻译,只需要通过当前状态语言,例如:
import moment from "moment"; const umoment = moment().locale(i18n.language); umoment.format('MMMM Do YYYY, h:mm:ss a'); // أيار مايو ٢ ٢٠١٧، ٥:١٩:٥٥ م
安托万的解决scheme工作正常,但有一些注意事项:
- 它直接使用React上下文,当我使用Redux的时候,我倾向于避免这种情况
- 它直接从文件中导入短语,如果您想在运行时(客户端)获取所需的语言,则可能会产生问题
- 它不使用任何i18n库,这是轻量级的,但不能让您访问像复数化和插值等方便的翻译function
这就是为什么我们在Redux和AirBNB的Polyglot之上构build了多语言多语言的原因。
(我是其中一位作者)
它提供 :
- 还原器在您的Redux商店中存储语言和相应的消息。 您可以通过以下任一方式提供:
- 一个中间件,你可以configuration捕捉特定的行动,扣除当前的语言和获取/获取相关的消息。
- 直接调度
setLanguage(lang, messages)
- 一个
getP(state)
select器,检索一个P
对象,公开了4个方法:-
t(key)
:原来的polyglot Tfunction -
tc(key)
:大写翻译 -
tu(key)
:大写翻译 -
tm(morphism)(key)
:自定义变形翻译
-
- 一个
getLocale(state)
select器来获取当前语言 - 通过在道具中注入
p
对象来translate
更高阶的组件来增强您的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.pt('common.hello_world')} </div> ); MyComponent.propTypes = { p: PropTypes.shape({t: PropTypes.func.isRequired}).isRequired, } export default translate(MyComponent);
请告诉我,如果您有任何问题/build议!
从我的研究来看,在JavaScript, ICU和gettext中似乎有两种主要的方法用于国际化。
我只使用gettext,所以我有点偏见。
令我惊奇的是,这种支持有多差。 我来自PHP世界,无论是CakePHP或WordPress。 在这两种情况下,这是一个基本的标准,所有的string都被__('')
包围,然后再使用PO文件非常容易地进行翻译。
gettext的
您可以熟悉sprintf格式化string,并且PO文件将被成千上万的不同机构轻松转换。
有两个受欢迎的选项:
- i18next ,用arkency.com博客文章描述的用法
- 杰德 ,用sentry.io文章描述的用法和这个React + Redux的post ,
两者都支持gettext风格,sprintf风格的string格式和导入/导出到PO文件。
i18next有自己开发的React扩展 。 杰德不。 Sentry.io似乎使用Jed和React的自定义集成。 React + Redux后 ,build议使用
工具:jed + po2json + jsxgettext
然而,Jed似乎更像是一个专注于gettext的实现 – 也就是它expression的意图,其中i18next只是它的一个选项。
ICU
这对翻译的边缘情况有更多的支持,例如处理性别问题。 如果你有更复杂的语言可以翻译成英文,我想你会看到这个好处。
一个stream行的select是messageformat.js 。 在这个sentry.io博客教程中简要讨论。 messageformat.js实际上是由写Jed的同一个人开发的。 他对使用ICU提出了相当强烈的要求 :
杰德在我看来是function完备。 我很乐意修复错误,但是通常我不感兴趣向库中添加更多内容。
我也维护messageformat.js。 如果你不特别需要一个gettext实现,我可能会build议使用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' });
如果还没有看到https://react.i18next.com/可能是一个很好的build议。; 它基于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
除此之外,您还应该在开发期间考虑工作stream程,稍后再考虑翻译人员 – > https://www.youtube.com/watch?v=9NOzJhgmyQE