IT story

반응 / 리 독스 및 다국어 (국제화) 앱-아키텍처

hot-time 2020. 7. 26. 12:00
반응형

반응 / 리 독스 및 다국어 (국제화) 앱-아키텍처


여러 언어 및 로캘로 제공되어야하는 앱을 만들고 있습니다.

내 질문은 순전히 기술적 인 것이 아니라 아키텍처와 사람들이 실제로이 문제를 해결하기 위해 프로덕션에서 사용하는 패턴에 관한 것입니다. "쿡북"을 찾을 수 없어서 좋아하는 Q / A 웹 사이트로 전환하고 있습니다. :)

내 요구 사항은 다음과 같습니다 (실제로 "표준"임).

  • 사용자는 언어를 선택할 수 있습니다 (사소한)
  • 언어를 변경하면 인터페이스가 선택된 새 언어로 자동 번역됩니다.
  • 나는 숫자, 날짜 등을 포맷하는 것에 대해 너무 걱정하지 않고 문자열을 번역하는 간단한 솔루션을 원합니다.

내가 생각할 수있는 가능한 해결책은 다음과 같습니다.

각 구성 요소는 개별적으로 번역을 처리

즉, 각 구성 요소에는 번역 된 문자열과 함께 en.json, fr.json 등의 파일 세트가 있습니다. 그리고 선택한 언어에 따라 값을 읽는 데 도움이되는 도우미 기능.

  • 장점 : React 철학을 더 존중하며 각 구성 요소는 "독립형"입니다.
  • 단점 : 모든 번역을 파일로 중앙 집중화 할 수는 없습니다 (예를 들어 다른 사람이 새로운 언어를 추가하도록)
  • 단점 : 모든 피의 구성 요소와 자녀에게 여전히 현재 언어를 소품으로 전달해야합니다.

각 구성 요소는 소품을 통해 번역을받습니다.

그래서 그들은 현재 언어를 알지 못하고 현행 언어와 일치하는 소품으로 문자열 목록을 가져옵니다.

  • Pro :이 줄은 "위에서"나오기 때문에 어딘가에 집중할 수 있습니다
  • 단점 : 각 구성 요소가 이제 번역 시스템에 연결되어 있으므로 재사용 할 수 없으며 매번 올바른 문자열을 지정해야합니다.

소품을 약간 우회하고 컨텍스트를 사용 하여 현재 언어를 전달할 수 있습니다.

  • 장점 : 대부분 투명하며, 소품을 통해 항상 현재 언어 및 / 또는 번역을 전달할 필요가 없습니다.
  • 단점 : 사용하기가 번거로워 보입니다.

다른 아이디어가 있으면 말하십시오!

어떻게합니까?


꽤 많은 솔루션을 시도한 후, 나는 잘 작동하고 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] : 최근에 React & Redux로 빌드 한 Retrospective Board (Agile Retrospectives 용)에서 일했으며 다국어입니다. 많은 사람들이 주석에서 실제 사례를 요청했기 때문에 여기에 있습니다.

https://github.com/antoinejaussoin/retro-board/tree/master 에서 코드를 찾을 수 있습니다.


내 경험상 가장 좋은 방법은 여러 가지 이유로 i18n redux 상태 를 생성 하고 사용하는 것입니다.

1-데이터베이스, 로컬 파일 또는 EJS 또는 jade와 같은 템플릿 엔진에서 초기 값을 전달할 수 있습니다.

2- 사용자가 언어를 변경하면 UI를 새로 고치지 않고도 전체 응용 프로그램 언어를 변경할 수 있습니다.

3- 사용자가 언어를 변경하면 API, 로컬 파일 또는 상수에서 새 언어를 검색 할 수도 있습니다

4- 시간대, 통화, 방향 (RTL / LTR) 및 사용 가능한 언어 목록과 같은 문자열을 사용하여 다른 중요한 사항을 저장할 수도 있습니다.

5- 변경 언어를 일반적인 redux 조치로 정의 할 수 있습니다

6- 백엔드와 프론트 엔드 문자열을 한곳에서 가질 수 있습니다. 예를 들어, 제 경우에는 현지화에 i18n 노드사용 하고 사용자가 UI 언어를 변경하면 정상적인 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)

가장 유명한 날짜 및 시간 npm 라이브러리 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 (redux없이)를 사용하여 동일한 개념을 구현하는 많은 프레임 워크가 있습니다. 개인적으로 I18next를 추천 했습니다.


Antoine의 솔루션은 잘 작동하지만 몇 가지주의 사항이 있습니다.

  • React 컨텍스트를 직접 사용하므로 이미 Redux를 사용할 때 피하는 경향이 있습니다.
  • 파일에서 직접 문구를 가져옵니다. 런타임시 클라이언트 측에서 필요한 언어를 가져 오려는 경우 문제가 될 수 있습니다.
  • 경량 인 i18n 라이브러리를 사용하지 않지만 복수 및 보간과 같은 편리한 번역 기능에 액세스 할 수는 없습니다.

이것이 Redux와 AirBNB의 Polyglot 위에 redux-polyglot구축 한 이유 입니다. (저는 저자 중 하나입니다)

그것은 제공합니다 :

  • 언어와 해당 메시지를 Redux 스토어에 저장하는 감속기. 다음 중 하나를 통해 둘 다 제공 할 수 있습니다.
    • 특정 동작을 포착하고 현재 언어를 빼내고 관련 메시지를 가져 오거나 가져 오도록 구성 할 수있는 미들웨어.
    • 직접 파견 setLanguage(lang, messages)
  • 4 가지 방법을 제공 getP(state)하는 P객체 를 검색 하는 선택기 :
    • t(key): 오리지널 폴리 글 로트 T 함수
    • tc(key): 대문자 번역
    • tu(key): 대문자 번역
    • tm(morphism)(key): 사용자 정의 모핑 번역
  • a getLocale(state)selector to get current language
  • a translate higher order component to enhance your React components by injecting the p object in props

Simple usage example :

dispatch new language :

import setLanguage from 'redux-polyglot/setLanguage';

store.dispatch(setLanguage('en', {
    common: { hello_world: 'Hello world' } } }
}));

in component :

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

Please tell me if you have any question/suggestion !


From my research into this there appears to be two main approaches being used to i18n in JavaScript, ICU and gettext.

I've only ever used gettext, so I'm biased.

What amazes me is how poor the support is. I come from the PHP world, either CakePHP or WordPress. In both of those situations, it's a basic standard that all strings are simply surrounded by __(''), then further down the line you get translations using PO files very easily.

gettext

You get the familiarity of sprintf for formatting strings and PO files will be translated easily by thousands of different agencies.

There's two popular options:

  1. i18next, with usage described by this arkency.com blog post
  2. Jed, with usage described by the sentry.io post and this React+Redux post,

Both have gettext style support, sprintf style formatting of strings and import / export to PO files.

i18next has a React extension developed by themselves. Jed doesn't. Sentry.io appear to use a custom integration of Jed with React. The React+Redux post, suggests using

Tools: jed + po2json + jsxgettext

However Jed seems like a more gettext focussed implementation - that is it's expressed intention, where as i18next just has it as an option.

ICU

This has more support for the edge cases around translations, e.g. for dealing with gender. I think you will see the benefits from this if you have more complex languages to translate into.

A popular option for this is messageformat.js. Discussed briefly in this sentry.io blog tutorial. messageformat.js is actually developed by the same person that wrote Jed. He makes quite stong claims for using ICU:

Jed is feature complete in my opinion. I am happy to fix bugs, but generally am not interested in adding more to the library.

I also maintain messageformat.js. If you don't specifically need a gettext implementation, I might suggest using MessageFormat instead, as it has better support for plurals/gender and has built-in locale data.

Rough comparison

gettext with 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 (my best guess from reading the guide):

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

If not yet done having a look at https://react.i18next.com/ might be a good advice. It is based on i18next: learn once - translate everywhere.

Your code will look something like:

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

Comes with samples for:

  • webpack
  • cra
  • expo.js
  • next.js
  • storybook integration
  • razzle
  • dat
  • ...

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

Beside that you should also consider workflow during development and later for your translators -> https://www.youtube.com/watch?v=9NOzJhgmyQE


I would like to propose a simple solution using create-react-app.

The application will be built for every language separately, therefore whole translation logic will be moved out of the application.

The web server will serve the correct language automatically, depending on Accept-Language header, or manually by setting a cookie.

Mostly, we do not change language more than once, if ever at all)

Translation data put inside same component file, that uses it, along styles, html and code.

And here we have fully independent component that responsible for its own state, view, translation:

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

Add language environment variable to your 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",

That is it!

Also my original answer included more monolithic approach with single json file for each translation:

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

참고URL : https://stackoverflow.com/questions/33413880/react-redux-and-multilingual-internationalization-apps-architecture

반응형