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

Architecture React/Redux和多语言(国际化)应用程序-体系结构,architecture,internationalization,reactjs,translation,redux,Architecture,Internationalization,Reactjs,Translation,Redux,我正在构建一个应用程序,它需要有多种语言和地区 我的问题不是纯粹的技术问题,而是关于架构,以及人们在生产中实际使用的模式来解决这个问题。 我在任何地方都找不到这方面的“食谱”,所以我转向我最喜欢的问答网站:) 以下是我的要求(它们实际上是“标准”): 用户可以选择语言(普通) 更改语言后,界面应自动转换为新的选定语言 我不太担心格式化数字、日期等。目前,我想要一个简单的解决方案,只翻译字符串 以下是我可能想到的解决方案: 每个组件单独处理翻译 这意味着每个组件旁边都有一组en.json、fr

我正在构建一个应用程序,它需要有多种语言和地区

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

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

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

每个组件单独处理翻译

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

  • 赞成:更尊重React理念,每个组件都是“独立的”
  • 缺点:您不能将所有翻译集中在一个文件中(例如,让其他人添加一种新语言)
  • 缺点:你仍然需要将当前语言作为道具传递给每一个血腥的组件和他们的孩子
每个组件通过道具接收翻译

所以他们不知道当前的语言,他们只是把一系列字符串作为道具,这些字符串恰好与当前的语言相匹配

  • 赞成:因为这些字符串来自“顶部”,所以它们可以集中在某个地方
  • 缺点:每个组件现在都绑定到翻译系统中,您不能只重复使用一个,每次都需要指定正确的字符串
您略过了一些道具,可能会使用thingy来传递当前语言

  • 赞成:它基本上是透明的,不需要一直通过道具传递当前的语言和/或翻译
  • 缺点:使用起来很麻烦
如果你有任何其他想法,请说


您是如何做到的?

在尝试了很多解决方案后,我想我找到了一个效果很好的解决方案,应该是React 0.14的惯用解决方案(即,它不使用混合,但使用高阶组件)(编辑:当然,React 15也很好!)

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

组件

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

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

import{defaultasreact,PropTypes}来自'React';
从“./translate”导入翻译;
类MyComponent扩展了React.Component{
render(){
返回(
{this.props.strings.someTranslatedText}
);
}
}
MyComponent.propTypes={
字符串:PropTypes.object
};
MyComponent.defaultProps={
字符串:{
someTranslatedText:“你好,世界”
}
};
导出默认翻译('MyComponent')(MyComponent);
高阶分量

在前面的代码段中,您可能在最后一行注意到:
translate('MyComponent')(MyComponent)

translate
在本例中,是一个更高阶的组件,它封装了您的组件,并提供了一些额外的功能(此结构取代了以前版本的React的混合)

第一个参数是一个键,用于查找翻译文件中的翻译(我在这里使用了组件的名称,但可以是任何名称)。第二个(请注意,函数是curryd的,以允许ES7 decorators)是要包装的组件本身

以下是translate组件的代码:

从'React'导入{default as React};
从“../i18n/en”导入en;
从“../i18n/fr”导入fr;
常量语言={
EN
fr
};
导出默认函数转换(键){
返回组件=>{
类TranslationComponent扩展了React.Component{
render(){
console.log('current language:',this.context.currentLanguage);
var strings=languages[this.context.currentLanguage][key];
返回;
}
}
TranslationComponent.contextTypes={
currentLanguage:React.PropTypes.string
};
返回平移组件;
};
}
这并不是什么神奇的事情:它只是从上下文中读取当前语言(并且上下文不会流到整个代码库,只是在这个包装器中使用),然后从加载的文件中获取相关的strings对象。在这个例子中,这条逻辑相当幼稚,可以按照你真正想要的方式来做

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

位于层次结构的最顶端

在根组件上,只需从当前状态设置当前语言。下面的示例使用Redux作为类似通量的实现,但是可以使用任何其他框架/模式/库轻松地转换它

import{defaultasreact,PropTypes}来自'React';
从“../components/Menu”导入菜单;
从'react redux'导入{connect};
从“../state/lang”导入{changeLanguage};
类应用程序扩展了React.Component{
render(){
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'] }
);
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' });
{
  "language":"ar",
  "availableLanguages":[
    {"code":"en","name": "English"},
    {"code":"ar","name":"عربي"}
  ],
  "catalog":[
     "Hello":"مرحباً",
     "Thank You":"شكراً",
     "You have {count} new messages":"لديك {count} رسائل جديدة"
   ],
  "timezone":"",
  "currency":"",
  "direction":"rtl",
}
import template from "string-template";
const count = 7;
//....
template(i18n.catalog["You have {count} new messages"],{count}) // لديك ٧ رسائل جديدة
import humanFormat from "human-format";
//...
humanFormat(1337); // => '1.34 k'
// you can pass your own translated scale, e.g: humanFormat(1337,MyScale)
import moment from "moment";

const umoment = moment().locale(i18n.language);
umoment.format('MMMM Do YYYY, h:mm:ss a'); // أيار مايو ٢ ٢٠١٧، ٥:١٩:٥٥ م
<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>
"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",
{"hello": "Привет"}
export default require(`../lang/${process.env.REACT_APP_LANGUAGE}.json`);
import lang from '../lib/lang.js';
console.log(lang.hello);
export type Language = 'EN' | 'CA';
export const SET_LANGUAGE = 'SET_LANGUAGE';
import * as actionTypes from './actionTypes';
import { Language } from '../../shared/Types';

export const setLanguage = (language: Language) => ({
   type: actionTypes.SET_LANGUAGE,
   language: language,
});
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;
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;
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;
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;
{
    "message": "Welcome"
}
{
    "message": "Benvinguts"
}
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;