読者です 読者をやめる 読者になる 読者になる

EduTalk

教育に関するテクノロジーやスタートアップ、考えたことについて発信中。

React(Flux)アプリをPolyglot.jsで多言語化する

f:id:atsuhio:20160604221943p:plainリンガルボックスでは、現状のスタックは下記のようになっています。サーバーレンダリングは行っていません。

クライアントサイド: Flux(Alt.js) + ReactRouter + Webpack + Polyglot.js(多言語化)
サーバーサイド: Rails + Active Model Serializer

Polyglot.jsよりもUS Yahooのreact-intlの方が有名かと思うのですが、僕がRailsi18nの方法に慣れていることもあって同じように書けるPolyglot.jsを選びました。Polyglot.jsはAirBnBオープンソースとして公開してるパッケージでAirBnBの多言語化に利用されています。


今回はこのスタックで、多言語対応をどのように行っているのか簡単に書いていきます。

1. 各言語のロケールをlocalesフォルダーに保存

現状はlocalesフォルダーにen-US.js、ja-JP.js、es-ES.jsなど言語別にファイルを保存しています。

例えばこんな感じです。

export default {
  messages: {
    keywords: "aprender Inglés online, aprender a hablar Inglés, Cursos online de Inglés, Cursos de Inglés",
    description: {
      default: "Aprenda a falar inglês online com professores Filipinos certificados, o LingualBox oferece lições de alta qualidade por preços acessíveis começando a US$2 por seção. Comece suas suas aulas grátis agora."
    }....
}

2. localeの判断

リンガルボックスの場合ですが、ログインしてないユーザーに関してはサブドメインに併せて言語を変更しています。サブドメインが"ja"だと日本語、"www"だと英語、"pt"だとポルトガル語という具合です。ログイン後のサブドメインは必ず"www"になるようにしていて、cookieにlocale情報を保存しています。

3. 必要な言語の翻訳だけを読み込んで、AltBootstrapでLocaleStoreに渡す

Alt.jsにはAlt.bootstrapというメソッドがあってここで、Storeに任意の初期データを保存することが出来るようになっています。
このメソッドを使ってページが表示される前にStoreに必要な言語の翻訳を渡しています。

// localeには2で書いたようにサブドメインやCookieのLocaleに併せて'en-US'、'es-ES'といった文字列をいれています。

const { messages } = require(`./locales/${locale}.js`).default; // localesフォルダから翻訳を取ってきて

const storeData = {
  LocaleStore: {
    currentLocale: locale,
    messages,
  },
};

alt.bootstrap(JSON.stringify(storeData)); // LocaleStoreに翻訳を渡してます。

4. App.jsからChildContextにPolyglotインスタンスを渡す

リンガルボックスでは下記のようにして、ChildContextに翻訳データを渡しています。

getChildContext() {
    const polyglot = new Polyglot();
    polyglot.extend(this.state.i18nMessages);
    return {
      polyglot,
      ....
    };
  }

こうすることで、Component内で、下記のように翻訳を表示出来ます。

render() {
  const {polyglot} = context
  return <p>{polyglot.t('general.loading')}</p>;
}

5. 言語の変更

リンガルボックスの場合は、言語を変更した場合に都度Rails API側から翻訳データを取ってくるという風にしています。

■LocaleActions

import alt from '../alt.js';
import LocaleApiUtils from '../utils/LocaleApiUtils.js';
import logError from '../utils/logError.js';

class LocaleActions {
  constructor() {
    this.generateActions(
      'switchLocaleSucceeded'
    );
  }

  switchLocale(newLocale) {
    LocaleApiUtils.fetchMessages(newLocale).then(
      (data) => {
        this.switchLocaleSucceeded({
          locale: newLocale,
          messages: data.messages,
        });
        document.body.lang = newLocale.split('-')[0];
      }
    ).catch(logError);
  }
}

export default alt.createActions(LocaleActions);

■LocaleStore

import alt from '../alt';
import LocaleActions from '../actions/LocaleActions.js';
import UserActions from '../actions/UserActions.js';
import cookie from 'cookies-js';

const THREE_MONTHS = 7948800;

class LocaleStore {
  constructor() {
    this.bindListeners({
      onSwitchLocaleSucceeded: LocaleActions.SWITCH_LOCALE_SUCCEEDED,
      ....
    });
    ....
  }
  onSwitchLocaleSucceeded(data) {
    cookie.expire('_locale', { domain: baseDomain })
      .set('_locale', data.locale, {
        domain: 'lingualbox.com',
        secure: true,
        expires: THREE_MONTHS,
     });
    this.currentLocale = data.locale;
    this.messages = data.messages;
  }
  ....
}

Rails側は例えばこんな感じです。

def fetch_messages
    query_params = CGI::parse(request.query_string)
    locale = query_params['locale'].first || 'en-US'

    json_file_path = Rails.root.join('public', 'js_locales', locale + '.json')
    messages = open(json_file_path) do |io|
      JSON.load(io)
    end
    render json: {
      messages: messages
    }
end

後は取ってきた翻訳データをStoreに保存して、Componentに反映するという通常の流れで言語変更が簡単に出来ます。

まとめ

Railsi18nをするのに比べて割と大変だったので、記事にしました。Fluxアプリの多言語化を検討されている方の参考になれば嬉しいです。

Reactに詳しい方、記事についてご意見や、もっと良い方法などあればTwitterでご指摘いただけると嬉しいです。id: @atsuhio