はじめに
form.labelを使い、以下のようにキャメルケースの文字列のみ引数に渡したとき、表示された文字列が先頭のみ大文字で、2単語目以降小文字になっていました。
form.label('HogeFoo')
表示文字列:Hogefoo
Ruby on Railsの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メソッドが呼び出されます。
def label(method, text = nil, options = {}, &block)
@template.label(@object_name, method, text, objectify_options(options), &block)
end
labelメソッド呼び出し時、引数は第一引数のみなので、methodにHogeFooがセットされ、それ以外はnilや{}がセットされます。
@templateはformの初期化時(form_for呼び出し時)にFormHelperがセットされます。そのため、同じFormHelperのlabelメソッドが呼び出されます。
なお、formの初期化処理についてはここではスキップします。
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
このメソッドではTags::Labelクラスを初期化してrenderメソッドを呼び出しています。
まずTags::Labelクラスの初期化処理を見ていきます。
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
各変数に値をセット後、親クラスの初期化処理を呼び出しています。そのため、Labelクラスの親クラスであるBaseを見ます。
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
HogeFooが入ったmethod_nameは、to_sで文字列変換後、dupメソッドでコピーされ@method_nameにセットされます。
初期化メソッドでは各変数の初期化のみなので、次にrenderメソッドを見ていきます。
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
このメソッドではlabelタグの準備と描画を行っています。このメソッド内で呼び出されているadd_default_name_and_id_for_valueメソッドに関しては、name_and_idのデフォルト値をセットするのみなので、この記事ではスキップします。
その後、LabelBuilderクラスの初期化時に@method_nameを渡しているので、その初期化処理を見てみます。
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
この初期化処理ではLabelBuilderクラスが持つ@method_nameにmethod_nameをセットするのみとなっています。
さきほどのrenderメソッドに戻ると、LabelBuilderのインスタンスをrender_componentメソッドに渡しているのでその処理を見ていきます。
def render_component(builder)
builder.translation
end
このメソッドでは変換メソッドを呼び出すのみなので、その呼び出し先を見てみます。
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
@tag_valueはここまでの処理でnilが入っているので、method_and_valueに@method_nameがセットされます。このmethod_and_valueを使用してTranslatorクラスを初期化し、translateメソッドを呼び出しています。
そのため、ここまでと同様に、まずは初期化処理を見てみます。
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
とくに変わったことはしておらず、初期化の中でmethod_and_valueを@method_and_valueにセットしています。
次にtranslateメソッドを確認してみます。
def translate
translated_attribute = I18n.t("#{object_name}.#{method_and_value}", default: i18n_default, scope: scope).presence
translated_attribute || human_attribute_name
end
I18nで変換し、その後presenceメソッドで変換後の値が存在するかどうかを確認しています。
"#{object_name}.#{method_and_value}"はobject_nameがnilなので".HogeFoo"となります。もともとのform.label呼び出し時に他言語は用意していないので、デフォルト値がセットされます。デフォルト値はi18n_defaultメソッドで定義されているので、そのメソッドを見てみます。
def i18n_default
if model
key = model.model_name.i18n_key
["#{key}.#{method_and_value}".to_sym, ""]
else
""
end
end
modelはnilとなっているので、空文字が返されます。そのためI18nの変換は空文字となり、preseceメソッドでnilが返され、translated_attributeにセットされます。
I18nの変換処理の後は、translated_attribute || human_attribute_nameとなっているので、human_attribute_nameメソッドを見てみます。
def human_attribute_name
if model && model.class.respond_to?(:human_attribute_name)
model.class.human_attribute_name(method_and_value)
end
end
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タグを作成し、そこに値をセットする処理となります。
おわりに
コードを追いかけて、どこで小文字になる処理が入っているか調べました。
コードを読んでいくことで、このメソッドが他言語への変換も行っていることを新しく発見できました。
この記事が誰かのお役に立てれば幸いです。