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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

Как поживаете это?

6 101

6 ответов:

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

Компонент Более Высокого Порядка

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

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

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

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

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

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

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

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

6 - Вы можете иметь свои бэкэнд и передние строки в одном месте, например, в моем случае я использую i18n-node для локализации и когда пользователь меняет язык пользовательского интерфейса 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 компонент более высокого порядка для увеличения ваших реагирует компоненты путем впрыскивать

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

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

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

gettext

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

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

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

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

i18next имеет React extension развитые сами по себе. Джед не. Караульным.Ио появляются, чтобы использовать пользовательские интеграции Джед с реагировать. Элемент React+Redux post, предлагает использовать

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

кажется, Джед как-то gettext сфокусированная реализация-это выраженное намерение, где как i18next просто имеет его в качестве опции.

ICU

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

популярный вариант для этого messageformat.js. Кратко обсуждается в этом sentry.io блог учебник. messageformat.js на самом деле разработан тем же человеком, который написал Jed. он делает довольно стон претензии для использования 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);