Ruby on Railsで以下のように、モデルの日本語ファイルを用意する機会があると思います。
ja:
activerecord:
models:
article: 記事
attributes:
article:
title: 題名
content: 本文
created_at: 作成日時
updated_at: 更新日時
このファイルを用意しておくと、エラーメッセージなどでも日本語で表示してくれますね。
しかし、このファイルを毎回手動で作っていくのは非常に大変です。
そこで、自動でこのファイルを作れるようにしてみようっていうのが今回の記事の内容です。
完成イメージ
❯ bundle exec "rails tools:locale_generate"
Created: config/locales/ja/models/post.yml
config/locales/ja/models/user.yml already exists. Do you want to overwrite? (y/n/force/skip): skip
Created: config/locales/ja/models/post_user.yml
Created: config/locales/ja/models/company.yml
Created: config/locales/ja/models/user_profile.yml
-
bundle exec "rails tools:locale_generate"
というコマンドを実行すると、ファイルを作成していきます。 - すでにファイルがある場合は、上書きするか、スキップするか選べます
日本語は、以下のようなテーブルのコメント名やカラムのコメント名を参照します
前提
テーブルに記載されているコメント、カラムに記載されているコメントを参照します。
自分は基本的にテーブルやカラムにコメントを書いているので、同じようにしている方は同じコードで動かせると思います。
またconfig/application.rbのi18に関する設定は以下のようにしています。
# i18n
config.i18n.default_locale = :ja
config.i18n.load_path += Dir[Rails.root.join("config/locales/**/*.{rb,yml}")]
まずはRakeタスクを作ろう
自分は普段使うようなRakeタスクは、 lib/tasks/tools
フォルダを作ってそこに入れるようにしています。
ファイル作成
mkdir -p lib/tasks/tools
touch lib/tasks/tools/locale_generate.rake
このファイルの中身はひとまず全部以下に載せてしまいます。(GitHub Gistはこちら)
ファイルの中身
# frozen_string_literal: true
require "fileutils"
require "yaml"
require "pathname"
COLUMN_COMMENTS = {
ja: { "created_at" => "作成日時", "updated_at" => "更新日時" },
en: { "created_at" => "Created At", "updated_at" => "Updated At" }
}.freeze
EXCLUDED_TABLES = %w[schema_migrations ar_internal_metadata].freeze
DEFAULT_COLUMNS = %w[id created_at updated_at].freeze
module LocaleTasks
class << self
attr_accessor :overwrite_mode
def write_locale_file(args, table_name, locale_data)
locale_dir = Rails.root.join("config", "locales", args[:locale], "models")
create_directory(locale_dir)
locale_file_path = construct_file_path(locale_dir, table_name)
return unless file_writable?(locale_file_path)
write_to_file(locale_file_path, locale_data)
end
def generate_locale_data(args, table_name)
model_class = classify_constantize(table_name)
locale_data_initial(args, table_name).tap do |locale_data|
process_columns(args, model_class, table_name, locale_data)
end
rescue StandardError => e
handle_error(e, table_name)
end
private
def classify_constantize(table_name)
table_name.classify.constantize
end
def locale_data_initial(args, table_name)
table_comment = ActiveRecord::Base.connection.table_comment(table_name)
{ args[:locale].to_sym => { activerecord: { models: { table_name => table_comment }, attributes: { table_name => {} } } } }
end
def process_columns(args, model_class, table_name, locale_data)
ActiveRecord::Base.connection.columns(table_name).each do |column|
next if column.name == "id"
column_comment = fetch_column_comment(args, model_class, column)
locale_data[args[:locale].to_sym][:activerecord][:attributes][table_name][column.name] = column.comment || column_comment
end
end
def fetch_column_comment(args, model_class, column)
relation_name = find_relation_name(model_class, column.name)
if relation_name
reflection = model_class.reflections[relation_name]
if reflection.options[:polymorphic]
nil
else
related_class = reflection.klass
if related_class < ActiveRecord::Base
ActiveRecord::Base.connection.table_comment(related_class.table_name)
end
end
elsif DEFAULT_COLUMNS.include?(column.name)
COLUMN_COMMENTS[args[:locale].to_sym][column.name]
end
end
def find_relation_name(model_class, column_name)
model_class.reflections.find { |_k, v| v.foreign_key == column_name }&.first
end
def handle_error(error, table_name = nil)
message = table_name ? "Error with tables #{table_name}: #{error}" : error
puts "\e[31m#{message}\e[0m"
end
def create_directory(dir)
FileUtils.mkdir_p(dir) unless File.directory?(dir)
end
def construct_file_path(dir, table_name)
dir.join("#{table_name.classify.underscore}.yml")
end
def file_writable?(file_path)
return true unless File.exist?(file_path)
relative_path = relative_file_path(file_path)
overwrite_file?(relative_path)
end
def relative_file_path(file_path)
Pathname.new(file_path).relative_path_from(Pathname.new(Dir.pwd))
end
def overwrite_file?(relative_path)
case overwrite_mode
when "force"
true
when "skip"
false
else
print "#{relative_path} already exists. Do you want to overwrite? (y/n/force/skip): "
answer = $stdin.gets.chomp
self.overwrite_mode = answer.downcase
answer.casecmp("y").zero? || answer.casecmp("force").zero?
end
end
def write_to_file(file_path, data)
yaml_data = YAML.dump(stringify_keys(data))
File.write(file_path, yaml_data)
puts "\e[32mCreated: #{relative_file_path(file_path)}\e[0m"
end
def stringify_keys(hash)
case hash
when Hash
hash.to_h { |k, v| [k.to_s, stringify_keys(v)] }
when Enumerable
hash.map { |v| stringify_keys(v) }
else
hash
end
end
end
end
# コマンド: bundle exec "rails tools:locale_generate"
# コマンド: bundle exec "rails tools:locale_generate[ja,version]"
namespace :tools do
desc "Generate locale files from database comments"
task :locale_generate, [:locale, :model_name] => :environment do |_t, args|
args.with_defaults(locale: Rails.application.config.i18n.default_locale.to_s)
if args[:model_name].present?
table_name = args[:model_name].underscore.pluralize
locale_data = LocaleTasks.generate_locale_data(args, table_name)
LocaleTasks.write_locale_file(args, table_name, locale_data)
else
ActiveRecord::Base.connection.tables.each do |table_name|
next if EXCLUDED_TABLES.include?(table_name)
locale_data = LocaleTasks.generate_locale_data(args, table_name)
LocaleTasks.write_locale_file(args, table_name, locale_data)
end
end
end
end
気にしたポイント1: 特定のモデルのみの作成に対応
Rakeタスクの引数、なくても構わないのですが、2つ取ることができるようになっています。
# コマンド: bundle exec "rails tools:locale_generate"
# コマンド: bundle exec "rails tools:locale_generate[ja,version]"
namespace :tools do
desc "Generate locale files from database comments"
task :locale_generate, [:locale, :model_name] => :environment do |_t, args|
args.with_defaults(locale: Rails.application.config.i18n.default_locale.to_s)
if args[:model_name].present?
table_name = args[:model_name].underscore.pluralize
locale_data = LocaleTasks.generate_locale_data(args, table_name)
LocaleTasks.write_locale_file(args, table_name, locale_data)
else
ActiveRecord::Base.connection.tables.each do |table_name|
next if EXCLUDED_TABLES.include?(table_name)
locale_data = LocaleTasks.generate_locale_data(args, table_name)
LocaleTasks.write_locale_file(args, table_name, locale_data)
end
end
end
end
1つ目の引数には、日本語か英語かなど、どの言語であるかを指定できます。
この引数がない場合は、今Applicationで設定されている言語として作成します。
今設定されている言語は、これで取得します。
Rails.application.config.i18n.default_locale.to_s
2つ目の引数には、モデルを指定できます。
特定のモデルのみ作成する場合などに利用します。
つまり、下記のようなコマンドの場合には、言語はjaで、モデルはversionで作成することになります。
❯ bundle exec "rails tools:locale_generate[ja,version]"
Created: config/locales/ja/models/version.yml
気にしたポイント2: ファイルがすでに存在する場合は、上書きするかどうかを選べる
ファイルがすでに存在する場合には、上書きしたいこともあれば、上書きしたくないこともあると思います。
そこで、y
かn
かで選択できるようにしています。
print "#{relative_path} already exists. Do you want to overwrite? (y/n/force/skip): "
answer = $stdin.gets.chomp
self.overwrite_mode = answer.downcase
answer.casecmp("y").zero? || answer.casecmp("force").zero?
-
y
が入力されたら、そのファイルを上書きします。 -
force
が入力されたら、そのファイル以外もすでに存在するファイルは全て上書きする設定です。 -
n
が入力されたら、そのファイルを上書きしません。 -
skip
が入力されたら、そのファイル以外もすでに存在するファイルは全て上書きしない設定です。
上書き対象になるファイルが非常に多くなる可能性があったので、skipやforceのオプションも追加しています。
気にしたポイント3: belongs_toになっているカラムには、そのテーブルのコメントを参照する
例えば以下のように、referencesで作ったカラムに、コメントがない場合があるかもしれません。
class CreatePosts < ActiveRecord::Migration[7.0]
def change
create_table :posts, id: :uuid, comment: "投稿" do |t|
t.references :company, foreign_key: true, null: false, comment: "会社"
t.references :user, foreign_key: true, null: false
t.string :title, null: false, collation: "ja-x-icu", limit: 255, comment: "タイトル"
t.text :content, null: false, comment: "投稿内容"
t.timestamps
end
end
end
この時、コメント(comment: 〇〇
)があればコメントを参照します。
が、コメントがない上記ではuser_id
カラムのような時には、users
テーブルのテーブルに書かれているコメント名を参照してファイルを作成するようにしています。
---
ja:
activerecord:
models:
posts: "投稿"
attributes:
posts:
company_id: "会社"
user_id: "ユーザー" # ←カラムにはコメントないが、usersテーブルのコメント名が反映される
title: "タイトル"
content: "投稿内容"
created_at: "作成日時"
updated_at: "更新日時"
終わりに
こちら実際に作ってみて結構便利だったので、紹介の記事でした。
他の人が各モデルの日本語化をどのようにやっているかも気になっています。ぜひ良い方法あれば教えてください。
ぜひ同じような手順やってみて、localesの作成が便利になったとかあったら嬉しいです!