React / Redux и многоязычные (интернационализация) приложения-архитектура

Я создаю приложение, которое должно быть доступно на нескольких языках и языках.

мой вопрос не чисто технический, а скорее об архитектуре и шаблонах, которые люди фактически используют в производстве для решения этой проблемы. Я не мог найти нигде никакой "поваренной книги" для этого, поэтому я обращаюсь к своему любимому веб-сайту Q/A :)

вот мои требования (они действительно "стандартный"):

  • пользователь может выбрать язык (тривиально)
  • при изменении языка интерфейс должен автоматически переводиться на новый выбранный язык
  • Я не слишком беспокоюсь о форматирование чисел, дат и т. д. на данный момент, я хочу простое решение, чтобы просто перевести строки

вот возможные решения, которые я мог бы придумать:

каждый компонент имеет дело с переводом в изоляции

Это означает, что каждый компонент, к примеру набор ванной.json, fr.JSON и т. д. файлы рядом с ним с переведенными строками. И вспомогательная функция для чтения значений из тех, которые зависят от выбранного языка.

  • Pro: более уважительно к философии React, каждый компонент является "автономным"
  • минусы: вы не можете централизовать все переводы в файле (например, чтобы кто-то другой добавил новый язык)
  • минусы: вам все равно нужно передать текущий язык в качестве опоры, в каждом кровавом компонент и их дети

каждый компонент получает переводы через реквизит

таким образом, они не знают о текущем языке, они просто берут список строк в качестве реквизита, которые совпадают с текущим языком

  • Pro: поскольку эти строки идут "сверху", они могут быть централизованы где-то
  • минусы: каждый компонент теперь привязан к системе перевода, вы не можете просто повторно использовать его, вы нужно указывать правильные строки каждый раз

вы немного обходите реквизит и, возможно, используете контекст thingy, чтобы передать текущий язык

  • Pro: это в основном прозрачно, не нужно передавать текущий язык и/или переводы через реквизит все время
  • минусы: это выглядит громоздко использовать

Если у вас есть какие-либо другие идеи, пожалуйста, скажите!

Как вы это делаете это?

6 ответов


попробовав довольно много решений, я думаю, что нашел тот, который хорошо работает и должен быть идиоматическим решением для React 0.14 (т. е. он не использует миксины, но компоненты более высокого порядка) (редактировать: также прекрасно с 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).

первый аргумент-это ключ, который будет использоваться для поиска переводов в файле перевода (я использовал имя компонента, но это может быть что угодно). Второй (обратите внимание, что функция curryed, чтобы позволить декораторам ES7) является Сам компонент обертывания.

вот код для компонента translate:

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

это не магия: он просто прочитает текущий язык из контекста (и этот контекст не истекает кровью по всей базе кода, просто используется здесь, в этой оболочке), а затем получит соответствующий объект strings из загруженных файлов. Эта часть логики довольно наивна в этом примере, может быть сделано так, как вы действительно хотите.

важная часть заключается в том, что она принимает текущий язык из контекста и преобразует его в строки, учитывая предоставленный ключ.

на самом верху иерархии

в корневом компоненте вам просто нужно установить текущий язык из вашего текущего состояния. В следующем примере используется Redux в качестве реализации, подобной потоку, но ее можно легко преобразовать с помощью любой другой структуры / шаблона / библиотеки.

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 по их собственному значению.

[Edit: 31/03/2016]: недавно я работал на ретроспективной доске (для Agile Retrospectives), построенной с React & Redux и многоязычной. Поскольку довольно много людей попросили реальный пример в комментариях, вот он:

вы можете найти код здесь:https://github.com/antoinejaussoin/retro-board/tree/master


из моего опыта лучший подход-создать реализация i18n Redux на состояние и использовать его по многим причинам:

1-Это позволит вам передать начальное значение из базы данных, локального файла или даже из механизма шаблонов, таких как ejs или jade

2-Когда пользователь меняет язык, вы можете изменить весь язык приложения, даже не обновляя пользовательский интерфейс.

3 - Когда пользователь меняет язык это также позволит вам получить новый язык из api, локального файла или даже из констант

4-Вы также можете сохранить другие важные вещи со строками, такими как часовой пояс, валюта, направление (RTL/LTR) и список доступных языков

5 - Вы можете определить язык изменения как обычное действие redux

6 - Вы можете иметь свои бэкэнд и фронтальные строки в одном месте, например, в моем случае я использую как i18n-узле для локализации и при изменении пользователем языка пользовательского интерфейса i просто сделайте обычный вызов api, и в бэкэнде я просто верну i18n.getCatalog(req) это вернет все строки user-только для текущего языка

мое предложение для начального состояния 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- строку-шаблон это позволит вам вводить значения между строками каталога, например:

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

2- человека-формате этот модуль позволит вам преобразовать число в / из читаемой человеком строки, например:

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

решение Антуана работает нормально, но есть некоторые предостережения:

  • он использует контекст React напрямую, чего я обычно избегаю, когда уже использую Redux
  • он импортирует непосредственно фразы из файла, что может быть проблематично, если вы хотите получить необходимый язык во время выполнения, на стороне клиента
  • он не использует библиотеку i18n, которая является легкой, но не дает вам доступ к удобным функциям перевода, таким как плюрализация и интерполяция

вот почему мы построили redux-полиглот поверх обоих Redux и AirBNB в Полиглот.
(Я один из авторов)

обеспечивает :

  • редуктор для хранения языка и соответствующих сообщений в вашем магазине Redux. Вы можете поставить либо :
    • промежуточное ПО, которое можно настроить для улавливания определенных действий, вычитания текущего языка и получения/выборки сообщения.
    • прямая рассылка setLanguage(lang, messages)
  • a getP(state) селектор, который получает P объект, который предоставляет 4 способа :
    • t(key): оригинальный полиглот t функция
    • tc(key): оприходованы перевод
    • tu(key): перевод в верхнем регистре
    • tm(morphism)(key): таможня превратился перевод
  • a getLocale(state)селектор для получения текущего языка
  • a translate выше компонентов, чтобы повысить React компонентов путем инъекций

из моего исследования в этом, похоже, два основных подхода используются для i18n в JavaScript,ICU и gettext.

Я только когда-либо использовал gettext, поэтому я предвзят.

что меня поражает как бедных поддержка. Я пришел из мира PHP, либо CakePHP, либо WordPress. В обеих этих ситуациях основным стандартом является то, что все строки просто окружены __(''), затем дальше по строке вы получаете переводы с помощью PO файлы очень легко.

gettext

вы получаете знакомство sprintf для форматирования строк и PO-файлов будет легко переведен тысячами различных агентств.

есть два популярных варианта:

  1. i18next, С использованием, описанным этим arkency.com сообщение в блоге
  2. Джед, С использованием, описанным часовой.IO post и React+Redux пост,

оба имеют поддержку стиля gettext, форматирование стиля sprintf строк и импорт / экспорт в PO-файлы.

i18next имеет расширение React развитые сами по себе. Джед-нет. Часовой.io, похоже, использует пользовательскую интеграцию Jed с React. The React+Redux post, предлагает использовать

инструменты: jed + po2json + jsxgettext

однако Jed кажется более gettext целенаправленное осуществление - это намерение, где, как i18next просто это как вариант.

ОИТ

Это имеет большую поддержку для крайних случаев вокруг переводов, например, для решения гендерных проблем. Я думаю, вы увидите преимущества от этого, если у вас есть более сложные языки для перевода.

популярный вариант это messageformat.js. Кратко обсуждается в этом часовой.IO блог учебник. messageformat значение.Яш на самом деле разработан тем же человеком, который написал Jed. он делает довольно stong претензии для использования ICU:

Jed-это функция, полная, На мой взгляд. Я рад исправить ошибки, но, как правило, не заинтересован в добавлении больше в библиотеку.

Я также поддерживаю messageformat.js. Если вам конкретно не нужна реализация gettext, я мог бы предложить использовать MessageFormat вместо этого, так как он имеет лучшую поддержку для нескольких / пола и имеет встроенный языковой стандарт данные.

грубое сравнение

gettext с 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' });

если еще не сделано, взглянув на 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
  • cra
  • ЭКСПО.js
  • далее.js
  • сборник рассказов интеграция
  • razzle
  • dat
  • ...

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

кроме того, вы также должны рассмотреть рабочий процесс во время разработки и позже для ваших переводчиков ->https://www.youtube.com/watch?v=9NOzJhgmyQE


Я хотел бы предложить простое решение с помощью create-react-app.

приложение будет построено для каждого языка отдельно, поэтому вся логика перевода будет перемещена из приложения.

веб-сервер будет автоматически обслуживать правильный язык, в зависимости от Принять-Язык заголовок или вручную, установив 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},
});
// 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',
    }
);
export default withStyles(styles)(Component);

добавьте переменную языковой среды в свой пакета.в 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);