RevComm Tech Blog

コミュニケーションを再発明し 人が人を想う社会を創る

Webアプリケーションの国際化対応をバックエンドからフロントエンドに移行した話

はじめに

株式会社 RevComm の Software Engineer 宇佐美です。

RevComm では、電話営業や顧客対応を可視化する音声解析 AI 搭載型のクラウド IP 電話 MiiTel (ミーテル) を提供しています。

miitel.com

MiiTel の中核プロダクトである MiiTel Analytics は、フロントエンドが React ・バックエンドが Python (Django) という構成の Web アプリケーションです。メイン言語は日本語を想定していますが、ユーザーが設定言語を変更することで英語で利用することも可能です。

今回、従来はバックエンドで行っていたWebアプリケーションの国際化対応 (internationalization, i18n) をフロントエンドに移行するという作業を行いました。

この過程でわかった国際化対応の方法や、国際化対応をバックエンドで行う場合とフロントエンドで行う場合それぞれのメリット・デメリットなどを紹介したいと思います。

前提・本記事のスコープ

国際化対応とは?

一口に国際化対応といっても、この言葉が指すスコープはコンテキストやビジネス要件によって大きく変わってきます。

一般的に Web アプリケーションの国際化対応といったときに含まれるものとしては、以下のようなものがあります。

  • 多言語対応 (文字セット、文言、書式など)
  • 日時情報の時差 (Time zones)
  • 通貨情報

Django はこれらすべてを含む国際化対応をフルサポートしていますが、この記事のスコープとしては多言語対応、それも日英の 2 言語のみの対応が中心となります。

Webシステム・アプリケーションの国際化対応は非常に幅広く奥深い分野なので、すべてを完璧に対応することは事実上不可能ですが、英語だけでも対応しておくと国際展開の可能性が広がります。

国際化対応の深淵な世界に関心がある方は、以下の動画をご覧になるとその一端がわかるかと思います。

www.youtube.com

国際化とローカル化

国際化と類似の概念として、ローカル化 (localization, l10n) というものがあります。この2つはしばしば混同されがちなので、Django の公式ドキュメント内にある定義を添付します。

  • 国際化 (internationalization)
    • ソフトウェアをローカル化に備えさせることです。通常、開発者によって行われます。
  • ローカル化 (localization)
    • 翻訳およびローカルな表示形式を記述することです。通常、翻訳者によって行われます。

https://docs.djangoproject.com/ja/3.2/topics/i18n/#definitions

実務ではローカル化も含めて国際化対応と呼ばれることがあったり、開発者がローカル化の作業 (翻訳テキストの用意) まで行うこともあるかと思います。

ただ、最低でもユーザーの目に見える部分に関するローカル化はネイティブレベルのチェックが必要になるでしょう。

バックエンドでの i18n 対応

ここからは実際の i18n 対応のやり方について紹介していきます。まずはバックエンド (Django) での対応です。

前述のとおり、MiiTel Analytics ではもともとバックエンドで国際化対応を行っていました。

Django は非常に多機能なフレームワークなので、国際化対応向けの機能ももちろん用意されています。

事前準備

Django で多言語対応を行う場合に必要となる事前準備がいくつかあります。主に settings.py への値の設定です。

# デフォルトの言語コード
LANGUAGE_CODE = ‘en’
 
# 翻訳用 Middleware を追加
MIDDLEWARE = [
    ...
    'django.middleware.locale.LocaleMiddleware',
    ...
]
 
# 言語ファイルのパス
LOCALE_PATHS = (
    os.path.join(BASE_DIR, 'locale'),
)

なお、LocaleMiddleware は SessionMiddleware の後かつ CommonMiddleware の前に置く必要があります。

https://docs.djangoproject.com/ja/3.2/topics/i18n/translation/#how-django-discovers-language-preference

翻訳文字列の指定

Django で多言語対応を行う場合、アプリケーション内で翻訳を行う対象となるテキストに対して翻訳文字列 (translation string) を指定し、翻訳対象の文言であることをマークします。

Django はこの翻訳文字列を見て、後述する言語ファイルの内容に従って翻訳対象となる言語に翻訳します。翻訳文字列の指定を行うときは、django.utils.translation から gettextgettext_lazy モジュールをインポートして使います。

from django.http import HttpResponse
from django.utils.translation import gettext as _
 
def my_view(request):
    output = _("TEXT_TO_BE_TRANSLATED")
    return HttpResponse(output)
    # 翻訳後の結果が HTTP レスポンスに渡される

ここでは TEXT_TO_BE_TRANSLATED という文字列が翻訳文字列として指定されています。

テンプレートを使う場合はシンタックスがやや異なり、i18n をロードしてから trans タグを使って翻訳文字列を指定します。

{% load i18n %}
<h1>{% trans "TEXT_TO_BE_TRANSLATED" %}</h1>
# 翻訳後の結果が HTML ファイルで表示される

デフォルト言語を設定しておけば、その言語をそのまま翻訳文字列として利用することも可能です。こうすると、ユーザーの利用言語がデフォルト言語を利用する場合は翻訳文字列がそのまま出力されます。

ただ、本来は翻訳文字列は Django に翻訳対象の文言であることを知らせるための ID としての役割を担っているので、文章よりはユニークな ID として成立する文字列が望ましいでしょう。

翻訳を利用する側の views や templates ですることは、基本的にはこれだけです。

言語ファイルの作成

翻訳文字列の指定ができたら、これに対応した言語ファイル (language file) を作成します。

言語ファイルは、翻訳文字列を実際に各言語にどのように翻訳するかを指示するためのファイルです。

言語ファイルという形式自体は Django や Python に固有のものではなく、UNIX-like な OS で使用されている翻訳システム gettext の一実装である GNU gettext で定められているものです。

https://www.gnu.org/software/gettext/manual/gettext.html#PO-Files

拡張子は .po です。

言語ファイルの構造は大きく分けて以下のようになっています。

  • メタデータ
  • エントリー
    • msgid (メッセージの ID。翻訳文字列と一致している必要がある)
    • msgstr (翻訳後の文言)

このファイルを日本語、英語などの翻訳対象の言語ごとに作成します。

言語ファイルの作成は、Django がコマンドから自動で行ってくれます。

$ django-admin makemessages -l ja

makemessages コマンドを実行すると、Django はソースコード全体から翻訳文字列を探し出して言語ファイルを作成します。

言語ファイルはそれぞれの翻訳文字列があるアプリケーション内の locale ディレクトリ内に作成されますが、settings.py 内の文言やアプリケーションに属さないファイルに関するものは LOCALE_PATHS で定義したディレクトリに作成されます。

https://docs.djangoproject.com/ja/3.2/ref/django-admin/#makemessages

なお MiiTel Analytics ではバックエンドのアプリケーションで使う言語ファイル以外に、フロントエンドで使うための言語パック API を用意しています。

フロントエンドではこの API から取得した言語パックを state として保持して、メッセージの表示などフロントエンド特有のユースケースで使用していました。

言語ファイルのコンパイル

言語ファイルはそれ自体ではアプリケーションから読み込むことができず、翻訳言語ごとにコンパイルする必要があります。

これも Django のコマンドから行うことができます。

$ django-admin.py compilemessages -l ja

コンパイルされた結果、.po ファイルと同じディレクトリに .mo ファイルが生成されます。

これでアプリケーションに翻訳が反映されることになります。

フロントエンドでの i18n 対応

上記のとおり、従来はバックエンド (Django) で処理してきた i18n 対応ですが、今回これをフロントエンドの React に移行しました。

とはいえ、バックエンドで使用していた言語ファイルは膨大な数があるため、すべてを一度に置き換えることは難しく、インクリメンタルに移行していっているという状況です。

react-i18next

フロントエンド (React) での i18n 導入にあたって、ライブラリは react-i18next を選びました。

https://react.i18next.com/

選定の基準としては、ダウンロード数・GitHub Stars 数・更新頻度の多さなどでしたが、すでに RevComm 内の他プロジェクトでも採用していることもありスムーズに導入できると考えました。

参考: npm trends

React の i18n 関連ライブラリ (2022 年 1 月時点)

ダウンロード数を見ると i18next が一番多いですが、これは Node.js などのバックエンドでの国際化対応も含むライブラリで、react-i18next は i18next をもとにできた React 用ライブラリです。

react-i18next is a powerful internationalization framework for React / React Native which is based on i18next. Check out the history of i18next and when react-i18next was introduced.

https://react.i18next.com/#what-is-react-i18next

インストールから導入まで

react-i18next はドキュメントが充実しているので、基本的にはドキュメントどおりに進めていくと簡単に導入することができます。

https://react.i18next.com/getting-started

まずはアプリケーションのルートディレクトリでライブラリをインストールします。

$ npm install react-i18next i18next

翻訳用の JSON ファイルを作成します。これはバックエンドでの言語ファイルにあたります。

"en": {
    "test": {
        "Welcome to React": "Welcome to React and react-i18next"
    }
},
"ja": {
    "test": {
        "Welcome to React": "React と react-i18next へようこそ"
    }
}

利用側の React コンポーネントでは以下の 2 つを行います。

  • react-i18next の初期化
  • 翻訳文字列の指定

まず react-i18next を初期化します。一般的にはトップレベルのコンポーネント (index.js や App.js など) で行います。

// App.js
import i18n from ‘i18next’;
import { initReactI18next } from 'react-i18next';
 
import resources from './i18n/resources.json';
 
i18n.use(initReactI18next)
    .init({
        lng,
        fallbackLng: ‘en’,
        debug: false,
        resources,
    });

i18n.init() 関数に渡す Options は色々と設定できるので、詳細は公式ドキュメントを参照してください。

https://www.i18next.com/overview/configuration-options

ユーザーの使用言語は lng で指定します。ブラウザの設定言語を動的に取得したり、後からi18next.changeLanguage() を使って言語を変更することもできます。

(ユーザーの locale に応じて言語を変更する場合などで使えます)

実際に翻訳を行うときは Django で行っていたのと同様に翻訳文字列を指定して、useTranslation() フックから取得した t 関数を使います。

useTranslation() の引数には JSON ファイル内のキーを指定します。

import React from "react";
import { useTranslation } from "react-i18next";
 
function SomeComponent() {
    const { t } = useTranslation(‘test’);
    return <h2>{t('Welcome to React')}</h2>;
    // Output: <h2>Welcome to React and react-i18next</h2>
}

ディレクトリ構成

MiiTel Analytics のフロントエンドでのディレクトリ構成はざっくりと以下のようになっていて、components ディレクトリ内でそれぞれ i18n 用の JSON ファイルを持っています。

ディレクトリ構成

このようにすることで追加や修正などの管理もしやすくなり、グローバルでのメッセージ ID の重複を考慮する必要もなくなります。

ディレクトリごとに i18n の JSON ファイルを管理する場合、i18n.addResourceBundle() を使ってリソースを読み込みます。

https://www.i18next.com/overview/api#addresourcebundle

deep と overwrite オプションを省略した場合、このファイル内では引数に渡された resource で言語ファイルが置き換えられます。

lng (言語) や ns (namespase) などをコンポーネントごとに毎回設定するのは不便なので、下記のように言語ファイルからこれらの情報を抽出する util 関数を作っておくと便利です。

export const I18nUtil = {
    addResourceJson: (i18n, json) => {
        Object.entries(json).forEach(([lng, value]) => {
            Object.entries(value).forEach(([ns, resources]) => {
                i18n.addResourceBundle(lng, ns, resources);
            });
        });
    },
};

フロントエンドで国際化対応するメリット・デメリット

あくまでも今回の技術スタックを使ったうえでの内容ですが、移行する中で気づいたメリット・デメリットは以下のようなものがありました。

メリット

ユーザーに見える部分を柔軟にカスタマイズしやすい

バックエンドで対応する方法だと、API レスポンス以外の国際化対応は何らかの形でまとめてフロントエンドに JSON を渡すような方法になってしまいがちです。

フロントエンドで対応することによって、対象のコンポーネントに限って国際化対応を行ったり、ディレクトリ構成を分けて管理しやすくなります。

言語ファイルのコンパイルが不要でデプロイが容易

Django では言語ファイルの更新があったときに再コンパイルが必要だったので、デプロイのタイミングでコマンドを実行する必要がありました (MiiTel Analytics では CI/CD に組み込んで自動実行していました) 。

フロントエンド移行に伴って、言語ファイルの JSON を更新するだけでよくなるので、何かの原因でコンパイルが漏れていて追加したエラーメッセージが翻訳されないといったことがなくなりました。

言語ファイルをコンテキストごとに分けて管理しやすい

フロントエンドで使うための言語ファイルをバックエンドから取得する場合、巨大な管理しづらいオブジェクトになってしまいやすいですし、コンポーネント間での props の受け渡しなども発生します。

コンポーネントごとに切ったディレクトリ内で言語ファイルを作ることで、コンテキストを絞って限定された namespace の中で管理することができます。

デメリット

サーバーサイドに閉じた文言は対応できない

外部公開している API のエラーメッセージなど、サーバーサイドに閉じていて UI を持たない、かつ国際化対応が必要なものについては、やはりサーバーサイドで対応するしかありません。

パフォーマンスが落ちることがある

バックエンドで国際化対応を行う場合はコンパイル済みのメッセージを表示するだけでよくキャッシュも効きやすいですが、フロントエンドでは JavaScript で処理する分のオーバーヘッドが多少なりともあります。

SEO に弱くなる可能性がある

Google などの検索エンジンの Bot やクローラーは JavaScript を適切に解釈するとされていますが、サーバーサイドで表示するのと同じレベルでクライアントサイドでの翻訳後の文言を見てくれるかどうかははっきりしません。

ただ、Next.js などで Server-side rendering を行っている場合は、react-i18next でも SSR 対応することが可能なようなので、SEO 重視のサービスであれば導入するのも手です。

https://react.i18next.com/legacy-v9/serverside-rendering

まとめ

以上のようにメリット・デメリットはありますが、デメリットの部分がサービス運営上クリティカルでない場合は、フロントエンドで国際化対応を行うというのはアリな選択肢だと思います。

MiiTel Analytics ではまだバックエンドで国際化対応を行っているものも多々ありますので、今後はできるだけフロントエンドに移行していく予定です。

RevComm としてもこれから海外展開に積極的に取り組んでいく時期ですので、興味がある方はぜひ採用ページをご覧ください!

www.revcomm.co.jp

補足

余談ですが、日本語から英語の翻訳を行うときに適切な文言を作るちょっとしたコツを共有したいと思います。

ユーザーの目に見える部分に関してはネイティブチェックを必須とすべきですが、内部的に利用するメッセージやコメントなどは開発者自身が英語を考える場面も現実的にはあります。

一番いいのは (身も蓋もないですが) 英語を勉強することですが、即席の対策としては Google 翻訳や DeepL などの自動翻訳サービスを使った方法がおすすめです。

ポイントとしては、単に日本語から英語の翻訳を行うのではなく、

  1. 自分で考えた英語訳を Input に入力する
  2. 上部の矢印をクリックして日本語と英語を反転させる
  3. 日本語に誤りや違和感があれば修正する
  4. Output の英語が問題ないか確認する

のような順番で行うのがおすすめです。

単語単位よりもなるべく長いセンテンスを入力したほうが、コンテキストがあるので正確で意図したものに近い訳になります。

英語を入力してから言語を反転させる

Google 翻訳などの自動翻訳も完璧ではありませんが、日本語話者が翻訳する場合と比較して、英語として成立しない文章や不自然な文章などは生成されにくいです。

サービス展開も開発チームもいつ多国籍になるかわからないので、開発者としては英語力は常に向上させていきたいですね。

参考資料

https://docs.djangoproject.com/en/3.2/topics/i18n/

https://react.i18next.com/

https://www.i18next.com/

https://stackoverflow.com/questions/49137654/internationalization-backend-or-frontend

www.youtube.com

www.youtube.com