はじめに
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タグを作成し、そこに値をセットする処理となります。
おわりに
コードを追いかけて、どこで小文字になる処理が入っているか調べました。
コードを読んでいくことで、このメソッドが他言語への変換も行っていることを新しく発見できました。
この記事が誰かのお役に立てれば幸いです。