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

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

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

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

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

Каждый компонент выполняет перевод отдельно

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

  • Плюсы: более уважительное отношение к философии React, каждый компонент является «автономным».
  • Минусы: вы не можете централизовать все переводы в файле (например, чтобы кто-то еще добавил новый язык).
  • Минусы: вам все равно нужно передавать текущий язык в качестве опоры в каждом кровавом компоненте и их дочерних элементах.

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

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

  • Плюс: поскольку эти струны идут «сверху», их можно где-то централизовать.
  • Минусы: каждый компонент теперь привязан к системе перевода, вы не можете просто использовать его повторно, вам нужно каждый раз указывать правильные строки

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

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

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

Как ты делаешь это?

124
Antoine Jaussoin 29 Окт 2015 в 15:11

7 ответов

Лучший ответ

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

Первый аргумент - это ключ, который будет использоваться для поиска переводов в файле перевода (здесь я использовал имя компонента, но это могло быть что угодно). Второй (обратите внимание, что функция каррирована, чтобы позволить декораторам 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 своими собственными средствами.

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

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

110
Antoine Jaussoin 15 Апр 2016 в 07:54

Еще одно (легкое) предложение, реализованное в 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;

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

0
Sergi Juanati 28 Авг 2020 в 15:40

Я хотел бы предложить простое решение с использованием create-response-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',
    }
);

Добавьте переменную языковой среды в свой package.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);
0
Igor Sukharev 14 Ноя 2018 в 10:26

Если вы еще не закончили, попробуйте 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
  • expo.js
  • next.js
  • интеграция сборника рассказов
  • болтать
  • Дат
  • ...

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

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

1
jamuhl 21 Сен 2017 в 10:59

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

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

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

Gettext

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

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

  1. i18next с использованием, описанным в этом сообщение в блоге arkency.com
  2. Jed, использование которого описано в сообщение sentry.io и этот React + Redux post,

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

I18next имеет разработанное ими расширение React. Джед не знает. Sentry.io, похоже, использует пользовательскую интеграцию Jed с React. В сообщении React + Redux предлагается использовать

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

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

ICU

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

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

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

Я также поддерживаю 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' });
2
Community 20 Июн 2020 в 09:12

Решение Антуана работает нормально, но с некоторыми оговорками:

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

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

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

  • редуктор для хранения языка и соответствующих сообщений в вашем магазине Redux. Вы можете предоставить оба варианта:
    • промежуточное программное обеспечение, которое вы можете настроить для отслеживания определенных действий, вывода текущего языка и получения / получения связанных сообщений.
    • прямая отправка setLanguage(lang, messages)
  • селектор getP(state), который извлекает объект P, который предоставляет 4 метода: < UL>
  • 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);
    

    Пожалуйста, скажите мне, если у вас есть какие-либо вопросы / предложения!

    5
    Jalil 20 Ноя 2016 в 11:30

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

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

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

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

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

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

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

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

    Обновление (14.06.2019)

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

    20
    Fareed Alnamrouti 14 Июн 2019 в 06:44