💎

Rails で config/locales/xxx/ja.yml を自動生成する gem を作った

2024/06/09に公開

リポジトリ

RubyGems
https://rubygems.org/gems/i18n_factory

github
https://github.com/tkmfujise/i18n_factory

概要

Gemfile
gem 'i18n_factory', group: :development

でインストールすると、

$ bin/rails g model Post title content:text 

を実行した際に、翻訳ファイルの雛形

config/locales/post/ja.yml
ja:
  activerecord:
    models:
      post: Post  
    attributes:
      post:
        title:   Title
        content: Content

を作ります。

また、既にDBにテーブルが存在する場合でも

$ bin/rails g i18n_factory:update Post
or
$ bin/railg g i18n_factory:update_all

を実行すると上記ファイルを作成します。
既にファイルが存在する場合は、追加した列のみ追記して元の定義は残すようにしています。(後述)

背景

Rails を触っていると、日本語のみに対応するシステムであっても翻訳ファイルを定義していると嬉しいことが多々あります。

  • エラーメッセージを翻訳ファイルから生成できる
  • 管理画面に ActiveAdmin を使用していると翻訳ファイルから自動的に画面名、画面項目名を生成してくれる
  • ViewファイルやModelファイルから日本語を排除できて翻訳ファイルで一元管理できる
  • DB設計書等を確認しなくても翻訳ファイルを見れば列が確認できる

翻訳ファイルで定義した値は下記で確認できます。(例: Post モデルの場合)

config/locales/post/ja.yml
ja:
  activerecord:
    models:
      post: 投稿  
    attributes:
      post:
        title:   タイトル
        content: 内容
Post.model_name.human # => '投稿'
Post.human_attribute_name :title # => 'タイトル'

そのため、自分が関わるプロジェクトは基本的に翻訳ファイルを作成して、
config/locales/<モデル名>/ja.yml という形でモデルごとに1ファイルずつ定義しているのですが、
雛形を作成するのがだるいなと思ったので勉強のため gem にしてみました。

gem の作成方法について

gem を作成すること自体が初めてだったのでやり方がわかりませんでしたが、いろいろググると情報があって助かりました。
下記の手順で作成できます。

1. gem の雛形の作成

$ bundle gem ここにgemの名前

2. gemspec でTODOになっている箇所の修正

3. 開発

実際に動くかどうか試したかったので、既存のRailsプロジェクトの lib/i18n_factory ディレクトリに上記で作成されたファイルを置いて、

Gemfile
gem 'i18n_factory', path: 'lib/i18n_factory'

でインストールして動作確認していました。

4. rubygems にアカウント登録

5. ビルド→デプロイ

ビルド

$ gem build xxx.gemspec

すると、xxx-0.1.0.gem が作成されるのでそれをプッシュします。

$ gem signin
$ gem push xxx-0.1.0.gem 

自分の場合はビルドを docker 上で行ない、できたファイルをホスト上でプッシュしました。

$ rails g model にフックさせる

$ rails g model で一緒にファイルを作成させたかったのですが、ググってもドンピシャの情報が出てこず。
わかったのは hook_for を指定するとそいつが実行されたあとに指定されたジェネレータも一緒に実行してくれるということでした。

Rails::Generators::ModelGenerator.hook_for で、作成した :i18n_factory ジェネレータをフックさせることで $ rails g model を実行した際に一緒に作成されるようにしました。

i18n_factory/lib/generators/model_generator.rb
Rails::Generators::ModelGenerator.hook_for(
  :i18n_factory,
  default: true, type: :boolean
) do |model, i18n_factory|
  model.invoke i18n_factory, [
    model.name, model.attributes.map(&:name)
  ]
end

module I18nFactory
  module Generators
    class ModelGenerator < Rails::Generators::NamedBase
      # ...
    end
  end
end

この辺は active_decorator などを参考にさせていただきました。

設定について

言語の判別

下記のようにロケールを設定すると思うので、そこから作成する翻訳ファイル名を自動的に決めています。

config/application.rb
module YourApplicationName
  class Application < Rails::Application
    # ...
    config.i18n.default_locale = :ja
  end
end

複数の言語の翻訳ファイルを自動で作りたい場合は、I18nFactory.config.locales を設定すれば動くようにしています。

config/environments/development.rb
Rails.application.configure do
  # ...
  config.after_initialize do
    I18nFactory.configure do |factory|
      factory.locales = [:ja, 'zh-TW']
    end
  end
end

出力不要な列について

I18nFactory.config.ignore_columns を設定すれば翻訳ファイルに出力しない列名を指定できます。
デフォルトは、id, updated_at, created_at です。

config/environments/development.rb
Rails.application.configure do
  # ...
  config.after_initialize do
    I18nFactory.configure do |factory|
      factory.ignore_columns = %w(id updated_at created_at)
    end
  end
end

出力不要なモデルについて

I18nFactory.config.ignore_paths を設定すれば翻訳ファイルに出力しないモデルを指定できます。

config/environments/development.rb
Rails.application.configure do
  # ...
  config.after_initialize do
    I18nFactory.configure do |factory|
      factory.ignore_paths = [
        'app/models/foo/bar.rb',
      ]
    end
  end
end

$ rails g i18n_factory:update について

$ rails g model 以外でも翻訳ファイルを作成したいので、コマンドを用意しました。

$ bin/rails g i18n_factory:update モデル名
もしくは
$ bin/railg g i18n_factory:update_all

で、DBに接続して列名を取得し翻訳ファイルを作成します。
i18n_factory:update_all コマンドは app/models 配下を舐めて、自動的に i18n_facotyr:update を実行するので最終的に実行されるのは同じです。

i18n_factory/lib/generators/update_all_generator.rb
module I18nFactory
  module Generators
    class UpdateAllGenerator < Rails::Generators::Base
      def update_all_i18n_files
        all_model_names.each do |name|
          begin
            if name.constantize.ancestors.include? ActiveRecord::Base
              generate 'i18n_factory:update', name
            end
          end
        end
      end
      # 略

既に翻訳ファイルが存在する場合
例えば、自分は enum_help という gem を使うので、enums.post.xxx という定義を書くことがあります。

config/locales/post/ja.yml
ja:
  activerecord:
    models:
      post: 投稿  
    attributes:
      post:
        title:   タイトル
        content: 内容
        status:  ステータス
  enums:
    post:
      status:
        published: 公開
        private:   非公開

のように定義されていたとして、Post#created_by を追加した場合も元の定義を残して追加するようにしました。
また、インデントは揃っていてほしいのでキーの後ろの空白は最大長のキーの長さに自動で揃えるようにしています。

config/locales/post/ja.yml
ja:
  activerecord:
    models:
      post: 投稿  
    attributes:
      post:
        title:      タイトル # ← 空白を挿入して縦列を揃えます
        content:    内容
        status:     ステータス
        created_by: CreatedBy # ←追加
  enums:
    post:
      status:
        published: 公開
        private:   非公開

最後に

rails g i18n_factory:update では元の定義を残したかったのと、空白で縦列を自動的に揃えたかったのでライブラリを使わず自前で生成しています。
そのため変なバグが発生する場合があるかもしれません。発見したら github の Issue で教えてほしいです。

Discussion