【コードリーディング】form.labelで2単語目以降小文字になる箇所がどこなのか

# はじめに
`form.label`を使い、以下のようにキャメルケースの文字列のみ引数に渡したとき、表示された文字列が先頭のみ大文字で、2単語目以降小文字になっていました。

```ruby
form.label('HogeFoo')
```
表示文字列：Hogefoo

Ruby on Railsの[labelのドキュメント](https://railsdoc.com/form#label)を確認したところ、ラベル配下のコンテンツなしの場合は以下のように記載されていました。

>
f.label
ラベル配下のコンテンツなし

```
f.label :name
#<label for="page_name">Name</label>
```

1単語のみの場合最初の文字が大文字になるようですが、単語が複数続いた場合については記載がありませんでした。
そのため、コードのどこで小文字になっているのか調べました。

# 環境
Ruby: 3.0.0
Ruby on Rails: 6.1.2.1
コード確認日: 2020/03/03

# コードを読む
それではRuby on Railsのコードを読んでいきます。
`form.label('HogeFoo')`を実行すると`FormHelper`の`label`メソッドが呼び出されます。

```form_helper.rb
def label(method, text = nil, options = {}, &block)
  @template.label(@object_name, method, text, objectify_options(options), &block)
end
```
https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/actionview/lib/action_view/helpers/form_helper.rb#L2252

`label`メソッド呼び出し時、引数は第一引数のみなので、`method`に`HogeFoo`がセットされ、それ以外は`nil`や`{}`がセットされます。
`@template`は`form`の初期化時（`form_for`呼び出し時）に`FormHelper`がセットされます。そのため、同じ`FormHelper`の`label`メソッドが呼び出されます。
なお、formの初期化処理についてはここではスキップします。

```form_helper.rb
def label(object_name, method, content_or_options = nil, options = nil, &block)
  Tags::Label.new(object_name, method, self, content_or_options, options).render(&block)
end
```

https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/actionview/lib/action_view/helpers/form_helper.rb#L1117

このメソッドでは`Tags::Label`クラスを初期化して`render`メソッドを呼び出しています。
まず`Tags::Label`クラスの初期化処理を見ていきます。

```label.rb
def initialize(object_name, method_name, template_object, content_or_options = nil, options = nil)
  options ||= {}

  content_is_options = content_or_options.is_a?(Hash)
  if content_is_options
    options.merge! content_or_options
    @content = nil
  else
    @content = content_or_options
  end

  super(object_name, method_name, template_object, options)
end
```

https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/actionview/lib/action_view/helpers/tags/label.rb#L30

各変数に値をセット後、親クラスの初期化処理を呼び出しています。そのため、`Label`クラスの親クラスである`Base`を見ます。

```base.rb
def initialize(object_name, method_name, template_object, options = {})
  @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
  @template_object = template_object

  @object_name.sub!(/\[\]$/, "") || @object_name.sub!(/\[\]\]$/, "]")
  @object = retrieve_object(options.delete(:object))
  @skip_default_ids = options.delete(:skip_default_ids)
  @allow_method_names_outside_object = options.delete(:allow_method_names_outside_object)
  @options = options

  if Regexp.last_match
    @generate_indexed_names = true
    @auto_index = retrieve_autoindex(Regexp.last_match.pre_match)
  else
    @generate_indexed_names = false
    @auto_index = nil
  end
end
```

https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/actionview/lib/action_view/helpers/tags/base.rb#L12

`HogeFoo`が入った`method_name`は、`to_s`で文字列変換後、`dup`メソッドでコピーされ`@method_name`にセットされます。
初期化メソッドでは各変数の初期化のみなので、次に`render`メソッドを見ていきます。

```label.rb
def render(&block)
  options = @options.stringify_keys
  tag_value = options.delete("value")
  name_and_id = options.dup

  if name_and_id["for"]
    name_and_id["id"] = name_and_id["for"]
  else
    name_and_id.delete("id")
  end

  add_default_name_and_id_for_value(tag_value, name_and_id)
  options.delete("index")
  options.delete("namespace")
  options["for"] = name_and_id["id"] unless options.key?("for")

  builder = LabelBuilder.new(@template_object, @object_name, @method_name, @object, tag_value)

  content = if block_given?
    @template_object.capture(builder, &block)
  elsif @content.present?
    @content.to_s
  else
    render_component(builder)
  end

  label_tag(name_and_id["id"], content, options)
end
```

https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/actionview/lib/action_view/helpers/tags/label.rb#L44

このメソッドではlabelタグの準備と描画を行っています。このメソッド内で呼び出されている`add_default_name_and_id_for_value`メソッドに関しては、`name_and_id`のデフォルト値をセットするのみなので、この記事ではスキップします。
その後、`LabelBuilder`クラスの初期化時に`@method_name`を渡しているので、その初期化処理を見てみます。

```label.rb
class LabelBuilder # :nodoc:
  attr_reader :object

  def initialize(template_object, object_name, method_name, object, tag_value)
    @template_object = template_object
    @object_name = object_name
    @method_name = method_name
    @object = object
    @tag_value = tag_value
  end
・・・
end
```

https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/actionview/lib/action_view/helpers/tags/label.rb#L10

この初期化処理では`LabelBuilder`クラスが持つ`@method_name`に`method_name`をセットするのみとなっています。

さきほどの`render`メソッドに戻ると、`LabelBuilder`のインスタンスを`render_component`メソッドに渡しているのでその処理を見ていきます。

```ruby
def render_component(builder)
  builder.translation
end
```

https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/actionview/lib/action_view/helpers/tags/label.rb#L75

このメソッドでは変換メソッドを呼び出すのみなので、その呼び出し先を見てみます。

```LabelBuidler.rb
def translation
  method_and_value = @tag_value.present? ? "#{@method_name}.#{@tag_value}" : @method_name

  content ||= Translator
    .new(object, @object_name, method_and_value, scope: "helpers.label")
    .translate
  content ||= @method_name.humanize

  content
end
```

https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/actionview/lib/action_view/helpers/tags/label.rb#L18

`@tag_value`はここまでの処理で`nil`が入っているので、`method_and_value`に`@method_name`がセットされます。この`method_and_value`を使用して`Translator`クラスを初期化し、`translate`メソッドを呼び出しています。
そのため、ここまでと同様に、まずは初期化処理を見てみます。

```translator.rb
class Translator # :nodoc:
  def initialize(object, object_name, method_and_value, scope:)
    @object_name = object_name.gsub(/\[(.*)_attributes\]\[\d+\]/, '.\1')
    @method_and_value = method_and_value
    @scope = scope
    @model = object.respond_to?(:to_model) ? object.to_model : nil
  end
・・・
end
```

https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/actionview/lib/action_view/helpers/tags/translator.rb#L7

とくに変わったことはしておらず、初期化の中で`method_and_value`を`@method_and_value`にセットしています。
次に`translate`メソッドを確認してみます。

```translator.rb
  def translate
    translated_attribute = I18n.t("#{object_name}.#{method_and_value}", default: i18n_default, scope: scope).presence
    translated_attribute || human_attribute_name
  end
```

https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/actionview/lib/action_view/helpers/tags/translator.rb#L14

`I18n`で変換し、その後`presence`メソッドで変換後の値が存在するかどうかを確認しています。
`"#{object_name}.#{method_and_value}"`は`object_name`が`nil`なので`".HogeFoo"`となります。もともとの`form.label`呼び出し時に他言語は用意していないので、デフォルト値がセットされます。デフォルト値は`i18n_default`メソッドで定義されているので、そのメソッドを見てみます。

```translator.rb
def i18n_default
  if model
    key = model.model_name.i18n_key
    ["#{key}.#{method_and_value}".to_sym, ""]
  else
    ""
  end
end
```

https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/actionview/lib/action_view/helpers/tags/translator.rb#L22

`model`は`nil`となっているので、空文字が返されます。そのため`I18n`の変換は空文字となり、`presece`メソッドで`nil`が返され、`translated_attribute`にセットされます。

`I18n`の変換処理の後は、`translated_attribute || human_attribute_name`となっているので、`human_attribute_name`メソッドを見てみます。

```translator.rb
def human_attribute_name
  if model && model.class.respond_to?(:human_attribute_name)
    model.class.human_attribute_name(method_and_value)
  end
end
```

https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/actionview/lib/action_view/helpers/tags/translator.rb#L31

ActiveModelの`human_attribute_name`メソッドを使用して`I18n`の変換を行うメソッドですが、ここも`model`が`nil`なので`nil`が返されます。そのため、`TransLator`クラスではとくに何も行われずに`nil`を返していることがわかります。

`LabeBuidler`クラスの処理に戻り、次に実行される`content ||= @method_name.humanize`を見てみます。`TransLator`クラスからの戻り値`content`は`nil`だったので`@method_name.humanize`が実行されます。
この`humanize`メソッドにより、最初の文字のみ大文字となり、2単語目以降は小文字になります。

ちなみに、その後の`label_tag`メソッド以降の処理では、実際にHTMLのlabelタグを作成し、そこに値をセットする処理となります。

# おわりに
コードを追いかけて、どこで小文字になる処理が入っているか調べました。
コードを読んでいくことで、このメソッドが他言語への変換も行っていることを新しく発見できました。
この記事が誰かのお役に立てれば幸いです。