React / Redux 和多语言(国际化)应用程序 - 架构
React / Redux and Multilingual (Internationalization) Apps - Architecture
我正在构建一个需要以多种语言和区域设置提供的应用程序。
我的问题不是纯粹的技术问题,而是关于架构,以及人们在生产中实际使用的模式来解决这个问题。
我在任何地方都找不到 "cookbook",所以我转向我最喜欢的 Q/A 网站 :)
以下是我的要求(真的是"standard"):
- 用户可以选择语言(琐碎)
- 更改语言后,界面应自动翻译为新选择的语言
- 目前我不太担心格式化数字、日期等,我想要一个简单的解决方案来翻译字符串
以下是我能想到的可能的解决方案:
每个组件单独处理翻译
这意味着每个组件都有例如一组 en.json、fr.json 等文件以及已翻译的字符串。以及一个辅助函数,可帮助根据所选语言从那些值中读取值。
- 亲:比较尊重React哲学,每个组件都是"standalone"
- 缺点:您无法将所有翻译集中在一个文件中(例如让其他人添加新语言)
- 缺点:您仍然需要在每个血腥组件及其子组件中将当前语言作为 prop 传递
每个组件通过 props 接收翻译
所以他们不知道当前的语言,他们只是将字符串列表作为恰好与当前语言匹配的 props
- Pro: 由于这些字符串即将到来 "from the top",它们可以集中在某个地方
- 缺点:现在每个组件都绑定到翻译系统中,您不能只重复使用一个,每次都需要指定正确的字符串
你稍微绕过道具,可能会使用 context 东西来传递当前的语言
- Pro:它大部分是透明的,不必一直通过道具传递当前语言and/or翻译
- 缺点:使用起来比较麻烦
如果您有任何其他想法,请尽管说!
你是怎么做到的?
在尝试了很多解决方案之后,我认为我找到了一个效果很好的解决方案,它应该是 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 文件将被成千上万的不同机构轻松翻译。
有两个常用选项:
- i18next, with usage described by this arkency.com blog post
- 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 扩展,但我试图展示完整的设置以澄清所有疑问。完成此操作后,可以非常快速和灵活地在任何地方添加语言和使用描述。
我正在构建一个需要以多种语言和区域设置提供的应用程序。
我的问题不是纯粹的技术问题,而是关于架构,以及人们在生产中实际使用的模式来解决这个问题。 我在任何地方都找不到 "cookbook",所以我转向我最喜欢的 Q/A 网站 :)
以下是我的要求(真的是"standard"):
- 用户可以选择语言(琐碎)
- 更改语言后,界面应自动翻译为新选择的语言
- 目前我不太担心格式化数字、日期等,我想要一个简单的解决方案来翻译字符串
以下是我能想到的可能的解决方案:
每个组件单独处理翻译
这意味着每个组件都有例如一组 en.json、fr.json 等文件以及已翻译的字符串。以及一个辅助函数,可帮助根据所选语言从那些值中读取值。
- 亲:比较尊重React哲学,每个组件都是"standalone"
- 缺点:您无法将所有翻译集中在一个文件中(例如让其他人添加新语言)
- 缺点:您仍然需要在每个血腥组件及其子组件中将当前语言作为 prop 传递
每个组件通过 props 接收翻译
所以他们不知道当前的语言,他们只是将字符串列表作为恰好与当前语言匹配的 props
- Pro: 由于这些字符串即将到来 "from the top",它们可以集中在某个地方
- 缺点:现在每个组件都绑定到翻译系统中,您不能只重复使用一个,每次都需要指定正确的字符串
你稍微绕过道具,可能会使用 context 东西来传递当前的语言
- Pro:它大部分是透明的,不必一直通过道具传递当前语言and/or翻译
- 缺点:使用起来比较麻烦
如果您有任何其他想法,请尽管说!
你是怎么做到的?
在尝试了很多解决方案之后,我认为我找到了一个效果很好的解决方案,它应该是 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 文件将被成千上万的不同机构轻松翻译。
有两个常用选项:
- i18next, with usage described by this arkency.com blog post
- 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 扩展,但我试图展示完整的设置以澄清所有疑问。完成此操作后,可以非常快速和灵活地在任何地方添加语言和使用描述。