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

IT技术 architecture internationalization reactjs translation redux
2021-03-30 10:28:01

我正在构建一个应用程序,该应用程序需要支持多种语言和区域设置。

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

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

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

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

每个组件都单独处理翻译

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

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

每个组件通过 props 接收翻译

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

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

您稍微绕过props并可能使用上下文来传递当前语言

  • 优点:它主要是透明的,不必一直通过props传递当前语言和/或翻译
  • 缺点:使用起来比较麻烦

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

你怎么做呢?

6个回答

在尝试了很多解决方案之后,我想我找到了一个运行良好的解决方案,应该是 React 0.14 的惯用解决方案(即它不使用 mixins,而是使用高阶组件)(编辑:当然,React 15 也完全没问题! )。

所以这里的解决方案,从底部(单个组件)开始:

组件

你的组件唯一需要的东西(按照惯例)是stringsprops。它应该是一个包含您的组件需要的各种字符串的对象,但它的形状实际上取决于您。

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

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

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

重要的是,它从上下文中获取当前语言并将其转换为字符串,给定提供的键。

在层次结构的最顶端

在根组件上,您只需要从当前状态设置当前语言。以下示例使用 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 和 Redux 构建,并且是多语言的。由于很多人在评论中要求提供一个真实的例子,这里是:

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

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

根据我的经验,最好的方法是创建一个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 的额外有用module:

1- string-template这将允许您在目录字符串之间注入值,例如:

import template from "string-template";
const count = 7;
//....
template(i18n.catalog["You have {count} new messages"],{count}) // لديك ٧ رسائل جديدة

2-人类格式此module将允许您将数字转换为人类可读的字符串,例如:

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

这种方法是否也适用于两种以上的语言?考虑目录的设置
2021-05-24 10:28:01
投了反对票。这并没有回答这个问题。OP 要求提供架构想法,而不是建议或比较任何 i18n 库。
2021-06-05 10:28:01
我建议 i18n 目录为 redux 状态,看来你不了解 redux
2021-06-11 10:28:01

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在 props 中注入对象来增强你的 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);

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

要翻译的更好的原始短语。并制作一个工具来解析_()函数的所有组件,例如获取所有这些字符串。因此,您可以在语言文件中更轻松地翻译它,并且不要乱用疯狂的变量。在某些情况下,着陆页需要布局的特定部分以不同的方式显示。因此,也应该提供一些关于如何选择默认值与其他可能选择的智能功能。
2021-05-25 10:28:01
我对这个和 polyglot.js 的主要问题是它完全是在重新发明轮子,而不是建立在 PO 文件之上。这个替代库看起来很有前途npmjs.com/package/redux-i18n我不认为这有什么不同——它只是提供了一个额外的层来转换和转换 PO 文件。
2021-05-29 10:28:01
嗨@ArkadyB,我们在几个非开源项目的生产中使用它。您可以在module自述文件中找到更多信息: npmjs.com/package/redux-polyglot 您是否有任何问题/使用它的困难?
2021-06-01 10:28:01
嗨@Jalil,有没有完整的中间件示例?
2021-06-02 10:28:01

另一个(轻量级)提案在 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
2021-05-28 10:28:01
@AntoineJaussoin 您是否尝试过这种方法,如果是,您遇到了什么陷阱?仅仅是为了让你的组件更可重用而不是依赖 redux 吗?
2021-06-10 10:28:01

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

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

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

获取文本

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

有两种流行的选择:

  1. i18next,此arkency.com 博客文章描述了用法
  2. Jed,其用法由sentry.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 获取文本:

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' });
在我看来,这个答案没有提供我(和其他人)正在寻找的信息。您提供的信息很有帮助,但也许是另一个问题。我只是想贡献我的反对票,让正确的答案出现在顶部(我希望如此)。
2021-05-26 10:28:01
投了反对票。这并没有回答这个问题。OP 要求提供架构想法,而不是建议或比较任何 i18n 库。
2021-05-31 10:28:01
@TrungDQ 如果这不是您要查找的内容,那么只需对您使用过的答案投赞成票并忽略其他答案,而不是对与您感兴趣的问题的特定部分不匹配的完全有效的答案投反对票。
2021-06-01 10:28:01
@TrungDQ 这就是 OP 所问的:“我的问题不是纯粹的技术问题,而是关于架构,以及人们在生产中实际使用的模式来解决这个问题。” . 这是生产中使用的两种模式。
2021-06-17 10:28:01