Nic Lin's Blog

喜歡在地上滾的工程師

Rails i18n 小技巧總匯

公司專案基本上都會碰到 i18n,就算沒有要做多國語系,放 i18n 的好處還有之一就是改文案快。

i18n 這個看起來完全不難的東西,實際上碰到很多需求時會發現,啊好麻煩啊,一下要插 html 一下字太多,但是在這些場景其實可以有更優雅的解法。

HTML 處理

有些狀況是希望在翻譯內的字距加上 html, 例如粗線或連結等等

zh-TW:
  privacy_tips: 請參考隱私權政策

所以會把 html 放在 yaml 中

zh-TW:
  privacy_tips: 請參考<a href="xxx.com">隱私權政策</a>

然後在用 raw 去做處理。

<%= raw(t('privacy_tips')) %>

但其實可以有更優雅的作法,直接在尾綴加上 _html 就可以幫你處理了

zh-TW:
  privacy_tips_html: 請參考<a href="xxx.com">隱私權政策</a>

好處:

  1. 在 view 裡面一看就知道這個是帶有 html 的 i18n
  2. 不需要到處 raw 來 raw 去
  3. 簡單,一致性高

長語句處理

翻譯最麻煩的情況就是一大堆文字,例如政策、說明、法律說明等等

假如有三句話分別要放三個 <p> tag,一般可能會這樣做

zh-TW:
  description_1: 第一行說明
  description_2: 第二行說明
  description_3: 第三段說明

然後在 view 裡面放上

<p> <%= t("description_1") %> </p>
<p> <%= t("description_2") %> </p>
<p> <%= t("description_3") %> </p>

但其實可以運用 YAML block literals 中的 |

zh-TW:
  description: |
    第一行說明
    第二行說明
    第三段說明

| 的功能是在每一行自動插上換行符 \n

所以我們可以用 rails console 打印出來

會發現

I18n.t("description")

# "第一行說明\n第二行說明\n第三段說明\n"

搭配 simple_format 更方便

<%= simple_format t("description") %>

會直接變成

<p>第一行文字</p>
<p>第二行文字</p>
<p>第三行文字</p>

或是用 each_line 處理

<ul>
<% t("description").each_line do |description| %>
  <li><%= description %></li>
<% end %>
</ul>

也可以直接變成

<ul>
  <li>第一行文字</li>
  <li>第二行文字</li>
  <li>第三行文字</li>
</ul>

Partial 使用語系檔案名稱

如果樣版裡面比較多靜態內容或是不同語系裡面有不同的說明,不想要一個一個翻,也可以這樣做。

app/views/pages/faq.zh-TW.html.erb
app/views/pages/faq.en.html.erb

這樣一來,在 localeen 時會自動選擇 faq.en.html.erb 樣版,而 zh-TW 時會使用 faq.zh-TW.html.erb 這個樣版

簡寫

在檔案路徑為 app/views/products/index.html.erb 時

在前綴使用 . 做開頭

<%= t('.edit_label') %>

會自動找尋 products.index.edit_label 的 i18n key

key 是對照路徑(包含 namespace),所以說這樣做要注意只要 partail 路徑變更時,語系檔沒更改就會找不到翻譯。

Scope

<% i18n_scope = "products.index" %>

<%= t('edit_label', scope: i18n_scope) %>

這樣做的話可以解決上面提到的路徑變動疑慮,搬動 partial 時不會太難,變數跟著改一下就行了。

不過 scope 除了這個場景以外,我不太建議的原因有兩個

  1. 非到不可不在 view 裡面定義變數,請都由 controller 給,如果要計算或處理都丟 helper
  2. scope 寫法除了傳 String 還可以傳 Symbol, 可預期會有各種寫法出現

以下三種寫法是一樣的

# 第一種
<%= t('edit_label', scope: "product.index") %>

# 第二種
<%= t('edit_label', scope: [:product, :index]) %>

# 第三種
<%= t('product.index.edit_label') %>

建議都請寫第三種

原因:

  1. 全域搜尋好找
  2. 階層容易懂

語系檔放在 Database

如果不想寫在 yml, 也可以放在 Database 裡

在 config/initializers/locale.rb 下加入

require 'i18n/backend/active_record'
I18n.backend = I18n::Backend::ActiveRecord.new

好處大概就是可以有一個後台隨時換字,不用動 code 這樣 XD,需求一來直接空中換機翼的概念

實際作法可以參考 i18n-active_record repo

區分多檔案

不建議把所有翻譯檔全部塞在同一個檔案裡。

只用 en.ymlzh-TW.yml 並不容易維護

更建議的作法是拆開

config/locales/admin.en.yml
config/locales/admin.zh-TW.yml

config/locales/controllers/products.en.yml
config/locales/controllers/products.zh-TW.yml

至於怎麼拆就開團隊的分類喜好了,只要能讓工程師好維護,翻譯好翻就是最適合團隊的拆法囉。

然而檔案名稱無論是什麼,只要 yml 裡面的第一層結構有對應 locale 名稱就會被載入 i18n 內。

Rails 預設只會抓 config/locales 這個階層下的檔案,再深入的資料夾是不抓的,所以要讓語系檔都載入請在 config/application.yml 加入這行

config.i18n.load_path += Dir[Rails.root.join("config", "locales", "**", "*.{rb,yml}")]

基本設定

自定義預設語系

修改 config/application.rb 的預設語系

config.i18n.default_locale = "zh-TW"

搭配 Model 使用

這樣一來噴 error 時就會直接抓 i18n,很方便顯示。

zh-TW:
  activerecord:
    attributes:
      product:
        title: "品項名稱"
        description: "描述"

內嵌變數

如果要在詞彙內嵌變數的話,可以使用 %{variable_name} 的語法

zh-TW:
  welcome: "你好 %{name}"

在 view 中使用

<%= t("welcome", name: "Nic") %>

# 你好 Nic

參考資源

comments powered by Disqus