DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.

Rails 國際化 (I18n) API

Ruby I18n(簡稱國際化)這個gem隨著Ruby on Rails(從Rails 2.2開始)一起提供了一個易於使用且可擴充的框架,用於將你的應用程序翻譯成一種非英語的單一自定義語言或者提供多語言支持

「國際化」的過程通常意味著將所有字串和其他特定地區的位元(例如日期或貨幣格式)從您的應用程序中抽象出來。 「本地化」的過程是提供這些位元的翻譯和本地化格式。1

因此,在你的rails應用程序”國際化”的過程中,你必須:

  • 確保Rails提供I18n的支援
  • 告訴Rails在哪裡找到語言字典(locale dictionaries)
  • 告訴Rails如何設定、保留、切換語言

在應用程式本地化的過程,你可能會想要做以下三件事:

  • 替換或補充Rails的預設語言,例如日期、時間格式、月份名稱、Active Record 模型名稱等。
  • 將應用程式中的字串抽象成關鍵字字典,像是flash訊息、view中的靜態文字…等。
  • 把得到的字典存在某處。

本指南將引導你完成I18n的API,並包含一個從頭開始國際化Rails應用程式的教學。

閱讀完本指南後,你將會了解:

  • Ruby on Rails 中 I18n 的工作方式
  • 如何正確使用 I18n 在 RESTful 應用程式中以不同方式使用 I18n
  • 如何使用 I18n 翻譯 Active Record 錯誤或 Action Mailer 郵件主旨
  • 進一步幫你翻譯應用程式中的其他工具

備註: Ruby I18n框架為您提供了國際化/本地化Rails應用程序所需全部必要的支援。 您還可以使用各種可用的gem來添加其他功能或特色。 有關詳細信息,請參閱rails-i18n gem

1、Rails 中 I18n 的工作原理

國際化是一個複雜的問題。自然語言有許多不同之處(例如複數規則),很難提供一次性解決所有問題的工具。因此Rails I18n API的重點是:

  • 支援英語及類似語言
  • 容易訂製、擴展,以支持其他語言

為了解決這個問題, Rails框架中所有的靜態字串(像是Active Record的驗證訊息、時間和日期格式都已經國際化)。對Rails應用程序進行_本地化_意味著把這些靜態字串翻譯為所需語言。

要對你的應用程序中的內容進行本地化、存儲和更新(例如翻譯不羅格文章),詳見Translating model content部分。

1.1 I18n 的資料庫整體架構

因此,Ruby I18n gem 分成兩個部分

  • I18n框架的公開API - 一個Ruby公開方法的模塊,它定義I18n資料庫的工作方式
  • 一個預設後端(有意命名為簡單後端)實現了這些方法

作為用戶,我們應該只訪問I18n模塊上的公開方法,但是了解後端的能力是有幫助的。

備註: 可以將預設的簡單後端與功能更強大的後端進行交換,該後端將在關係數據庫、GetText字典或類似位置中存儲翻譯數據。請參閱下面的[使用不同的後端](#使用不同的後端)部分。

1.2 公開的I18n API

I18n最重要的兩個方法:

translate # 查找文本翻譯
localize  # 把日期和時間的物件轉成本地格式

這兩個方法的別名稱作 #t 和 #l,你可以像下面這樣使用它們:

I18n.t 'store.title'
I18n.l Time.now

對於下列屬性,I18n還提供屬性的讀取值、設定值的方法

load_path                 # 自定義翻譯文件的路徑 
locale                    # 獲取或設定當前區域
default_locale            # 獲取或設定默認區域
available_locales         # 可供應用的允許區域
enforce_available_locales # 強制執行允許區域(真或假)
exception_handler         # 使用其他異常處理程序
backend                   # 使用其他後端

因此,在下一個章節,讓我們來對一個簡單的Rails應用程式進行國際化設定!

2、幫 Rails 應用程式設定國際化

要幫Rails應用程式啟用I18n支援,需要執行以下幾個步驟:

2.1 設置I18n的模組

根據慣例優於設定的哲學,Rails I18n提供合理的預設翻譯字串,當需要不同的翻譯字串時,可以覆蓋預設值。

Rails會自動將 config/locales 目錄中的所有 .rb.yml 文件加入 翻譯載入路徑

這個目錄中的預設 en.yml 語系包含一對示例翻譯字串:

en:
  hello: "Hello world"

這代表,在 :en 語系, 鍵值 hello 會對應到 Hello world 這個字串上。在Rails中,字串都是以這種方式國際化的,例如,Active Model的驗證訊息在activemodel/lib/active_model/locale/en.yml文件中,時間和日期格式在activesupport/lib/active_support/locale/en.yml文件中。我們可以使用YAML或標準的Ruby Hash,把翻譯訊息存在預設的簡單後端中。

I18n資料庫預設的語系是用英文,例如,如果沒有設置其他的語系,那就使用 :en 語系來查找翻譯。

備註: I18n資料庫對於選取區域設定的鍵時,採取了務實的方法(在一些討論後),也就是僅包含語言的部分,例如 :enpl ,而不是傳統上使用語言和區域兩部分,像是: en-USen-GB 。傳統上通常都會把”語言”和”區域設置”或”方言”分開,很多國際化的應用反而是只使用”語言”這個元素,例如:cs:th:es(分別為捷克語、泰語、西班牙語)。但是不同語言群體間,也存在區域差異,這很重要。例如,在:"en-US"語系中,會有$作為貨幣符號,而在:"en-GB"中則會有£作為貨幣符號。因此,如果需要的話,你也可以使用傳統的方式設定語系,例如,在 :en-GB字典中,提供完整的 "English - United Kingdom" 區域。

Rails會自動加載 翻譯載入路徑 (I18n.load_path),它是一個保存有翻譯文件路徑的陣列,通過配置翻譯文件加載路徑,我們可以自定義翻譯文件的目錄結構和文件命名規則。

備註: I18n採用了延遲加載技術,相關翻譯訊息僅在第一次查找時加載。可以根據需求,隨時替換掉預設的後端設定。

你可以在 config/application.rb 中更改默認語言環境,並配置翻譯加載路徑,如下所示:

config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}')]
config.i18n.default_locale = :de

在查找翻譯文件之前,必須先指定翻譯文件的加載路徑,如果想要改變預設的語系的話,應該要使用 initializer 來做修改,而不是 config/application.rb

# config/initializers/locale.rb

# Where the I18n library should search for translation files
I18n.load_path += Dir[Rails.root.join('lib', 'locale', '*.{rb,yml}')]

# Permitted locales available for the application
I18n.available_locales = [:en, :pt]

# Set default locale to something other than :en
I18n.default_locale = :pt

直接把他添加到 I18n.load_path,而不是應用程式配置的 I18n 中,這樣 不會 覆蓋另外新安裝的 gems 的翻譯。

2.2 跨請求管理語系設定

本地化應用程式有時候會需要支援多個語言環境。為了實現這一目標,應該在每個請求開始時,設定語言環境,這樣在請求的整個生命週期中,會根據指定語系對所有字串進行翻譯。

除非使用 I18n.locale= 或是 I18n.with_locale,否則將使用默認語言環境進行所有翻譯。

如果沒有在 controller 中一致的設定, I18n.locale 可能會漏掉同一個線程中的後續請求,例如,在一個 POST 請求中,執行 I18n.locale = :es ,會對以後所有沒有設定語系的 controller 請求產生影響,不過這個只有在特定的線程中。因此,你可以使用 I18n.locale = 而不是 I18n.with_locale ,這樣就不會有洩漏問題。

我們可以在 ApplicationController 中使用 around_action 來設定語系:

around_action :switch_locale

def switch_locale(&action)
  locale = params[:locale] || I18n.default_locale
  I18n.with_locale(locale, &action)
end

這個範例說明了使用URL查詢參數來設定語系的語系的情況(e.g. http://example.com/books?locale=pt),通過這種設定, http://localhost:3000?locale=pt 渲染葡萄牙語的本地化,而 http://localhost:3000?locale=de 加載德語的本地化。

可以使用許多不同的方法,來設定語系。

2.2.1 根據域名設置語系

第一種方式,根據域名來設定語系。例如,通過 www.example.com 加載英語(預設)語系設定,通過 www.example.es 西班牙語系設定。也就是根據頂級域名設定語系。這種方式有幾種優點:

  • URL網址有一部分是語系設定很常見
  • 用戶可以看URL就知道這個頁面使用哪種語言
  • 在Rails中實現很簡單
  • 搜尋引擎偏愛這種把不同語言內容放在不同域名上的做法

你可以在 ApplicationController 這設定:

around_action :switch_locale

def switch_locale(&action)
  locale = extract_locale_from_tld || I18n.default_locale
  I18n.with_locale(locale, &action)
end

# Get locale from top-level domain or return +nil+ if such locale is not available
# You have to put something like:
#   127.0.0.1 application.com
#   127.0.0.1 application.it
#   127.0.0.1 application.pl
# in your /etc/hosts file to try this out locally
def extract_locale_from_tld
  parsed_locale = request.host.split('.').last
  I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil
end

我們也可以通過類似的方式,根據子域名來設定語系:

# Get locale code from request subdomain (like http://it.application.local:3000)
# You have to put something like:
#   127.0.0.1 gr.application.local
# in your /etc/hosts file to try this out locally
def extract_locale_from_subdomain
  parsed_locale = request.subdomains.first
  I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil
end

如果你想要在網站的menu上,增加切換語系的連結,可以使用以下代碼:

link_to("Deutsch", "#{APP_CONFIG[:deutsch_website_url]}#{request.env['PATH_INFO']}")

其中 APP_CONFIG[:deutsch_website_url] 的值類似於 http://www.application.de

儘管這個解決方案具有上面所有的優點,不過通過域名來設定不同版本的在地化語言(語言版本),通常不是我們的首選。在其他可以選擇的方案中,在URL參數(或請求路徑)中包含語系設定是最常見的。

2.2.2 根據URL參數設定語系

語系設定最常見的方式,是將其包含在URL參數之中,例如:在前文第一個範例中 _around_action_ 方法呼叫了 I18n.with_locale(params[:locale], &action) 。此時,我們會使用 www.example.com/books?locale=jawww.example.com/ja/books 這樣的網址。

這種方法與從網域名稱設定語系有幾點相似的優點,尤其是RESTful的命名風格,符合當前網站製作的潮流。不過採用這種方式,所需要的工作量會大一點。

params 獲取語系並做相對應的設定並不難,只要把語系設定包含在URL中,並通過請求傳遞即可,不過,沒有人會想要在生成每個URL地址時,添加語系設定,例如:link_to(books_url(locale: I18n.locale))

Rails 的 ApplicationController#default_url_options 方法,提供 “集中修改URL動態生成規則”的功能,正好可以解決上面那個問題:我們可以設定 url_for 及相關輔助方法與預設行為(通過覆蓋default_url_options方法)。

我們可以在 ApplicationController 中添加下面的程式碼:

# app/controllers/application_controller.rb
def default_url_options
  { locale: I18n.locale }
end

這樣每個依賴 url_for 的方法(像是路徑輔助方法:root_path、root_url,資源路徑輔助方法:books_path、books_url….等等)將會自動在查詢字串中添加語系設定,例如:http://localhost:3001/?locale=ja

雖然照上面那樣設定,就可以運行了,但是當語言設定”掛”在應用程式中,每個URL網址的末尾時,它會影響URL的可讀性。此外,從架構角度來看,語系設定的層級,應該高於URL地址中,除域名之外的其他組成部分,這一點也應該通過URL網址自身顯現出來。

你如果想使用:http://www.example.com/en/books(英文語系)和http://www.example.com/nl/books(荷蘭語系),這樣的URL網址,我們可以使用前面有說到的,覆蓋default_url_options 方法的方式,通過 scope方法來設定路徑:

# config/routes.rb
scope "/:locale" do
  resources :books
end

現在,當我們呼叫 books_path 方法時,你應該會得到 /en/books (默認語系設定),像 http://localhost:3001/nl/books 這樣的網址會加載荷蘭語系設定,之後呼叫 books_path 方法會返回 /nl/books (因為語系被改過了)。

警告,由於default_url_options方法的返回值是根據請求分別緩存的,因此無法通過循環調用輔助方法來生成URL地址中的語系設定,也就是說,無法在每次迭代中設定相應的 I18n.locale。正確的做法是,保持 I18n.locale 不變,像輔助方法顯示傳遞 :locale選項,或者編輯 request.original_fullpath

如果你不想在路徑中強制使用語系設定,我們可以使用可選的路徑作用域(用括號表示),像下面這樣:

# config/routes.rb
scope "(:locale)", locale: /en|nl/ do
  resources :books
end

透過這種方式,訪問不帶語系設定的 http://localhost:3001/books URL地址時,就不會有路徑錯誤了,這樣我們就可以在不指定語系設定的情況下,使用預設的語系設定。

當然,我們需要特別注意應用的根路徑(root_path),通常是主頁(homepage)、儀表板(dashboard),像root to: “books#index” 這樣不考慮語系設定的路徑聲明,會導致 http://localhost:3001/nl 無法正常拜訪(儘管只有一個跟路徑,看起來並沒有錯)。

因此,我們可以像下面這樣映射URL地址:

# config/routes.rb
get '/:locale' => 'dashboard#index'

要特別注意路徑的設定順序,要避免這條路徑覆蓋掉其他路徑。(你可以把這一條路徑加到root :to路徑設定前)

備註: 有一些gem可以幫助簡化路徑設定,像是routing_filter, route_translator

2.2.3 在用戶偏好設定語系

一個擁有認證用戶的應用程式可以允許用戶通過應用程式的介面設定語言偏好。通過這種方法,用戶選擇的語言偏好將被持久化到數據庫中,並用於對該用戶的認證要求設定語言。

around_action :switch_locale

def switch_locale(&action)
  locale = current_user.try(:locale) || I18n.default_locale
  I18n.with_locale(locale, &action)
end

2.2.4 使用隱式語系設定

如果今天沒有用顯示的語系設定(透過上面的幾種方法),應用程式會嘗試推斷你所在的區域。

2.2.4.1 根據HTTP頭部推斷語系設定

Accept-Language 頭部會指定回應請求時使用的語言,瀏覽器根據用戶的語言偏好設定這個HTTP頭部,這是推斷語系設定的首選。

下面是使用 Accept-Language HTTP 頭部的一个簡單範例:

def switch_locale(&action)
  logger.debug "* Accept-Language: #{request.env['HTTP_ACCEPT_LANGUAGE']}"
  locale = extract_locale_from_accept_language_header
  logger.debug "* Locale set to '#{locale}'"
  I18n.with_locale(locale, &action)
end

private
  def extract_locale_from_accept_language_header
    request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first
  end

實際上,我們通常會使用更可靠的代碼,Iain Hecker 開發的 http_accept_language 或 Ryan Tomayko 開發的 locale Rack 中介軟體,就提供了更好的解决方案。

2.2.4.2 根據IP位置來判斷語系

我們可以通過客戶端請求的IP地址,來推斷客戶端所在的地理位置,進而推斷他的語系設定。GeoLite2 Country這樣的服務,或 geocoder 這個gem可以實現這個功能。

一般來說,這種方式遠不如使用HTTP頭部可靠,因此並不適用於大多數的WEB應用程式。

警告: 我們可能會認為,可以把語系設定存在session或cookie中,但是,我們不能這樣做,語系設定應該要是透明的,並作為URL網址的一部分,這樣我們就不會打破用戶正常的預期情況:如果我們今天發送一個URL網址給朋友,他應該看到和我們一樣的頁面和內容,這就是所謂的REST規則,關於REST規則的更多介紹,請看 Stefan Tilkov 系列文章,之後將討論這個規則的例外情況。

3、國際化與本地化

現在我們已經完成對Rails應用程式I18n的初始化設定,並在不同的請求中使用語系設定。

接下來,我們將要通過抽象本地化相關元素,完成應用的國際化,最後,通過為這些抽象元素提供必要的翻譯,完成應用的本地化:

下面是一個例子:

# config/routes.rb
Rails.application.routes.draw do
  root to: "home#index"
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base

  around_action :switch_locale

  def switch_locale(&action)
    locale = params[:locale] || I18n.default_locale
    I18n.with_locale(locale, &action)
  end
end
# app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
    flash[:notice] = "Hello Flash"
  end
end
<!-- app/views/home/index.html.erb -->
<h1>Hello World</h1>
<p><%= flash[:notice] %></p>

rails i18n demo untranslated

3.1 抽象本地化程式碼

在我們的程式碼中,有兩個英文字串(“Hello Flash” 和 “Hello World”),他們在回應用戶的請求時顯示。為了國際化這部分的程式碼,需要使用Rails提供的 #t 輔助方法來代替這兩個字串,同時為每個字串選擇合適的鍵:

# app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
    flash[:notice] = t(:hello_flash)
  end
end
<!-- app/views/home/index.html.erb -->
<h1><%= t :hello_world %></h1>
<p><%= flash[:notice] %></p>

現在,當index畫面被選染出來時,會顯示錯誤訊息,告訴我們缺少 :hello_world:hello_flash 這兩個鍵的翻譯。

rails i18n demo translation missing

備註: Rails為 view 添加了 t(translate)辅助方法,從而避免了反覆使用 I18n.t 這麼長的寫法。此外,t輔助方法還能捕獲缺少翻譯的錯誤,把生成的錯誤訊息放在 元素裡面。

3.2 為國際化字串提供翻譯

接著,我們把剛剛缺少的翻譯,加進翻譯字典文件中:

# config/locales/en.yml
en:
  hello_world: Hello world!
  hello_flash: Hello flash!
# config/locales/pirate.yml
pirate:
  hello_world: Ahoy World
  hello_flash: Ahoy Flash

因為我們沒有修改 default_locale ,翻譯會使用 :en 語系設定,響應請求時生成的 view 會顯示英文字串:

rails i18n demo translated to English

如果我們把通過的網址語系設定改成pirate語系(http://localhost:3000?locale=pirate),響應請求生成的 view 就會是海盜黑話:

rails i18n demo translated to pirate

備註: 添加新的語系文件設定後,你需要重新啟動伺服器

要想把翻譯存在SimpleStore中,我們可以使用YAML (.yml) 或是 纯 Ruby(.rb)文件,大多數 Rails 開發者會優先使用YAML,不過YAML有一個很大的缺點,他對空格和特殊字符非常敏感,因此有可能出現應用無法正確加載字典的情況,而Ruby文件如果有錯誤,在第一次加載的時候就會崩潰,因此我們很容易就能找到問題(如果在使用 YAML 字典時,遇到了”奇怪的問題”,可以嘗試把字典的相關部分放入 Ruby 文件中。)

如果你的翻譯存在 YAML 檔案裡面,有一些字符需要被 escaped。像是:

  • true, on, yes
  • false, off, no

以下範例:

# config/locales/en.yml
en:
  success:
    'true':  'True!'
    'on':    'On!'
    'false': 'False!'
  failure:
    true:    'True!'
    off:     'Off!'
    false:   'False!'
I18n.t 'success.true'  # => 'True!'
I18n.t 'success.on'    # => 'On!'
I18n.t 'success.false' # => 'False!'
I18n.t 'failure.false' # => Translation Missing
I18n.t 'failure.off'   # => Translation Missing
I18n.t 'failure.true'  # => Translation Missing

3.3 把變數傳給翻譯

要成功地將應用程式國際化,一個重要的考慮因素是在抽象本地化的代碼時避免對語法規則做出不正確的假設。 在一個位置中看似基本的語法規則可能在另一個位置中並不成立。

以下範例展示了不正確的抽象化,在此假設了翻譯的不同部分的順序。請注意,Rails 提供了 number_to_currency 幫助函式來處理以下情況。

<!-- app/views/products/show.html.erb -->
<%= "#{t('currency')}#{@product.price}" %>
# config/locales/en.yml
en:
  currency: "$"
# config/locales/es.yml
es:
  currency: "€"

如果產品的價格是10,那麼西班牙語的正確翻譯是 “10 €” 而不是 “€10”,但上面的抽象化並不能正確處理這種情況。

為了創建正確的抽象化,I18n gem提供的變數插值(variable interpolation)的功能,他允許我們在翻譯定義(translation definition)中使用變數,並把這些變數的值傳給翻譯方法。

下面是一個正確抽象的範例:

<!-- app/views/products/show.html.erb -->
<%= t('product_price', price: @product.price) %>
# config/locales/en.yml
en:
  product_price: "$%{price}"
# config/locales/es.yml
es:
  product_price: "%{price} €"

所有語法和標點都由翻譯定義自己決定,所以抽象化可以給出正確的翻譯。

備註: defaultscope 是保留關鍵字,不能做變數名,如果誤用,會噴出這個錯誤 I18n::ReservedInterpolationKey,如果沒有把翻譯所需的變數差值傳遞給 #translate 方法,Rails會噴出 I18n::MissingInterpolationArgument 錯誤。

3.4 添加時間、日期格式

現在,我們要給 view 加上時間戳記,以便展示 日期/時間的本地化功能,要想本地化時間格式,可以把時間對象傳遞給 I18n.l 方法,或(最好)使用 #l 輔助方法,可以通過 :format 選項指定時間格式(默認情況下使用 :default 格式)。

<!-- app/views/home/index.html.erb -->
<h1><%= t :hello_world %></h1>
<p><%= flash[:notice] %></p>
<p><%= l Time.now, format: :short %></p>

然後在 pirate 翻譯文件中添加時間格式(Rails 默認使用的英文翻譯文件已經包含時間格式)

# config/locales/pirate.yml
pirate:
  time:
    formats:
      short: "arrrround %H'ish"

得到的結果如下:

rails i18n demo localized time to pirate

提示: 現在,我們可能需要添加一些日期/時間格式,這樣 I18n 後端才能如預期工作(至少應該為 pirate 語系設定添加日期/時間格式)。當然,很可能已經有人 通過翻譯 Rails 相關語系設定的默認值,完成這些工作,Github上的 Rails-i18n repository提供了各種本地化文件的存檔,把這些本地化文件放在 config/locales/ 資料夾中就可以正常使用。

3.5 其他區域的變形規則

Rails 允許我們為英語之外的語系定義變形規則 (像是單複數轉換規則),在 config/initializers/inflections.rb 文件中,我們可以為多個語系定義規則,這個初始化腳本包含為英語指定附加規則的例子,我們可以參考這些例子的格式為其他語系定義規則。

3.6 本地化 views

假設應用程式中包含 BooksController,你的 index 動作默認會渲染 app/views/books/index.html.erb 模板,如果我們在同一個資料夾中創建了包含本地化變量的 index.es.html.erb 模板,當語系設定為 :es 時,index動作就會渲染這個模板,而當語系設定為默認語系時,index動作會渲染通用的 index.html.erb 模板。(在 Rails 的未來模板中,本地化這種 自動化魔術,又可能被應用於 public 資料夾中的資源)。

本地化 view 功能很有用,像是如果我們有大量的靜態內容,就可以使用本地化view,從而避免把所有東西都放進 YAML 或 Ruby 字典裡,但要記住,一旦我們需要修改模板,就必須對每個模板文件進行逐一修改。

3.7 語系設定文件的管理

當我們使用 I18n 資料庫自帶的 SimpleStore 時,字典儲存在磁碟上的純文本文件中,對於每個區域,把應用的各部分翻譯都放在一個文件中,可能會造成管理上的困難,因此,把每個語系的翻譯放在多個文件中,分層進行管理是更好的選擇。

例如,我們可以像我們可以像下面這樣管理 config/locales 資料夾:

|-defaults
|---es.yml
|---en.yml
|-models
|---book
|-----es.yml
|-----en.yml
|-views
|---defaults
|-----es.yml
|-----en.yml
|---books
|-----es.yml
|-----en.yml
|---users
|-----es.yml
|-----en.yml
|---navigation
|-----es.yml
|-----en.yml

這樣,我們可以把模型、屬性名從同個 view 中的文本分離,同時還能使用 “預設值”(像是日期、時間格式),I18n資料庫的不同後端,提供了不同的分離方式。

備註: Rails 默認的語系設置加載機制,無法自動加載上面例子中位於千套文件中的語系設定文件,因此,我們需要進行顯示設定:

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

4、I18n API功能概述

現在我們對I18n資料庫有更深刻的了解,知道如何國際化Rails的應用程式,在下面的章節中,我們將更深入的了解相關功能。

這幾個章節,將展示使用 I18n.translate 方法,以及 translate view helper method(注意 view 輔助方法的提供的副加功能)。

提供的功能如下:

  • 查找翻譯
  • 把數據插入翻譯中
  • 複數的翻譯
  • 使用安全HTML翻譯(只針對view輔助方法)
  • 本地化日期、數字、貨幣…等

4.1 查找翻譯

4.1.1 基本查找、作用域和嵌套鍵

Rails 通過鍵來查找翻譯,其中鍵可以是符號或字串,這兩種鍵是等價的,例如:

I18n.t :message
I18n.t 'message'

translate 方法接受 :scope 選項,選項的值可以包含一個或多個副加鍵,用於指定翻譯鍵 (translation key) 的 “命名空間”或作用域:

I18n.t :record_invalid, scope: [:activerecord, :errors, :messages]

上述程式碼會在 Active Record 錯誤訊息中查找 :record_invalid 訊息。

此外,我們還可以用點號分隔的鍵來指定翻譯鍵或作用域:

I18n.translate "activerecord.errors.messages.record_invalid"

因此,下面的呼叫是無效的:

I18n.t 'activerecord.errors.messages.record_invalid'
I18n.t 'errors.messages.record_invalid', scope: :activerecord
I18n.t :record_invalid, scope: 'activerecord.errors.messages'
I18n.t :record_invalid, scope: [:activerecord, :errors, :messages]

4.1.2 預設值

如果指定了 :default 選項,在缺少翻譯的情况下,就會返回該選項的值:

I18n.t :missing, default: 'Not here'
# => 'Not here'

如果 :default 值是符號,這個值會被當作鍵並被翻譯,我們可以為 :default 選項指定多個值,第一個被成功翻譯的鍵或字串將被作為返回值。

例如,下面的程式碼首先嘗試翻譯 :missing 鍵,然後是 :also_missing 鍵,如果兩次翻譯都不能得到結果,最後會返回 “Not here” 字串。

I18n.t :missing, default: [:also_missing, 'Not here']
# => 'Not here'

4.1.3 批量和命名空間查找

如果想要一次查找多個翻譯,我們可以傳遞鍵的陣列作為參數:

I18n.t [:odd, :even], scope: 'errors.messages'
# => ["must be odd", "must be even"]

另外,鍵可以轉換為一組翻譯的(可能是嵌套的)Hash,例如,下面的程式碼可以生成所有 Active Record 錯誤訊息的Hash:

I18n.t 'errors.messages'
# => {:inclusion=>"is not included in the list", :exclusion=> ... }

如果你想對一個大量的翻譯Hash進行內插,你需要將 deep_interpolation: true 作為參數傳遞。當你有以下Hash時:

en:
  welcome:
    title: "Welcome!"
    content: "Welcome to the %{app_name}"

如果沒有設定,嵌套內差將會被忽略:

I18n.t 'welcome', app_name: 'book store'
# => {:title=>"Welcome!", :content=>"Welcome to the %{app_name}"}

I18n.t 'welcome', deep_interpolation: true, app_name: 'book store'
# => {:title=>"Welcome!", :content=>"Welcome to the book store"}

4.1.4 懶惰查找

Rails 實現了一種在 views 中查找語系設定的方便方法,如果有下述的字典:

es:
  books:
    index:
      title: "Título"

我們可以像下面這樣,在 app/views/books/index.html.erb 模板中,查找 books.index.title 的值:

<%= t '.title' %>

備註: 只有 translate view輔助方法,支援根據片段自動補完翻譯作用域的功能。

懶惰查找一樣可以被用在 controllers 中:

en:
  books:
    create:
      success: Book created!

用於設定 flash 訊息:

class BooksController < ApplicationController
  def create
    # ...
    redirect_to books_url, notice: t('.success')
  end
end

4.2 複數轉換

在英語中,一個字串只有一種單數形式和一種複數形式,例如,”1 message” 和 “2 messages”,其他語言(阿拉伯語日語俄語等)則具有不同的語法,有更多或更少的複數形式,因此,I18n API提供了靈活的複數轉換功能。

:count 插值變數具有特殊作用,既可以把它插入翻譯,又可以根據後端定義的複數型規則選擇複數型翻譯,預設情況下,只會應用英語的複數型規則:

I18n.backend.store_translations :en, inbox: {
  zero: 'no messages', # optional
  one: 'one message',
  other: '%{count} messages'
}
I18n.translate :inbox, count: 2
# => '2 messages'

I18n.translate :inbox, count: 1
# => 'one message'

I18n.translate :inbox, count: 0
# => 'no messages'

:en 語系設定的復轉轉換算法非常簡單:

lookup_key = :zero if count == 0 && entry.has_key?(:zero)
lookup_key ||= count == 1 ? :one : :other
entry[lookup_key]

根據以下程式碼 :one 表示為單數,:other 則被視為複數,如果 count 是零, 並且存在 :zero 選項,則他將用於取代 :other

如果查找鍵沒能返回可轉換為複數形式的Hash,就會導致 I18n::InvalidPluralizationData 的例外訊息。

4.2.1 語系設定規則

I18n的GEM提供一個複數的後端(Pluralization backend),可以用於啟用特定語系的規則,將他包含到 Simple 後端中,然後將本地化的複數型演算法添加到翻譯儲存中,當作 i18n.plural.rule

I18n::Backend::Simple.include(I18n::Backend::Pluralization)
I18n.backend.store_translations :pt, i18n: { plural: { rule: lambda { |n| [0, 1].include?(n) ? :one : :other } } }
I18n.backend.store_translations :pt, apples: { one: 'one or none', other: 'more than one' }

I18n.t :apples, count: 0, locale: :pt
# => 'one or none'

另外,可以使用獨立的gemrails-i18n,來提供更完整的特定語言語系的複數型規則。

4.3 語系設定和傳遞

語系設定可以偽全局的設定為 I18n.locale (使用 Thread.current,例如 Time.zone),也可以作為選項傳遞給 #translate#localize 方法。

如果我們沒有傳遞語系設定,Rails就會使用使用 I18n.locale

I18n.locale = :de
I18n.t :foo
I18n.l Time.now

顯示傳遞語系設定:

I18n.t :foo, locale: :de
I18n.l Time.now, locale: :de

I18n.locale 預設值是 I18n.default_locale,而 I18n.default_locale 預設值是 :en,可以像下面這樣設定預設語系:

I18n.default_locale = :de

4.4 使用安全HTML翻譯

帶有 “_html” 後綴的鍵名為 “html” 的鍵被認為是HTML安全,當我們在 view 中使用這些鍵時,HTML不會被轉譯。

# config/locales/en.yml
en:
  welcome: <b>welcome!</b>
  hello_html: <b>hello!</b>
  title:
    html: <b>title!</b>
<!-- app/views/home/index.html.erb -->
<div><%= t('welcome') %></div>
<div><%= raw t('welcome') %></div>
<div><%= t('hello_html') %></div>
<div><%= t('title.html') %></div>

不過插值變數是會被轉譯的,例如,下面:

en:
  welcome_html: "<b>Welcome %{username}!</b>"

我們可以安全的傳遞用戶設定的用戶名:

<%# This is safe, it is going to be escaped if needed. %>
<%= t('welcome_html', username: @current_user.username) %>

另一方面,安全字串將會逐字插入。

備註: 只有使用 translate (或 t) 輔助方法中才能自動將文字轉換為 HTML 安全的文字,這在 viewcontroller 中都適用。

i18n demo HTML safe

4.5 Active Record Models的翻譯

我們可以使用 Model.model_name.humanModel.human_attribute_name(attribute) 方法,來明顯的查找model、屬性名的翻譯

例如當我們添加下面的翻譯:

en:
  activerecord:
    models:
      user: Customer
    attributes:
      user:
        login: "Handle"
      # will translate User attribute "login" as "Handle"

User.model_name.human 會返回 “Customer”,而 User.human_attribute_name("login") 會返回 “Handle”。

我們可以像下面這樣為model名添加複數形式:

en:
  activerecord:
    models:
      user:
        one: Customer
        other: Customers

然後 User.model_name.human(count: 2) 會返回 “Customers”,而 count: 1User.model_name.human 將會返回 “Customer”

要想訪問model的嵌套屬性,我們可以在翻譯文件的model層級中嵌套使用 model/attribute

en:
  activerecord:
    attributes:
      user/role:
        admin: "Admin"
        contributor: "Contributor"

這時 User.human_attribute_name("role.admin") 會返回 “Admin”。

備註: 如果我們使用的的類別包含了 ActiveModel,而沒有繼承自 ActiveRecord::Base,我們就應該用 activemodel 取代上述例子中,鍵路徑中的 activerecord

4.5.1 錯誤訊息的作用域

Active Record 驗證的錯誤訊息翻譯起來很容易,Active Record 提供了一些用於放置消息翻譯的命名空間,以便為不同的模型、屬性和驗證提供不同的消息和翻譯,當然 Active Record 也考慮到了單表繼承問題。

這就給了根據應用需求靈活調整訊息,提供了非常強大的工具。

假設 User model 對 name 屬性進行驗證:

class User < ApplicationRecord
  validates :name, presence: true
end

此時,錯誤訊息的鍵是 :blank,Active Record 會在命名空間中查找這個鍵:

activerecord.errors.models.[model_name].attributes.[attribute_name]
activerecord.errors.models.[model_name]
activerecord.errors.messages
errors.attributes.[attribute_name]
errors.messages

因此,在本範例中, Active Record 會按順序查找下列的鍵,並返回第一個結果:

activerecord.errors.models.user.attributes.name.blank
activerecord.errors.models.user.blank
activerecord.errors.messages.blank
errors.attributes.name.blank
errors.messages.blank

如果 model 使用了繼承,Active Record還會在繼承鏈中查找訊息。
例如,對於繼承自 User model 的 Admin model:

class Admin < User
  validates :name, presence: true
end

Active Record 會根據下列順序查找訊息:

activerecord.errors.models.admin.attributes.name.blank
activerecord.errors.models.admin.blank
activerecord.errors.models.user.attributes.name.blank
activerecord.errors.models.user.blank
activerecord.errors.messages.blank
errors.attributes.name.blank
errors.messages.blank

這樣,我們就可以在 model 繼承鏈的不同位置,以及屬性、model或預設作用域中,為各種錯誤訊息提供特殊翻譯。

4.5.2 錯誤訊息的插值

翻譯後的 model 名、屬性名、以及,始終可以用於插值。

因此,舉例來說,我們可以用 "Please fill in your %{attribute}",這樣的屬性名來代替預設的 cannot be blank 錯誤訊息。

  • count 方法可以用時,可根據需要用於複數轉換:
validation with option message interpolation
confirmation - :confirmation attribute
acceptance - :accepted -
presence - :blank -
absence - :present -
length :within, :in :too_short count
length :within, :in :too_long count
length :is :wrong_length count
length :minimum :too_short count
length :maximum :too_long count
uniqueness - :taken -
format - :invalid -
inclusion - :inclusion -
exclusion - :exclusion -
associated - :invalid -
non-optional association - :required -
numericality - :not_a_number -
numericality :greater_than :greater_than count
numericality :greater_than_or_equal_to :greater_than_or_equal_to count
numericality :equal_to :equal_to count
numericality :less_than :less_than count
numericality :less_than_or_equal_to :less_than_or_equal_to count
numericality :other_than :other_than count
numericality :only_integer :not_an_integer -
numericality :in :in count
numericality :odd :odd -
numericality :even :even -
comparison :greater_than :greater_than count
comparison :greater_than_or_equal_to :greater_than_or_equal_to count
comparison :equal_to :equal_to count
comparison :less_than :less_than count
comparison :less_than_or_equal_to :less_than_or_equal_to count
comparison :other_than :other_than count

4.6 Action Mailer 電子信件主題的翻譯

如果你沒有將主題傳遞給 mail 方法, Action Mailer 會嘗試在翻譯中找到他,會使用 <mailer_scope>.<action_name>.subject 這種模式來查找。

# user_mailer.rb
class UserMailer < ActionMailer::Base
  def welcome(user)
    #...
  end
end
en:
  user_mailer:
    welcome:
      subject: "Welcome to Rails Guides!"

要將參數傳遞給插值法,請在 mailer 上使用 default_i18n_subject 方法。

# user_mailer.rb
class UserMailer < ActionMailer::Base
  def welcome(user)
    mail(to: user.email, subject: default_i18n_subject(user: user.name))
  end
end
en:
  user_mailer:
    welcome:
      subject: "%{user}, welcome to Rails Guides!"

4.7 提供 I18n 支持的其他內置方法概述

在 Rails 中,我們會使用固定字串,和其他本地化元素,例如,在一些輔助方法中使用的格式字串和其他格式訊息。本章節提供的簡要的概述。

4.7.1 Action View 輔助方法

  • distance_of_time_in_words 輔助方法翻譯並以複數形式顯示結果,同時插入秒、分鐘、小時的數值。 看更多 datetime.distance_in_words

  • datetime_selectselect_month 輔助方法使用翻譯後的月份名稱來填充生成的 select 標籤。更多介紹請看date.month_namesdatetime_select 輔助方法還會從 date.order 中查找 order 選項 (除非我們顯示傳遞了 order 選項)。如果可能,所有日期選擇輔助方法在翻譯提示訊息時,都會使用 datetime.prompts 作用域中的翻譯。

  • number_to_currency, number_with_precision, number_to_percentage, number_with_delimiter, 和 number_to_human_size 輔助方法使用 number 作用域中的數字格式設定。

4.7.2 Active Model 方法

  • model_name.humanhuman_attribute_name 方法會使用 activerecord.models 作用域中可用的 model、屬性名的翻譯,像錯誤訊息的作用域中介紹的那樣,這兩個方法也支持繼承類別名的翻譯(例如,用於STI)。

  • ActiveModel::Errors#generate_message 方法 (在Active Model 驗證時使用,也可以手動使用) 會使用上面介紹的 model_name.human and human_attribute_name 方法。像錯誤訊息的作用域中介紹的那樣,這個方法也會翻譯錯誤訊息,並支持繼承的類別名的翻譯。

  • ActiveModel::Error#full_messageActiveModel::Errors#full_messages 方法,使用分隔符號把屬性名稱添加到錯誤訊息的開頭,然後在 errors.format 中查找(預設格式為 "%{attribute} %{message}")) 。要自定義預設格式,請在應用程式的語系設定文件中覆蓋他,要自訂義每個model或是每個屬性的格式,請看config.active_model.i18n_customize_full_message

4.7.3 Active Support 方法

  • Array#to_sentence 方法使用 support.array 作用域中的格式設定。

5、如何儲存自定義翻譯

Active Support 自帶的 Simple backend,允許我們用純 Ruby 或 YAML 格式儲存翻譯。2
例如,通過 Ruby Hash 儲存翻譯的範例如下:

{
  pt: {
    foo: {
      bar: "baz"
    }
  }
}

對應的 YAML 文件如下:

pt:
  foo:
    bar: baz

正如我們看到的,在這兩種情況下,頂層的鍵是語系設定,:foo 是命名空間的鍵,:bar 是翻譯 “baz” 的鍵。

下面是來自 Active Support 自帶的 YAML 格式的翻譯文件 en.yml 的真實範例:

en:
  date:
    formats:
      default: "%Y-%m-%d"
      short: "%b %d"
      long: "%B %d, %Y"

因此,下列查找效果相同,都會返回 日期格式 "%b %d"

I18n.t 'date.formats.short'
I18n.t 'formats.short', scope: :date
I18n.t :short, scope: 'date.formats'
I18n.t :short, scope: [:date, :formats]

一般來說,我們推薦使用YAML作為儲存翻譯的一種格式,然而,在有些情況下,我們可能需要把 Ruby lambda 作為儲存語系設定訊息的一部分,例如特殊的日期格式。

6、自定義 I18n 設定

6.1 使用不同的後端

由於某些原因, Active Support 自帶的 Simple backend 只為 Ruby on Rails 做了 “完成任務所需的最少量工作”3,這意外著只有對英語以及和英語高度相似的語言, Simple backend 才能保證正常的工作。此外,Simple backend 只能讀取翻譯,而不能動態的把翻譯儲存為任何格式。

這並不意味著我們會被這些限制所困擾,Ruby I18n gem 讓我們能夠藉由將後端實體傳遞給 I18n.backend=, 輕易將 Simple backend 實現與更適合你需求的其他實現進行交換。

例如,你可以使用 Chain backend 來取代 Simple backend,以連結多個backend。當你想要使用標準翻譯與 Simple backend 一起使用,但是將自定義的應用翻譯儲存在資料庫或其他backend中時,這就很有用了。

使用 Chain backend,你可以使用Active Record backend並回到(預設) Simple backend :

I18n.backend = I18n::Backend::Chain.new(I18n::Backend::ActiveRecord.new, I18n.backend)

6.2 使用不同的異常處理程序

I18n API 定義了下列的異常,這些異常會在相應的意外情況發生時,由後端拋出:

Exception Reason
I18n::MissingTranslationData 找不到對應鍵的翻譯
I18n::InvalidLocale I18n.locale 的語系設定是無效的(例如 nil)
I18n::InvalidPluralizationData 傳遞 count 參數,但翻譯無法轉換成複數形式
I18n::MissingInterpolationArgument 翻譯所需的插值參數未傳遞
I18n::ReservedInterpolationKey 翻譯包含的插值變數名稱使用了保留關鍵字 (例如:scopedefault)
I18n::UnknownFileType 後端不知道應該如何處理添加到 I18n.load_path 中的文件類型

6.2.1 自定義 I18n::MissingTranslationData 的處理方式

如果 config.i18n.raise_on_missing_translationsI18n::MissingTranslationData 錯誤將會跳出來,在測試環境中打開它是好主意,這樣就可以找到缺少翻譯的地方。
如果 config.i18n.raise_on_missing_translations (所有環境中的預設值),例外錯誤訊息會被印出來,他包含缺少的鍵/範圍,所以你可以修復你的程式碼。

如果你想進一步定義此行為,你應該設定 config.i18n.raise_on_missing_translations = false,然後實施 I18n.exception_handler,自定義異常處理程序可以是一個 proc 或一個帶有 call 方法的類別:

# config/initializers/i18n.rb
module I18n
  class RaiseExceptForSpecificKeyExceptionHandler
    def call(exception, locale, key, options)
      if key == "special.key"
        "translation missing!" # return this, don't raise it
      elsif exception.is_a?(MissingTranslation)
        raise exception.to_exception
      else
        raise exception
      end
    end
  end
end

I18n.exception_handler = I18n::RaiseExceptForSpecificKeyExceptionHandler.new

這將會與預設處理程序相同的方式引發所有異常,除了 I18n.t("special.key") 的情況。

7、翻譯 model 內容

本指南中所描述的I18n API主要用於翻譯介面字串。如果你正在尋找翻譯模型內容(例如部落格文章),則需要另一種解決方案來幫助這項工作。

下面幾個 gem 可以解決這個問題:

  • Mobility: 支援用多種格式儲存翻譯,包含翻譯表、Json欄位(PostgreSQL)等等。
  • Traco: 根據以下程式碼,在模型表中儲存可翻譯的列。

8、結論

現在,我們已經對 Ruby on Rails 的 I18n 支持有了比較全面的了解,可以開始著手翻譯自己的專案了。

9、為 Rails I18n 做貢獻

I18n 是在 Ruby on Rails 2.2 中引入的,並且仍在不斷發展,該項目繼承了 Ruby on Rails 開發的優良傳統,各種解決方案首先應用於 gem 和真實應用,然後再把其中最好和最泛用的部分納入 Rails 核心。

因此,Rails 鼓勵每個人在 gem 或其他資料庫中試驗新想法和新特性,並將他們貢獻給社區。(別忘了在信箱列表上宣佈我們的工作!)

如果在 Ruby on Rails 的範例翻譯資料庫中沒找到想要的語系設定(語言),可以 fork repository,並添加翻譯數據,然後發送pull request

10、資源

  • Google group: rails-i18n - 項目的信件列表
  • GitHub: rails-i18n - rails-i18n 項目的程式碼repository和issue追蹤,最重要的是,我們可以在這裡找到很多的 Rails 翻譯範例,在大多數情況下,他們都適用於我們的應用。
  • GitHub: i18n - I18n gem 的程式碼repository和issue追蹤系統。

11、作者

12、參考資源

  1. 根據以下代碼,或者引用維基百科: 「國際化是一個設計軟件應用程序的過程,使其可以適應各種語言和地區而無需進行工程更改。本地化是對特定區域或語言的軟件進行適應的過程,通過添加特定於當地的組件並對文本進行翻譯。」 

  2. 其他後端可能允許或要求使用其他的格式,例如GetText後端可能允許讀取GetText文件 。 

  3. 其中一個原因是,我們不想為不需要 I18n 支持的應用增加不必要的負載,因此對於英語,I18n 資料庫應該盡可能的保持簡單,另一個原因是,為所有現存語言的 I18n 相關問題提供全部的解決方案是不可能的,因此,一個允許被完全替換的解決方案更加合適,這樣對特定功能和擴展進行試驗就會更容易。