i18nについて思ったこと

ランニング用のLサイズのパンツを楽天で買ったんです。
届いて気がつきました。
レディースを買っていたことに。

しょうがないのでお尻をプリプリさせながら、朝から40過ぎのおっさんが街中を走っていたんですが、ついに先日少し破れまして、躊躇なく捨てることができました!

めちゃくちゃ嬉しかった!

嬉しくて近所のまいばすけっとでシャンパン買ってきて夜は乾杯しました。

さて本題。

ある現場でi18nは直感的に見えづらい、国際化対応はまずやらないし、不必要なi18nの使用はアンチパターンだ。ということでi18nをつかった変換が文字列リテラルに書き換えられてました。

そういえばi18nを使うことについて言語化していなかったなと思ったので、言語化ついでに記事にしてみました。

文字列リテラルをそのまま使うこと

文字列リテラルってたしかに直感的で便利なんですが、ただの文字列でしかないんですよね。値それ以上の意味はない。

文字の内容がたしかに雄弁に語っているように見える、見えるんですがその文字が意味するものはコンテキストによるうっすらとした結合でしかないんです。
ほぼ何にも紐づいていない。

例えばUserコントローラーのshowアクションに書かれていた文字列リテラル(ユーザー等)の場合はUserリソース関連の処理だなとはわかる。
が、これは人間の認知力に完全に頼った理解であり、プログラム的には何も表現されていないし、実際に何も紐づいていないんです。

ユーザーとメンバーとカスタマーという概念があり、所属グループ、所属企業、サークルがある。
別々のコンテキストでユーザーという文字を埋め込むと、それはどれを表現しているといえますか?

そのユーザーはControllerではメンバーを表しているかも知れないし、viewでは所属と紐づける中間テーブルを表しているかもしれません。
しかし文字列リテラルはコード的には何も色付けされていないわけですから、そのユーザーという文字列がどの業務(ドメイン)領域のユーザーを表しているか?ということについては何も語ってくれないわけです。

これは文字列リテラルをなぜわざわざ定数として定義するのかの解にもなります。

定数として定義することにより、この文字列リテラルは特殊であり、そのクラスの所属であり、スコープはクラスに閉じるよ。
このクラスのコンテキストで利用するよということがわかるんです。

定数としてこのクラスの特別な文字列として宣言する(クラスの色を付けてあげる)。
そうすると影響が読みやすく、わかりやすくて変更が楽で安全なコードになるわけです。
定数として宣言することで文字列以上の働きと意味をもたせることができるようになります。

クラスの色をつけるといういう意味であれば、型付け言語であればメソッドを持たない型でありながら単純な文字列を格納するstatusというフィールドにstatusという型を明示的に定義する感覚に近いと思っています。

1
type status string

番外編としてはIDEの恩恵設けれますね。



i18nの国際化対応以外の効用


で最初に戻るんですが、i18nは国際化対応としての機能だけだと認識されがちです。
それ以外のメリットってないんでしょうか?

ヒントは先程の定数化(文字列の所属化、特定クラスの色をつける)と、デコレータにあります。
modelに紐づけたlocaleの定義って、railsが規約にのっとって自動的にモデルと紐づけてくれるんです。
その定義はモデルの所属になりますしスコープもそのモデルに限定されます。
そして対応する言葉を返すrailsの仕組みを介したデコレータにもなります。


1
2
3
4
5
6
7
8
9
10
11
# config/locales/ja.yml
ja:
  activerecord:
    models:
      user: ユーザー
    attributes:
      user:
        name: 名前
        email: メールアドレス
        created_at: 作成日
        updated_at: 更新日
1
2
User.model_name.human # => "ユーザー"
User.human_attribute_name(:name) # => "名前"

さきほどのユーザーとメンバーとカスタマーの例に戻りましょう。
かなり強引ですが、以下のように設定したとしましょう。

1
2
3
4
5
6
ja:
  activerecord:
    models:
      user: ユーザー
      member: ユーザー
      customer: ユーザー
1
2
3
User.model_name.human # => "ユーザー"
Member.model_name.human # => "ユーザー"
Customer.model_name.human # => "ユーザー"

同じユーザーという文字列ですが、それぞれのユーザーという文字列がどのモデルを指しているかわからないという人はいないでしょう。

文字列を所属化することでプログラム的に判断できるようになっています。
その処理の前後、あるいは書かれた場所を考慮してあやふやな人間の認知力によって判断する必要はありません。

人の認知力に頼らずプログラム的にユーザーという文字列にモデルクラスをあてることにより文字列の所属を明らかにしています。

これがただの文字列で埋め込まれていた場合。例えばCustomerの文脈で使われている「ユーザー」という文字列を「購入者」としたいという仕様変更が入った場合。
人間の認知力にたよって前後の処理、書かれた場所のユーザーという文字列からCustomerの文脈で使われている箇所を徹底的に探し出し、修正しなければいけません。

影響が閉じていないため、修正範囲の特定が困難になり、変更容易性が損なわれています。


DDDなどでは業務の言葉とプログラムで使う言葉を対応させるという書き方をします。
そうするとプログラムは業務を自然に素直に表現でき、ロジックが書いてある場所に違和感はなくなって適切な場所に配置され、 影響が明確になり、修正箇所も業務の内容がそのまま反映されているため容易に特定できます。

プログラムは英語で書かれます、しかし我々は普段日本語を使う環境にいます。
日本語を使う我々においてi18nの設定は、間に入ってプログラムの用語(モデルの要素)と日本語の用語をrailsの仕組みを介して1対1で対応させます。
つまり業務の言葉とプログラムで使う言葉を透過的に一致させてくれます。

また単純にモデルの属性に対となる一意な名称を与えるということは、表記揺れを防止するという恩恵もあります。

まとめ

いかがでしたでしょうか?
プログラムと紐づけて考えるのは静的型付け言語のアプローチなので、動的型付け言語ではあまり考えたりしないかもしれません。
I18nの設定はひと手間かかるし、何が書かれているか設定を見直さないといけないため面倒くさく感じてしまうかもしれません。
しかしその裏ではその手間以上の意義があると感じています。

もちろん完全にプロジェクト内のコードを1文字残らず把握していて問題ない。把握できないサイズにスケールしない。 チームではないというのであれば問題ないと思います。

モデルであればi18nを使うことに関しては以下のようなことを考えています

  • 国際化対応としての機能
  • 表記揺れ防止の機能
  • 所属化(定数化)させる機能
  • デコレータとしての機能

view専用のlocaleだとちょっと意味が弱くなりますが使う価値はあると思います

  • 国際化対応としての機能
  • 表記揺れ防止の機能
  • 文字列が必ずその文字をさす(ポインタ的な)

例えば文字列リテラルで埋め込むと、PRレビュー時にここは「保存」というボタン名なのに、ここは「保存する」になっているとか余計なことを気にしなくてよくなります。
人間が一度にできることは限りがあるので、気にしなくていいことが少ないにこしたことはありません。
規則的かつ統一的に表記揺れのない文字列を埋め込めます。

番外編

そういえば10年以上前に文字列で直接埋め込んでいたら、i18n使って下さいと言われてi18nに全部直したことがあります。

そして直した後、ログインユーザーのロールによって読み出すyamlを変えるという仕様が発生しました。 そこでi18nのメソッドをフックして読み出すルートを変える処理を書いたことがあります。

viewは共通だけど、ロールによって見せる文言を変えたいというやつです。
当時のコードもyamlも手元にないので具体的に話せないんですが、
こんな感じにyamlを設定したら先生と生徒で文字を読みかえてくれるような処理です。

1
2
3
4
5
ja:
  teacher:
    homework: 課題
  student:
    homework: 宿題

かなりニッチなケースなのでメリットとしてあげれないんですが、ちょっと面白かったのでご紹介でした。

既存の処理をフックして処理を追加して元の処理に戻したりと柔軟に対応できるのはrubyのいいところですね。

Comments