LoginSignup
44
44

More than 3 years have passed since last update.

Reactアプリでのi18n対応(国際化対応)についての勉強メモ

Last updated at Posted at 2019-01-15

年末〜年始にかけて、React with i18n対応(国際化対応)について調べていました。
下書きでずっと塩漬けになっているのを思い出し、TODOなどを残したまま一旦公開します。
あとで、少しずつ内容を充実させていきたい。

前提として、自身はi18n対応の経験がほとんどなく初心者レベルであるゆえ、書いてある内容も基本的なものがほとんどだと思います。
(昔、railsアプリ書いていたときにちょっとやっていた程度です...)

また、i18n対応を行うにあたってreact-intlを使用していますが、他の方の書かれた記事を見ていると、自分とは別のやり方をしていたり、いろいろとやり方がありそうな感じがしました。
実際に自身でもサンプルを作成してみて、言語ファイルの管理の仕方一つをとっても色々な手法が出てきそうな気がしたので、こんなやり方もあるんだなーという感じで見ていただいたほうが良いかもしれません。

react-intlを使う

Reactでフロントエンドアプリを作成する場合、いろいろとライブラリはあるようですが、Yahooのreact-intlが一番メジャーそうなので、これを使っていきます。 
https://github.com/yahoo/react-intl

ちなみに作ってみたサンプルアプリはこんな感じ。ボタンを押すと順繰りに言語が変更されていきます。

実際に作成してみたサンプルのgif demo

基本的な使い方

基本的にはreact-intl - wikiexamplesを見ておけば、なんとなくわかりそうでした。

自身で作成した実装のサンプルをタグも付けた状態でGithubにあげています。
https://github.com/shinshin86/try-i18n-at-react/releases/tag/v0.0.1

まず、言語ファイルを作成します。
下記をja.js(日本語の言語ファイル)としています。同じようにen.js(英語)ko.js(韓国語)なども作成しています。

const ja = {
  'Top.Title': 'サンプルタイトル',
  'Top.Message': 'こんにちは! { name }!',
  'ChangeLocale.Btn': '言語を切り替える',
  'ChangeLocale.Msg': '言語を韓国語に切り替えます。よろしいですか?'
};

export default ja;

{ name }と書くことで、下記のように値をパラメータとして動的に渡すことが可能です。

const name = 'John'



<FormattedMessage id="Top.Message" values={{ name }} />
{ // => こんにちは! John! }

余談

最初、ハマりそうになったことなのですが、言語ファイルはobjectのようにネストして管理ができません。

ただし、jsonymlで記述できるようにすることは可能です。
後ほど下記のライブラリを用いてymlで管理していこうと思います。
https://github.com/akameco/yaml-flat-loader

本当は👇のように書いて、<FormattedMessage id="Top.Message" values={{ name }} />で参照できたら良かったんだけど...

const ja = {
  Top : {
    Title: 'サンプルタイトル',
    Message: 'こんにちは! { name }!'
  },
  ChangeLocale: {
    Btn: '言語を切り替える',
    Msg: '言語を韓国語に切り替えます。よろしいですか?'
  }
};

設定した言語ファイルを実際に使用するためには、下記のような設定が必要となります。

import { addLocaleData } from 'react-intl';
import jaLocaleData from 'react-intl/locale-data/ja';
import enLocaleData from 'react-intl/locale-data/en';
import koLocaleData from 'react-intl/locale-data/ko';

import en from './en';
import ja from './ja';
import ko from './ko';

addLocaleData(enLocaleData);
addLocaleData(jaLocaleData);
addLocaleData(koLocaleData);

また言語を切り替える仕組みとして下記のようにchooseLocale関数を作成し、ページ初期化時や言語切替時に切り替えられるようにしました。
詳しくはソースコード内の実際の記述に譲りますが、ここではstateで選択した言語を保持して、反映させるようにしています。
(愚直な感じのコードを書いてしまっています。こればかりは自分の非力さを呪うばかり...)

ただし、実際にはReduxContext APIを使ったほうが現実的だと思います。

export const translationMessages = {
  en,
  ja,
  ko
};

export const chooseLocale = locale => {
  console.log('Your locale :', locale);
  switch (locale) {
    case 'en-GB':
      return translationMessages.en;
    case 'ja':
      return translationMessages.ja;
    case 'ko':
      return translationMessages.ko;
    default:
      return translationMessages.en;
  }
};

さて、上で諸々設定していますが、これらを実際にReact上で使用するために、下記のようにIntlProviderで囲んでやります。

  render() {
    const { locale } = this.state;
    return (
      <IntlProvider locale={locale} messages={chooseLocale(locale)}>
        <Main {...this.state} localeUpdate={this.localeUpdate} />
      </IntlProvider>
    );
  }

あとは下記のようにFormattedMessageから呼び出せます。

        <Title>
          <FormattedMessage id="Top.Title" />
        </Title>
        <p>
          <FormattedMessage id="Top.Message" values={{ name }} />
        </p>

pure functionとして使いたい場合

ちなみに純粋な関数として使いたい場合はinjectIntlを使うことで、props.intlから参照することが可能のようです。

// 下記のように`components`をラップすることで`this.props.intl`から参照できるようになる
export default injectIntl(Main);

👇

const { locale, messages } = this.props.intl;

まだ自分は試していませんが、他にも下記のようなやり方などがあるようでした。
https://blog.mitsuruog.info/2016/10/using-react-intl-make-react-app-as-i18n.html

言語ファイルの管理をもっと良い感じにしたい

ここからはakamecoさんの書かれた、この素晴らしい記事の受け売りにほぼなるかと思います。

コンポーネント時代のi18n
https://qiita.com/akameco/items/ccf32dedb3630f774358

実際にyml管理に変更したバージョンのソースコードは下記の通りとなります。

yaml-flat-loaderを使用することでymlでの管理を行えるようにします。
また上に書いたネストできない問題も解消します。

実際に、言語ファイルは下記のように可読性が向上しています。

Top:
  Title: サンプルタイトル
  Message: こんにちは! { name }!
ChangeLocale:
  Btn: 言語を切り替える
  Msg: 言語を英語に切り替えます。よろしいですか?

実際にどう書き換えたかについてはコミットログに説明を譲ります。
(規模も小さいゆえ、変更量も非常に少ないです)

(prettierの設定を修正したりしていますが、そこは関係ないので適当に読み流してください)

スナップショットテスト

アプリの規模が膨大になってきた場合、テストは必須となるでしょう。
ここでチェックしたいことは下記のようなことになるでしょうか?

  • すべての言語に値が入っているか?
  • keyのtypoチェック
  • ライセンスなど、言語が変わっても同一のテキストが本当に同一か?
    ⇒TODO: こういう共通の値ってどうやって管理するのが良いのか自分の中で見えていないので、後日調べる

(typoチェックはflowtypeを使っても実現できそうですが、今回はテストに寄せます。 → flowtypeによる保管の恩恵を受けられないようなパターンを想定しています)

const en = require('./en.yml');
const ja = require('./ja.yml');
const ko = require('./ko.yml');

const getBlanks = obj => Object.keys(obj).filter(v => obj[v] === '');
const getAllKeys = obj => Object.keys(obj);

test('snapshot [en]', () => {
  expect(en).toMatchSnapshot();
});
test('snapshot [ja]', () => {
  expect(ja).toMatchSnapshot();
});
test('snapshot [ko]', () => {
  expect(ko).toMatchSnapshot();
});
test('check whitelist [en]', () => {
  const blanks = getBlanks(en);
  expect(blanks).toMatchSnapshot();
});
test('check whitelist [ja]', () => {
  const blanks = getBlanks(ja);
  expect(blanks).toMatchSnapshot();
});
test('check whitelist [ko]', () => {
  const blanks = getBlanks(ko);
  expect(blanks).toMatchSnapshot();
});

test('check key equals', () => {
  const langList = [en, ja, ko];
  const allLangKeyList = [];
  for (var lang of langList) {
    expect(getAllKeys(lang)).toMatchSnapshot();

    if (allLangKeyList > 1) {
      expect(allLangKeyList[allLangKeyList.length - 1]).toEquals(
        allLangKeyList[allLangKeyList.length]
      );
    }
  }
});

余談、というかTODO

どうしても下記のエラーが解消できずに、const en = require('./en.yml')使っています...
babelrcの設定がおかしいのかと思うのですが、後日解消します。。。)

({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){import en from './en.yml';
                                                                                                    ^^

    SyntaxError: Unexpected identifier

    at ScriptTransformer._transformAndBuildScript (node_modules/jest-runtime/build/script_transformer.js:403:17)

meta要素のi18n対応

metaタグの対応を行う場合、Reactを用いているケースではreact-helmetを使用するのが一般的かと思います。
(あくまで私個人の観測圏内におけるデファクトスタンダードですが)

非常にシンプルなパターンを下に書き出します。

MetaTagsというメタタグ用のComponentを用意し、そちらにthis.props.intlをぶち込みます。

import MetaTags from './MetaTags';



    return (
      <Container>
        <MetaTags {...this.props.intl} />
        <Title>
          <FormattedMessage id="Top.Title" />
        </Title>
        <p>

あとは渡ってきた値から必要となるものを当てはめていくだけです。
{messages['Meta.Title']}という形で参照しているのが微妙な感じはありますが、ひとまずやりたいことの実現はできています。
(ここについては、もっとスマートなやり方があるかもしれません)

import React from 'react';
import { Helmet } from 'react-helmet';

const MetaTags = ({ messages }) => (
  <Helmet>
    <title>{messages['Meta.Title']}</title>
    <meta name="description" content={messages['Meta.Description']} />
  </Helmet>
);

export default MetaTags;

終わりに

なんとか最後まで書ききりましたが、振り返ってみると課題やTODOが盛りだくさんです。
また、実際にreact-intlを使って運用していく上で見えてくることなども多いと思うので、少しずつ修正しながら今後内容を充実させていけたらと考えています。

i18next ことはじめ(追記)

最近Reactアプリでの多言語対応にて、i18next, react-i18nextについて調べていました。
こちらもreact-intl同様、i18n対応では、よく使われているライブラリのようです。
触ってみた際の内容を下記にまとめたので、ポストのリンクを下記に貼らせてもらいます。

Reactアプリでのi18n対応(国際化/多言語対応)にi18nextを使ってみる

44
44
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
44
44