Zenn
🌐

多言語対応を半自動化する 〜Rails 後編〜

2025/02/14に公開

はじめに

RYDE ではデジタル乗車券アプリ「RYDE PASS」の開発・運用を行っています。

https://pass.ryde-go.com/

サービスの概要については以下の記事をご覧ください。

https://zenn.dev/ryde/articles/5c2ba40d930577

今回はこちらの記事の続編です。↓

https://zenn.dev/ryde/articles/6515fbb14eaac0

  1. バックエンド: DB の値
  2. バックエンド: locale ファイル
  3. モバイルアプリ: 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 ファイルのみ翻訳を行うようにします。

翻訳実行の流れは以下のようになります。

初回翻訳時

  1. 日本語の locale ファイルを作成/更新する
  2. locale ファイルの翻訳を行い、各言語の locale ファイルを生成する
  3. 翻訳後、日本語 locale のハッシュ値を保存する

2 回目以降翻訳時

  1. 日本語 locale を作成/更新する
  2. 最新の日本語 locale のハッシュ値と上記の日本語 locale ハッシュ値を比較して差分検知し、差分のある箇所のみ翻訳を行い、各言語の locale を生成する
  3. 日本語 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)の多言語対応について紹介します。

RYDE株式会社

Discussion

ログインするとコメントできます