【问题标题】:React / Redux and Multilingual (Internationalization) Apps - ArchitectureReact / Redux 和多语言(国际化)应用程序 - 架构
【发布时间】:2016-01-29 13:46:36
【问题描述】:

我正在构建一个需要支持多种语言和地区的应用。

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

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

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

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

每个组件单独处理翻译

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

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

每个组件通过 props 接收翻译

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

  • 专业人士:由于这些字符串“来自顶部”,它们可以集中在某个地方
  • 缺点:每个组件现在都绑定到翻译系统中,您不能只重复使用一个,每次都需要指定正确的字符串

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

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

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

你是怎么做到的?

【问题讨论】:

  • 我更喜欢将带有翻译字符串的键对象作为道具传递下来的想法,您不必单独将每个字符串作为道具传递。在顶层更改它应该会触发重新渲染。我不认为为此使用上下文是一个好主意,并且每个可以访问翻译文件的组件都使它们变得不那么“愚蠢”并且实际上是 imo 便携(并且更难让应用程序在语言更改时重新呈现)。
  • 实际上根据facebook.github.io/react/docs/context.html,使用上下文来共享当前语言是合法的用例之一。我现在尝试的方法是使用这个加上一个高阶组件来处理为该特定组件提取字符串的逻辑(可能基于某个键)
  • 也许你也可以看看Instant。他们通过在前端ala Optimizely(也就是在加载时更改DOM)中解决这个问题,以完全不同的方式处理这个问题。
  • 一点也不差!它确实是一个完全不同的野兽(它将您与您可能需要支付的服务联系起来,如果您的网站增长,您可能需要付费),但我喜欢这个想法,对于一个需要快速运行的小型网站来说,这确实是值得的!
  • 另外,您可能想提及您是 Instant 的联合创始人,而不是说“他们”,好像您与他们没有任何关系:)

标签: architecture internationalization reactjs translation redux


【解决方案1】:

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

高阶组件

在之前的 sn-p 中,您可能已经在最后一行注意到了这一点: translate('MyComponent')(MyComponent)

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

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

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

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

【讨论】:

  • 这是一个很酷的解决方案.. 想知道几个月后你是否还在使用这个?我没有找到太多关于此在线模式的建议方式
  • 实际上,我发现它可以完美地工作(满足我的需要)。默认情况下它使组件在没有翻译的情况下工作,翻译只是在它之上而不需要组件意识到它
  • @l.cetinsoy 您可以使用 dangerouslySetInnerHTML 属性,但请注意其含义(手动清理输入)。见facebook.github.io/react/tips/dangerously-set-inner-html.html
  • 你没有尝试过 react-intl 有什么原因吗?
  • 真的很喜欢这个解决方案。我要补充的一件事是我们发现对于一致性和节省时间非常有用的是,如果您有很多具有公共字符串的组件,您可以利用变量并传播对象,例如const formStrings = { cancel, create, required }; export default { fooForm: { ...formStrings, foo: 'foo' }, barForm: { ...formStrings, bar: 'bar' } }
【解决方案2】:

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

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'); // أيار مايو ٢ ٢٠١٧، ٥:١٩:٥٥ م

更新(2019 年 14 月 6 日)

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

【讨论】:

  • 这种方法是否也适用于两种以上的语言?考虑目录的设置
  • 投了反对票。这没有回答问题。 OP 要求提供架构理念,而不是建议或比较任何 i18n 库。
  • 我建议i18n目录为redux状态,看来你不懂redux
【解决方案3】:

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

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

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

它提供:

  • reducer 用于在 Redux 存储中存储语言和相应的消息。您可以通过以下任一方式提供两者:
    • 一个中间件,您可以配置它来捕获特定操作、扣除当前语言和获取/获取相关消息。
    • 直接派发setLanguage(lang, messages)
  • 一个getP(state) 选择器,它检索一个公开4 个方法的P 对象:
    • t(key): 原创多语言 T 函数
    • tc(key):大写翻译
    • tu(key):大写翻译
    • tm(morphism)(key): 自定义变形翻译
  • getLocale(state)selector 获取当前语言
  • 一个 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);

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

【讨论】:

  • 要翻译很多更好的原始短语。并制作一个工具来解析_() 函数的所有组件,例如获取所有这些字符串。所以你可以在语言文件中更容易地翻译它,不要乱用疯狂的变量。在某些情况下,登陆页面需要布局的特定部分以不同方式显示。因此,还应该提供一些关于如何选择默认选项和其他可能选项的智能功能。
  • 嗨@Jalil,有没有中间件的完整示例?
  • 嗨@ArkadyB,我们在几个非开源项目的生产中使用它。您可以在模块 README 中找到更多信息:npmjs.com/package/redux-polyglot您对使用它有任何疑问/困难吗?
  • 我对这个和 polyglot.js 的主要问题是它完全是在重新发明轮子,而不是建立在 PO 文件之上。这个备用库看起来很有前途npmjs.com/package/redux-i18n。我认为这并没有太大的不同——它只是提供了一个额外的层来转换 PO 文件。
【解决方案4】:

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

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

【讨论】:

  • 为什么没有人评论这种方法?对于自己动手的方法,这似乎是这里最好的最简单的解决方案。喜欢它@Sergi Juanati
  • @AntoineJaussoin 您尝试过这种方法吗?如果尝试过,您遇到了什么陷阱?是否只是为了让你的组件更可重用而不依赖于 redux?
【解决方案5】:

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

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

让我惊讶的是支持是多么糟糕。我来自 PHP 世界,无论是 CakePHP 还是 WordPress。在这两种情况下,所有字符串都简单地用__('') 包围是一个基本标准,然后您可以很容易地使用 PO 文件获得翻译。

获取文本

您熟悉 sprintf 来格式化字符串,并且 PO 文件将被数千个不同的机构轻松翻译。

有两种流行的选择:

  1. i18next,用法由 arkency.com blog post 描述
  2. Jed,用法由sentry.io post 和此React+Redux post 描述,

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

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

工具:jed + po2json + jsxgettext

然而,Jed 似乎是一个更侧重于 gettext 的实现 - 那是它表达的意图,而 i18next 只是将它作为一个选项。

重症监护室

这对翻译的边缘情况有更多支持,例如用于处理性别。如果您有更复杂的语言需要翻译,我认为您会从中受益。

一个流行的选项是messageformat.js。在此sentry.io blog tutorial 中进行了简要讨论。 messageformat.js 实际上是由编写 Jed 的同一个人开发的。 He makes quite stong claims for using ICU:

在我看来,Jed 功能齐全。我很乐意修复错误,但通常对向库中添加更多内容不感兴趣。

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

粗略比较

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

【讨论】:

  • 投了反对票。这没有回答问题。 OP 要求提供架构理念,而不是建议或比较任何 i18n 库。
  • @TrungDQ 这就是 OP 提出的问题:“我的问题不是纯粹的技术问题,而是关于架构,以及人们在生产中实际使用的模式 来解决这个问题。”。这是生产中使用的两种模式。
  • 在我看来,这个答案没有提供我(和其他人)正在寻找的信息。您提供的信息很有帮助,但可能是另一个问题。我只是想投出我的反对票,让正确的答案出现在顶部(我希望如此)。
  • @TrungDQ 如果这不是您要查找的内容,那么只需对您使用过的那个投赞成票并忽略其他的,而不是对与您问题的特定部分不匹配的完全有效的答案投反对票'感兴趣。
【解决方案6】:

如果还没有完成,请查看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>

附带以下示例:

  • 网页包
  • 克拉
  • expo.js
  • next.js
  • 故事书集成
  • 眩晕
  • 数据
  • ...

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

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

【讨论】:

  • 这没有回答问题。 OP 要求提供架构理念,而不是建议或比较任何 i18n 库。
  • @TrungDQ 与您对我的回答的评论一样,您投了反对票 - OP 要求提供生产中使用的当前解决方案。但是,我从 2 月份开始在 my answer 中建议了 i18next。
【解决方案7】:

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

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

网络服务器将根据 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);

【讨论】:

  • 它不是只能在编译时工作吗?用户无法即时更改语言?那将是一个不同的用例。
  • 该应用程序将针对所需的每种语言进行编译。 Web 服务器将根据“Accept-Language”标头或用户即时设置的 cookie 自动提供正确的版本。通过这样做,可以将整个翻译逻辑移出应用程序。
猜你喜欢
  • 2020-09-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多