サイバーウェーブ開発部の梶原です。
サイバーウェーブの自社プロダクト「VALUE KIT」は多言語サービスの開発に対応しています。エンジニア用語では「多言語への対応」を「Internationalization」略して「I18n」と呼びます。「Internationalization」のIとnのあいだに18文字(nternationalizatio)がはさまることから、このように表記する慣習となりました。
Ruby on Railsには、I18nの実装をするためのAPIがすでに存在しています。I18nのソースコードをあれこれ読み込んでいくうちに見つけたものをご紹介します。
I18n.reload!でホットリロードする
開発にはデバッグがつきものです。rails consoleやpry-railsのbinding.pryを使っていると、localeファイルを変更したあとに、その変更内容が反映されているかをすぐ確認したくなります。これまで私はlocaleファイルに変更したあと、わざわざrails consoleやbinding.pryを閉じて、開き直していました。
以下のメソッドを使えば、その必要がないことに気づきました。
I18n.reload!
.reload!メソッドは、書き換えられたlocaleファイルを再読み込みするメソッドです。
和訳を一覧表示させる
集中してコードを書いているとき、I18nで定義したモデルと属性の和訳がパッと浮かばず、いちいちlocaleファイルを探して開く・・・地味ですがめんどうな作業です。
たとえばUserモデルにID属性とは別に「出席番号」属性があるとしましょう。「出席番号」の和訳・・・何だっけ・・・ID属性は別に存在しているのでidではなさそうです。まさか`shusseki_bangou`とか?
rails consoleで和訳を一覧表示するスニペットを書いてみました。
->model{klass=model.to_s.classify.constantize;text_value=klass.model_name.human+"\n";text_value+=model.to_s+"\n\n";enumerize_group=Hash.new{|hash,key|hash[key]=[]};klass.singleton_class.included_modules.include?(Enumerize) ? klass.enumerized_attributes.attributes.each{|key,val|enumerize_group[key]=val.values} : nil;klass.column_names.each{|attr|text_value+="\s\s#{I18n.t("activerecord.attributes.#{klass.table_name.singularize}.#{attr}")}\n";text_value+="\s\s#{attr}\n";enumerize_group.include?(attr) ? enumerize_group[attr].each{|item|text_value+="\s\s\s\s#{item.text}\n";text_value+="\s\s\s\s#{item}\n"} : nil;text_value+="\n"};puts text_value;nil}.call :user
コード末尾のシンボル(上記の例では:user)に対応する和訳を一覧表示します。
# 出力例
ユーザー
User
ID
id
ステータス
status
有効
available
停止
suspended
退会
出席番号
attendance_number
「出席番号」は`attendance_number`でした。
便利なスニペットなのですがコードが長ったらしいので、メソッドにしてみました。
wayaku.rb:
# wayaku.rb
require 'pry'
require 'active_record'
require 'sqlite3'
require 'enumerize'
I18n.load_path = ['wayaku.yml']
I18n.locale = :ja
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT, level: Logger::DEBUG)
class User < ActiveRecord::Base
connection.create_table table_name do |t|
t.string :status
t.string :attendance_number
end unless table_exists?
extend Enumerize
enumerize :status, in: [:available, :suspended, :leaved]
end
module Wayaku
extend_object ActiveRecord::Base
def wayaku(value = nil)
puts value.present? ? search(value) : list
end
def list
text = "\r\n"
text += model_name.human + "\r\n"
text += model_name.to_s + "\r\n\r\n"
column_names.each do |attr|
text += "\s\s" + human_attribute_name(attr) + "\r\n"
text += "\s\s" + attr + "\r\n"
if respond_to?(:enumerize) && enumerized_attributes[attr].present?
enumerized_attributes[attr].each_value do |value|
text += "\s\s\s\s" + value.text + "\r\n"
text += "\s\s\s\s" + value + "\r\n"
end
end
text += "\r\n"
end
text
end
def search(value)
return "\r\n警告\:引数は文字列\r\n\r\n" unless (value).is_a? String
mix = wayaku_attributes.merge(wayaku_enumerized)
mix = mix.invert unless value.match?(/^[a-z_]+$/)
mix = mix.transform_keys(&:to_s)
result = mix[value].to_s
result = result.presence || '警告:何も見つかりませんでした'
result = "\r\n" + result + "\r\n\r\n"
end
private
def wayaku_hash
I18n.config.backend.translations[:ja]
end
def model_symbol
model_name.singular.to_sym
end
def wayaku_attributes
wayaku_hash[:activerecord][:attributes][model_symbol]
end
def wayaku_enumerized
enumerized = wayaku_hash[:enumerize][model_symbol].values
enumerized = enumerized.inject { |result, item| result.merge(item) }
enumerized
end
end
# ->model{klass=model.to_s.classify.constantize;text_value=klass.model_name.human+"\n";text_value+=model.to_s+"\n\n";enumerize_group=Hash.new{|hash,key|hash[key]=[]};klass.singleton_class.included_modules.include?(Enumerize) ? klass.enumerized_attributes.attributes.each{|key,val|enumerize_group[key]=val.values} : nil;klass.column_names.each{|attr|text_value+="\s\s#{I18n.t("activerecord.attributes.#{klass.table_name.singularize}.#{attr}")}\n";text_value+="\s\s#{attr}\n";enumerize_group.include?(attr) ? enumerize_group[attr].each{|item|text_value+="\s\s\s\s#{item.text}\n";text_value+="\s\s\s\s#{item}\n"} : nil;text_value+="\n"};puts text_value;nil}.call :user
# User.wayaku
# User.wayaku('出席番号')
# User.wayaku('attendance_number')
wayaku.yml:
# wayaku.yml
ja:
activerecord:
models:
user: 'ユーザー'
attributes:
user:
id: 'ID'
status: 'ステータス'
attendance_number: '出席番号'
enumerize:
user:
status:
available: '有効'
suspended: '停止'
leaved: '退会'
Gemfile:
# Gemfile
# frozen_string_literal: true
source "https://rubygems.org"
gem 'pry'
gem 'activerecord', '7.0.8', require: "active_record"
gem 'sqlite3', '~> 1.7.3'
gem 'enumerize'
localeファイル(.ymlファイル)から読み込まれたデータを再利用する
上記のWayakuモジュールをつくっているとき、そもそもlocaleファイル(.ymlファイル)から読み込まれたデータはどこに保存されているのだろう? と思いました。私がlocaleファイルの中身をI18n経由で取り出すことはできないでしょうか。I18nのgemコードの中を探していたら見つけました。
以下のメソッドでI18nがlocaleファイルから読み込んだ中身を取得できます。
I18n.config.backend.translations
つどlocaleファイルから取り出しているのではなく、I18nがメモリに保存した内容を返しているので、rails consoleの実行中でないと値は返ってきません。
式展開のなかの変数名を確認する
I18nをコードリーディングしていたら、興味深いメソッドの存在に気がつきました。localeファイル(.ymlファイル)に式展開が含まれている場合、変数名を返します。interpolation_keysというメソッドです。
参照: https://github.com/ruby-i18n/i18n/blob/55c7750a79aff26caa1a0b053c3a5b4432d23160/lib/i18n.rb#L253
localeファイル(.ymlファイル)が次のとき
ja:
one: 'One interpolation %{foo}'
以下で変数fooが含まれていることを確認できます。
I18n.interpolation_keys('one')
# => "foo"
I18nのコードリーディングは練習にぴったり
もともと私がI18nについて調べはじめたのは、コードリーディングの練習のためでした。I18nは一般によく使われるgemで、読み進めていくと、I18nだけでなく、日々ふれるコードすべての読み方がだんだん分かるようになってきた気がします。gemのコードを読んだ経験が少ない私でも読めるところがあり、自分の知らないコードも登場するので興味深いです。
まだまだ勉強している最中ですが、また新しい学びがあったらご紹介しますね!
サイバーウェーブでは一緒に働く仲間を募集しています
サイバーウェーブでは一緒に働く仲間を募集しています。当社は創業20年を機に「第2の創業期」として、事業を拡大方針へと舵を切りました。会社が急拡大しており、若いメンバーやインターン生がどんどん入社しています。個人の成長は、勢いのある環境のなかでこそ加速されるものです。成長事業に参画できるチャンスです!
サイバーウェーブはコード1行1行に対してこだわりを持って、プロ意識をもったエンジニアを育てている、技術力に自信のあるシステム開発会社です。社内には、創業23年のノウハウの詰まった研修コンテンツや、安定したシステム開発をするための手順が整っています。実力のあるシステム開発会社だからこそ、経験を積みながら、実践的なシステム開発の技術も学べます。自信をもって主義主張ができる『飯が食える』エンジニアを目指していただきます。
エンジニアとしてしっかりと飯を食べていけるまでには、道のりは決して短くありません。長期で頑張り、エンジニアになるという強い思いがあれば、実戦的な開発経験と、周りの仲間とコミュニケーションしながら、しっかりと成長できます。当社のノウハウを余すことなく活かし、技術力を大きく伸ばしていただきます。
ぜひ、エントリーをお待ちしております!
インターン採用
新卒・既卒・第二新卒採用