Програми React / Redux та Multilingual (Internacionalization) - Архітектура


119

Я будую додаток, який повинен бути доступний на декількох мовах та локалях.

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

Ось мої вимоги (вони справді "стандартні"):

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

Ось можливі рішення, які я міг би придумати:

Кожен компонент займається перекладом ізольовано

Це означає, що кожен компонент має, наприклад, набір файлів en.json, fr.json тощо поряд із перекладеними рядками. І допоміжна функція, яка допомагає читати значення з тих, що залежать від обраної мови.

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

Кожен компонент отримує переклади через реквізит

Тож вони не знають про поточну мову, вони просто приймають список рядків як реквізит, який відповідає поточній мові

  • Про: оскільки ці рядки надходять "зверху", їх можна десь централізувати
  • Мінуси: кожен компонент тепер прив’язаний до системи перекладу, ви не можете просто використовувати його повторно, вам потрібно щоразу вказувати правильні рядки

Ви трохи обминаєте реквізит і, можливо, використовуєте контекст, щоб передати поточну мову

  • Про: він переважно прозорий, не потрібно весь час передавати поточну мову та / або переклади через реквізити
  • Мінуси: це виглядає громіздко у використанні

Якщо у вас є якась інша ідея, будь ласка, скажіть!

Як ти це робиш?


2
Я вважаю за краще ідею об’єкта ключів з рядками перекладу, який передається як опора, вам не потрібно передавати кожну рядок як опору окремо. Зміна цього на найвищому рівні має призвести до повторного відображення. Я не думаю, що використання контексту не є хорошою ідеєю для цього, і кожен компонент, що має доступ до файлу перекладу, робить їх менш "тупими" та переносними, фактично imo (і складніше отримати додаток для повторного відображення змін мови).
Домінік

1
Насправді згідно з facebook.github.io/react/docs/context.html , використання контексту для обміну поточною мовою є одним із законних випадків використання. Підхід, який я зараз намагаюся, полягає у використанні цього плюс компонента вищого порядку для вирішення логіки вилучення рядків для цього конкретного компонента (ймовірно, на основі якогось ключа)
Антуан Жауссон

1
Можливо, ви також можете поглянути на Миттєвий . Вони вирішують цю проблему зовсім по-іншому, вирішуючи її в передній частині ala Optimizely (також змінюючи DOM під час завантаження).
Марсель Пансе

1
Зовсім непогано! Це справді зовсім інший звір (який пов’язує вас із послугою, яку, можливо, вам доведеться заплатити, якщо ваш веб-сайт росте), але мені подобається ідея, і це, мабуть, варто того маленького веб-сайту, на який вам потрібно швидко запустити!
Антуан Жауссон

4
Крім того, ви можете сказати, що ви є співзасновником Instant, замість того, щоб сказати "Вони", як ніби ви нічого не мали з ними :)
Антуан Жассойн

Відповіді:


110

Спробувавши декілька рішень, я думаю, що я знайшов таке, яке добре працює і повинно бути ідіоматичним рішенням для 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).

Перший аргумент - це ключ, який буде використовуватися для пошуку перекладів у файлі перекладу (я тут використовував ім’я компонента, але це може бути все, що завгодно). Другий (зауважте, що функція завита, щоб дозволити декораторам 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]: Нещодавно я працював над ретроспективною дошкою (для Agile Retrospectives), побудованою за допомогою React & Redux, і є багатомовною. Оскільки в коментарях досить багато людей запитували приклад із реального життя, ось це:

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


Це круте рішення .. цікаво, чи все-таки ви все ще зустрічаєтеся через кілька місяців? Я не знайшов багато порад у вигляді порад щодо моделей цього Інтернету
Деймон

2
Насправді я виявив, що це ідеально працює (для моїх потреб). Це змушує компонент працювати без перекладу за замовчуванням, і переклад просто над ним, а компонент не усвідомлює цього
Антуан Жауссон

1
@ l.cetinsoy ви можете використовувати dangerouslySetInnerHTMLопору, пам’ятайте про наслідки (вручну саніруйте введення). Дивіться facebook.github.io/react/tips/dangerously-set-inner-html.html
Теодор Санду,

6
Чи є причина, чому ви не пробували react-intl?
SureshCS

1
Дуже подобається це рішення. Я хотів би додати одне, що ми вважаємо дуже корисним для послідовності та економії часу, це те, що якщо у вас є багато компонентів із загальними рядками, ви можете скористатись змінними та розповсюдженням по об'єктах, наприкладconst formStrings = { cancel, create, required }; export default { fooForm: { ...formStrings, foo: 'foo' }, barForm: { ...formStrings, bar: 'bar' } }
Huw Davies

18

З мого досвіду найкращий підхід - це створити стан редукції i18n і використовувати його з багатьох причин:

1- Це дозволить вам передати початкове значення з бази даних, локального файлу або навіть з двигуна шаблону, такого як EJS або нефрит

2- Коли користувач змінить мову, ви можете змінити всю мову програми, навіть не оновлюючи інтерфейс користувача.

3- Коли користувач змінить мову, це також дозволить отримати нову мову з API, локального файлу або навіть з констант

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

5- Ви можете визначити мову зміни як звичайну редукційну дію

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

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 найбільш відомі дати та час НОЙ бібліотеки, ви можете перевести момент , але він вже має вбудований в перекладі просто необхідно передати поточний стан мови, наприклад:

import moment from "moment";

const umoment = moment().locale(i18n.language);
umoment.format('MMMM Do YYYY, h:mm:ss a'); // أيار مايو ٢ ٢٠١٧، ٥:١٩:٥٥ م

Оновлення (14.06.2019)

В даний час існує багато фреймворків, що реалізують ту саму концепцію, використовуючи API реакції контексту (без скорочення), я особисто рекомендував I18next


Чи може цей підхід працювати і для більш ніж двох мов? Враховуючи налаштування каталогу
tempranova

Даун проголосував. Це не дає відповіді на запитання. OP попросив ідею архітектури, а не пропозицію чи порівняння будь-якої бібліотеки i18n.
TrungDQ

9
Я запропонував каталог i18n як стан
редукса

5

Рішення Антуана працює чудово, але є деякі застереження:

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

Ось чому ми побудували редукс-поліглот поверх як Redux, так і AirBNB's Polyglot .
(Я один з авторів)

Він передбачає:

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

Простий приклад використання:

відправлення нової мови:

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

Скажіть, будь ласка, чи є у вас якесь питання / пропозиція!


1
Набагато краще оригінальні фрази для перекладу. І зробити інструмент, який аналізує всі компоненти для _()функцій, наприклад, щоб отримати всі ці рядки. Тож ви можете в мовному файлі перекласти це простіше і не возитися з божевільними змінними. У деяких випадках цільові сторінки потребують, щоб певна частина макета відображалася по-різному. Тож має бути доступна і якась розумна функція вибору за замовчуванням проти інших можливих варіантів.
Роман М. Косс

Привіт @Jalil, чи є десь повний приклад із проміжним програмним забезпеченням?
АркадійБ

Привіт @ArkadyB, Ми використовуємо це у виробництві для кількох проектів, які не мають відкритих джерел. Ви можете знайти більше інформації про модуль README: npmjs.com/package/redux-polyglot У вас є питання / труднощі з його використанням?
Джаліл

Моя основна проблема з цим і polyglot.js полягає в тому, що він повністю переосмислює колесо, а не будує поверх файлів PO. Ця альтернативна бібліотека виглядає перспективною npmjs.com/package/redux-i18n . Я не думаю, що це робиться набагато інакше - це просто надання додаткового шару для перетворення в та з PO-файлів.
icc97

2

З мого дослідження цього, як видається, є два основні підходи, які використовуються для 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, розроблене власноруч. Джед ні. Здається, Sentry.io використовує власну інтеграцію Jed з React. Повідомлення React + Redux пропонує використовувати

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

Однак Jed виглядає як більш орієнтована на gettext реалізація - це виражений намір, де як i18next це є лише опцією.

ІКУ

Це має більшу підтримку кращих випадків навколо перекладів, наприклад, для розгляду гендерних питань. Я думаю, ви побачите переваги від цього, якщо у вас будуть складніші мови для перекладу.

Популярний варіант для цього - messageformat.js . Коротко обговорено в цьому підручнику щодо блогу sentry.io . messageformat.js насправді розроблений тією ж людиною, яка написала Jed. Він висловлює досить жорсткі претензії на використання ICU :

На мій погляд, джед є повною характеристикою. Я радий виправляти помилки, але взагалі мені не цікаво додавати більше до бібліотеки.

Я також підтримую messageformat.js. Якщо вам спеціально не потрібна реалізація gettext, я б запропонував замість цього використовувати MessageFormat, оскільки він має кращу підтримку множини / статі та має вбудовані дані локалів.

Грубе порівняння

gettext зі спринтом:

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

Даун проголосував. Це не дає відповіді на запитання. OP попросив ідею архітектури, а не пропозицію чи порівняння будь-якої бібліотеки i18n.
TrungDQ

@TrungDQ Ось що запитувала ОП: "Моє запитання не суто технічне, а скоріше стосується архітектури та моделей, які люди насправді використовують у виробництві для вирішення цієї проблеми". . Це дві моделі, які використовуються у виробництві.
icc97

На мою думку, ця відповідь не дає інформації, яку я (та інші шукаю) шукаю. Надана вами інформація корисна, але можливо для іншого питання. Я просто хочу внести свій внесок у те, щоб правильна відповідь спливала до вершини (сподіваюся).
TrungDQ

@TrungDQ Якщо це не те, що ви шукаєте, тоді просто підкресліть той, який ви використовували, і проігноруйте інші, а не забороняючи певно вірні відповіді, які не відповідають конкретній частині питання, яке вас цікавить.
icc97

1

Якщо цього ще не зробити, ознайомтеся з 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>

Поставляється з зразками для:

  • вебпакет
  • cra
  • expo.js
  • next.js
  • інтеграція книжок
  • нездужання
  • дат
  • ...

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

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


Це не дає відповіді на запитання. OP попросив ідею архітектури, а не пропозицію чи порівняння будь-якої бібліотеки i18n.
TrungDQ

@TrungDQ як з вашим коментарем до моєї відповіді, що ви підтримали - ОП попросило поточні рішення, що використовуються у виробництві. Однак я запропонував i18next у своїй відповіді ще в лютому
icc97

0

Я хотів би запропонувати просте рішення за допомогою програми 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',
    }
);

Додайте змінну мовного середовища до свого пакета.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);

Це не буде працювати лише під час компіляції? Без можливості для користувача змінювати мову на льоту? Тоді це був би інший варіант використання.
Антуан Жауссон

Додаток буде складено для кожної потрібної мови. Веб-сервер подаватиме правильну версію автоматично, залежно від заголовка "Accept-Language" або від файлів cookie, встановлених користувачем під час руху. Тим самим всю логіку перекладу можна було б перемістити з програми.
Ігор Сухарєв
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.