Ruby on Railsを使ったウェブアプリケーションを開発していてテンプレートをHamlで書いています。
今までi18nせずに日本語で書いていたテンプレートをi18nしたくなりました。
Rails Internationalization (I18n) APIを参考に、viewの中の日本語の文字列を#tヘルパーを使って置き換えます。
継続的にi18nしたい
i18nは一度全部置き換えただけでは終わりません。置き換えた後もテンプレートは書き換えられていきます。
テンプレートに変更があってもi18nされた状態を維持したいです。
コードレビューでi18nされているか確認することは出来ますが人間なので見落とします。
なのでlintをかけます。
lintをかけるためにhaml_i18n_lint gemを作りました。
haml_i18n_lintを使う
haml_i18n_lintを使うとhamlテンプレート中にi18nが必要な文字列が残されているか確認できます。使い方を説明します。
インストール
% gem i haml_i18n_lint
実行
何もオプションを指定せずに実行してみます。今回はmastodonに対して実行してみます。
% ghq get tootsuite/mastodon
% cd ~/src/github.com/tootsuite/mastodon
% haml_i18n_lint
app/views/kaminari/_prev_page.html.haml:9
8: %span.prev
9: = link_to_unless current_page.first?, safe_join([fa_icon('chevron-left'), t('pagination.prev')], ' '), url, rel: 'prev', remote: remote
----------------
app/views/kaminari/_next_page.html.haml:9
8: %span.next
9: = link_to_unless current_page.last?, safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), url, rel: 'next', remote: remote
----------------
app/views/authorize_follows/_card.html.haml:7
6: %span.display-name
7: = link_to TagManager.instance.url_for(account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do
8: %strong.emojify= display_name(account)
----------------
app/views/accounts/_header.html.haml:17
16: %span= "@#{account.username}"
17: = fa_icon('lock') if account.locked?
18: .details
----------------
app/views/accounts/_og.html.haml:1
1: %meta{ property: 'og:url', content: url }/
2: %meta{ property: 'og:site_name', content: site_title }/
----------------
app/views/accounts/_og.html.haml:2
1: %meta{ property: 'og:url', content: url }/
2: %meta{ property: 'og:site_name', content: site_title }/
3: %meta{ property: 'og:title', content: [yield(:page_title).strip.presence, site_title].compact.join(' - ') }/
----------------
app/views/accounts/_og.html.haml:3
2: %meta{ property: 'og:site_name', content: site_title }/
3: %meta{ property: 'og:title', content: [yield(:page_title).strip.presence, site_title].compact.join(' - ') }/
4: %meta{ property: 'og:description', content: account.note }/
----------------
app/views/accounts/_og.html.haml:4
3: %meta{ property: 'og:title', content: [yield(:page_title).strip.presence, site_title].compact.join(' - ') }/
4: %meta{ property: 'og:description', content: account.note }/
5: %meta{ property: 'og:image', content: full_asset_url(account.avatar.url(:original)) }/
----------------
app/views/accounts/_og.html.haml:5
4: %meta{ property: 'og:description', content: account.note }/
5: %meta{ property: 'og:image', content: full_asset_url(account.avatar.url(:original)) }/
6: %meta{ property: 'og:image:width', content: '120' }/
----------------
app/views/accounts/_og.html.haml:6
5: %meta{ property: 'og:image', content: full_asset_url(account.avatar.url(:original)) }/
6: %meta{ property: 'og:image:width', content: '120' }/
7: %meta{ property: 'og:image:height', content: '120' }/
----------------
app/views/accounts/_og.html.haml:7
6: %meta{ property: 'og:image:width', content: '120' }/
7: %meta{ property: 'og:image:height', content: '120' }/
8: %meta{ property: 'twitter:card', content: 'summary' }/
----------------
app/views/accounts/_og.html.haml:8
7: %meta{ property: 'og:image:height', content: '120' }/
8: %meta{ property: 'twitter:card', content: 'summary' }/
----------------
app/views/accounts/show.html.haml:8
7:
8: %meta{ property: 'og:type', content: 'profile' }/
9: = render 'og', account: @account, url: account_url(@account, only_path: false)
----------------
app/views/accounts/show.html.haml:15
14: .h-feed
15: %data.p-name{ value: "#{@account.username} on #{site_hostname}" }/
16:
----------------
app/views/accounts/show.html.haml:28
27: - if @statuses.size == 20
28: = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), short_account_url(@account, max_id: @statuses.last.id), class: 'next', rel: 'next'
----------------
(長いので省略)
lib/templates/haml/scaffold/_form.html.haml:6: Illegal nesting: nesting within plain text is illegal. (Haml::SyntaxError)
from /home/sei/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/haml-5.0.1/lib/haml/parser.rb:301:in `plain'
from /home/sei/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/haml-5.0.1/lib/haml/parser.rb:285:in `process_line'
from /home/sei/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/haml-5.0.1/lib/haml/parser.rb:133:in `block in call'
from /home/sei/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/haml-5.0.1/lib/haml/parser.rb:119:in `loop'
from /home/sei/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/haml-5.0.1/lib/haml/parser.rb:119:in `call'
from /home/sei/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/haml_i18n_lint-0.10.0/lib/haml_i18n_lint/linter5.rb:8:in `parse'
from /home/sei/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/haml_i18n_lint-0.10.0/lib/haml_i18n_lint/linter.rb:63:in `lint'
from /home/sei/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/haml_i18n_lint-0.10.0/lib/haml_i18n_lint/runner.rb:33:in `lint'
from /home/sei/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/haml_i18n_lint-0.10.0/lib/haml_i18n_lint/runner.rb:18:in `block in run'
from /home/sei/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/haml_i18n_lint-0.10.0/lib/haml_i18n_lint/runner.rb:17:in `map'
from /home/sei/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/haml_i18n_lint-0.10.0/lib/haml_i18n_lint/runner.rb:17:in `run'
from /home/sei/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/haml_i18n_lint-0.10.0/exe/haml_i18n_lint:28:in `<top (required)>'
from /home/sei/.rbenv/versions/2.4.1/bin/haml_i18n_lint:22:in `load'
from /home/sei/.rbenv/versions/2.4.1/bin/haml_i18n_lint:22:in `<main>'
app/views/accounts/show.html.haml:15
の%data.p-name{ value: "#{@account.username} on #{site_hostname}" }/
のような、直接文字列が書かれている部分が検知されました。
一方、fa_icon
に渡している文字列や、link_to
やmetaタグの属性に渡している文字列が誤って検知されているようです。
そして残念ながらv0.9.0時点ではパースに失敗すると全体的に落ちてしまうようです。
オプション
オプションで設定ファイルを指定したり、対象ファイルを絞り込めます。
% haml_i18n_lint --help
Usage: haml_i18n_lint [OPTION]... [FILE]...
-c, --config=FILE configuration file
-f, --files=PATTERN pattern to find Haml template files, default: -f '**/*.haml'
誤検知を無視する設定を追加する
haml_i18n_lintでは-c
で設定ファイルを指定できます。
以下のような設定をhaml_i18n_lint.rb
として保存しました。
# fa_iconは引数でFontAwesomeの名前の文字列を取り、i18nの必要がないので無視する
def ignore_methods
super + %w(fa_icon)
end
# link_toのtargetオプションやmetaタグのproperty/content属性はi18nの必要がないものとして無視する
def ignore_keys
super + %w(target property content)
end
# パースに失敗するファイルを弾く
def files
super.reject { |path| path == 'lib/templates/haml/scaffold/_form.html.haml' }
end
ignore_methods: 引数をi18nする必要がないメソッド名の配列
Railsでは引数で文字列を受け取るヘルパーメソッドをよく使います。ヘルパーメソッドによっては引数に渡す文字列についてi18nが必要ない場合があります。
/ 例: render
= render 'foos/bar', bar: @bar
引数をi18nする必要がないメソッド名の配列をignore_methods
で指定すると、それらのメソッドはhaml_i18n_lintで検知されなくなります。
デフォルトの設定だとignore_methods
にはt
やrender
やRailsの一部のヘルパーなどが含まれています。
独自のヘルパーなどを追加したい場合、super
に追加したいメソッド名を足します。
ignore_keys: 値をi18nする必要がない属性のキー
Hamlでは以下のようにハッシュでid属性やclass属性を指定できますが、これらの属性はi18nする必要がありません。
%p{id: 'foo', class: 'bar'}
i18nが必要ない属性の名前をignore_keys
で指定すると、それらの属性はhaml_i18n_lintで検知されなくなります。
files: lint対象のファイル
files
ではlint対象のファイルパスの配列を返します。
デフォルトでは**/*.haml
にマッチするファイルの配列が返ってきます。
-f
オプションや引数でファイルを指定することもできますが、例えば特定のディレクトリのファイルのみ無視したい場合のように、対象ファイルの絞り込みが複雑な場合はRubyで書いたほうが楽です。
設定ファイルを指定して実行
-c
オプションで今書いた設定ファイルを指定して実行してみます。
% haml_i18n_lint -c haml_i18n_lint.rb
app/views/accounts/show.html.haml:15
14: .h-feed
15: %data.p-name{ value: "#{@account.username} on #{site_hostname}" }/
16:
----------------
(長いので省略)
パースに失敗するファイルを対象から外したので無事最後まで実行できました
fa_icon
が誤検知されなくなりました
metaタグの属性やlink_toのオプションが検知されなくなりました
件数を確認する
設定ファイルに以下を追加します。
result_count = 0
define_method(:report) do |result_set|
result_count += result_set.count
super(result_set)
end
# 終了時に総検知数を出す
at_exit do
puts "Total: #{result_count}"
end
実行してみると172箇所検知されました。(誤検知もあると思います)
% haml_i18n_lint -c haml_i18n_lint.rb | tail
38: = link_to t('admin.reports.mark_as_resolved'), admin_report_path(@report, outcome: 'resolve'), method: :put, class: 'button'
39: - elsif !@report.action_taken_by_account.nil?
----------------
app/views/admin/reports/show.html.haml:43
42: %p
43: %strong Action taken by:
44: = @report.action_taken_by_account.acct
----------------
172
i18nが必要かどうか判断する処理をカスタマイズする
設定ファイルに以下を追加します。need_i18n?(str)
メソッドを定義すると、文字列のi18nが必要かどうか判断する処理をカスタマイズできます。デフォルトだと文字列が/\p{Alpha}/
にマッチするかどうかで判断しています。
# スペースが全く無い文字列は機械向けかも?
def need_i18n?(str)
super && str.include?(' ')
end
実行してみると結構減りました。
----------------
app/views/admin/reports/show.html.haml:43
42: %p
43: %strong Action taken by:
44: = @report.action_taken_by_account.acct
----------------
85
その他の設定
examplesディレクトリ以下に上記のような設定例のほか、i18n用のYAMLを出力する例などがまとまっています。
https://github.com/okinawarb/haml_i18n_lint/tree/master/examples
まとめ
haml_i18n_lintを使うとガッとi18nしたあとも継続的にi18nできそう。