ブログ

Ruby on RailsのI18n gem(多言語対応)のコードを読み込んだら便利な発見がたくさんあった!

サイバーウェーブ開発部の梶原です。

サイバーウェーブの自社プロダクト「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コードの中を探していたら見つけました。

参考: https://github.com/ruby-i18n/i18n/blob/55c7750a79aff26caa1a0b053c3a5b4432d23160/lib/i18n/backend/simple.rb#L69

以下のメソッドで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年のノウハウの詰まった研修コンテンツや、安定したシステム開発をするための手順が整っています。実力のあるシステム開発会社だからこそ、経験を積みながら、実践的なシステム開発の技術も学べます。自信をもって主義主張ができる『飯が食える』エンジニアを目指していただきます。

エンジニアとしてしっかりと飯を食べていけるまでには、道のりは決して短くありません。長期で頑張り、エンジニアになるという強い思いがあれば、実戦的な開発経験と、周りの仲間とコミュニケーションしながら、しっかりと成長できます。当社のノウハウを余すことなく活かし、技術力を大きく伸ばしていただきます。

ぜひ、エントリーをお待ちしております!

インターン採用

インターン採用

新卒・既卒・第二新卒採用

新卒・既卒・第二新卒

おすすめ記事