多言語対応を半自動化する 〜Rails 後編〜
はじめに
RYDE ではデジタル乗車券アプリ「RYDE PASS」の開発・運用を行っています。
サービスの概要については以下の記事をご覧ください。
今回はこちらの記事の続編です。↓
- バックエンド: DB の値
- バックエンド: locale ファイル
- モバイルアプリ: locale ファイル
今回は「バックエンド: locale ファイル」の多言語対応を半自動化した方法を紹介します。
前回の記事では、Rails アプリケーションにおける データベースの多言語対応 を効率化する方法を紹介しました。
本記事では、その続編として、エラーメッセージ等に使用する locale ファイル の多言語対応について解説します。
なお、Rails アプリの多言語化にはrails-i18n
を使用します。
対応方法
locale ファイルの翻訳についても、前回記事と同様に Cloud Translation を使用します。
サービス概要や選定理由については前回記事を参照ください。
翻訳の差分管理
多言語の翻訳を行うにあたり、翻訳コストには気をつけなければなりません。
新しい locale を追加するたび、既存のものも含めた全 locale に対して自動翻訳を行ってしまうと、あっという間に Cloud Translation の無料枠を食い潰し、利用料金が嵩んでしまいます。
そこで本記事では、以下のように差分管理をし、差分が検出された日本語 locale のみに対して自動翻訳を行うようにします。
- locale ファイルを関心事ごとなどに細かく分割する
- 各日本語 locale ファイルのハッシュ値を保存する
この方法で locale を管理するため、以下のようなディレクトリ構造とします。
config/locales/
├── 1_1_base
│ ├── ja.yml
│ ├── models.ja.yml
│ ├── paypay.ja.yml
│ ├── stripe.ja.yml
│ ...
├── 1_2_generated # 自動生成した翻訳
│ ├── en.yml
│ ├── ko.yml
│ ├── zh-CN.yml
│ ├── zh-TW.yml
│ ├── models.en.yml
│ ├── models.ko.yml
│ ├── models.zh-CN.yml
│ ├── models.zh-TW.yml
│ ├── paypay.en.yml
│ ├── paypay.ko.yml
│ ├── paypay.zh-CN.yml
│ ├── paypay.zh-TW.yml
│ ├── stripe.en.yml
│ ├── stripe.ko.yml
│ ├── stripe.zh-CN.yml
│ ├── stripe.zh-TW.yml
│ ...
└── locales.lock
各ディレクトリの役割
-
1_1_base
: 日本語ベースの翻訳元ファイル -
1_2_generated
: Cloud Translation API で自動生成された翻訳ファイル -
locales.lock
: 各ファイルのハッシュ値を記録し、差分管理を行うファイル
locales.lock
では、以下のように各 locale ファイルのパスとハッシュ値を記録します:
# locales.lock
config/locales/1_1_base/ja.yml: xxxxxxxxxxxxxxxxxxxxxxxx
config/locales/1_1_base/model.ja.yml: yyyyyyyyyyyyyyyyyyyyyyyy
...
翻訳を実行する時に現在のファイル内容からハッシュ値を計算し、locales.lock
に記録された値と比較することで、差分を特定します。これにより、変更のないファイルはスキップし、差分のある locale ファイルのみ翻訳を行うようにします。
翻訳実行の流れは以下のようになります。
初回翻訳時
- 日本語の locale ファイルを作成/更新する
- locale ファイルの翻訳を行い、各言語の locale ファイルを生成する
- 翻訳後、日本語 locale のハッシュ値を保存する
2 回目以降翻訳時
- 日本語 locale を作成/更新する
- 最新の日本語 locale のハッシュ値と上記の日本語 locale ハッシュ値を比較して差分検知し、差分のある箇所のみ翻訳を行い、各言語の locale を生成する
- 日本語 locale ハッシュ値を最新の値に上書きする
翻訳実行
以下のような rake タスクを作成します。
これを実行すると、上記の仕組みにより自動翻訳が行われます。
namespace :multilingual do
# rake multilingual:translate_locales
desc 'localeファイル翻訳'
task translate_locales: :environment do
ja_yaml_paths = Dir.glob('config/locales/1_1_base/**/*ja.yml')
translator = Gcp::Translator.new
lock_file_path = 'config/locales/locales.lock'
lock_ref = YAML.load_file(lock_file_path) || {}
lock = {}
ja_yaml_paths.each do |src_path|
check_sum = Digest::SHA256.file(src_path).hexdigest
lock[src_path] = check_sum
# 日本語localeのハッシュ値比較し、差分がなければ翻訳しない
if check_sum == lock_ref[src_path]
puts "#{src_path}: Skipped"
next
end
print "#{src_path}: Translating..."
ja_yaml = YAML.load_file(src_path)
ja_flattened = flatten_keys(ja_yaml['ja']) # ネストしたyamlを1階層に変換
# 翻訳
Constants::AVAILABLE_LOCALES.reject { |element| element == 'ja' }.each do |lang|
translated_texts_hash = translate_texts(translator, ja_flattened, 'ja', lang)
# yml書き出し
target_path = src_path.gsub('1_1_base', '1_2_generated').gsub('ja.yml', "#{lang}.yml")
FileUtils.mkdir_p(File.dirname(target_path))
File.write(target_path, { lang => translated_texts_hash }.to_yaml(line_width: -1))
end
puts "\r\e[34m#{src_path}: Translated! \e[0m"
end
File.write(lock_file_path, lock.to_yaml)
puts "\e[32m✨Completed!\e[0m"
end
def translate_texts(translator, texts, source_lang, target_lang)
# textsの値をCloud Translationで翻訳実行(省略)
end
end
翻訳結果を修正したい場合は、生成された locale ファイルを直接編集します。
これにより、locale ファイルも多言語対応の半自動化が可能となります。
課題
一方で、この方式では locale ファイル単位で差分検知を行うため、既存の locale ファイルを更新した場合、特に変更のない文言まで翻訳が更新されてしまうことがあります。
現在の RYDE での運用としては、各 locale ファイルをなるべく小さくすることで余計な差分をなるべく抑えるようにしていますが、最終的には個別の key value に対して差分管理を行えるようにしたいと思っています。
最後に
まだ課題はあるものの、手動では到底できない 5 言語分の多言語対応が、この仕組みにより可能となりました。
次回はモバイルアプリ(React Native)の多言語対応について紹介します。
Discussion