在线版的内容可能落后于电子书,如果想及时获得更新,请购买电子书

第 15 章 Rails 国际化 API

Rails(Rails 2.2 及以上版本)自带的 Ruby I18n(internationalization 的简写)gem,提供了易用、可扩展的框架,用于把应用翻译成英语之外的语言,或为应用提供多语言支持。

“国际化”(internationalization)过程通常是指,把所有字符串及本地化相关信息(例如日期或货币格式)从应用中抽取出来。“本地化”(localization)过程通常是指,翻译这些字符串并提供相关信息的本地格式。[1]

因此,在国际化 Rails 应用的过程中,我们需要:

  • 确保 Rails 提供了 I18n 支持;

  • 把区域设置字典(locale dictionary)的位置告诉 Rails;

  • 告诉 Rails 如何设置、保存和切换区域(locale)。

在本地化 Rails 应用的过程中,我们可能需要完成下面三项工作:

  • 替换或补充 Rails 的默认区域设置,例如日期和时间格式、月份名称、Active Record 模型名等;

  • 从应用中抽取字符串,并放入字典,例如视图中的闪现信息(flash message)、静态文本等;

  • 把生成的字典储存在某个地方。

本文介绍 Rails I18n API,并提供国际化 Rails 应用的入门教程。

读完本文后,您将学到:

  • Rails 中 I18n 的工作原理;

  • 在 REST 式应用中正确使用 I18n 的几种方式;

  • 如何使用 I18n 翻译 Active Record 错误或 Action Mailer 电子邮件主题;

  • 用于进一步翻译应用的其他工具。

注意

Ruby I18n 框架提供了 Rails 应用国际化/本地化所需的全部必要支持。我们还可以使用各种 gem 来添加附加功能或特性。更多介绍请参阅 rails-18n gem

15.1 Rails 中 I18n 的工作原理

国际化是一个复杂的问题。自然语言在很多方面(例如复数规则)有所不同,要想一次性提供解决所有问题的工具很难。因此,Rails I18n API 专注于:

  • 支持英语及类似语言

  • 易于定制和扩展,以支持其他语言

作为这个解决方案的一部分,Rails 框架中的每个静态字符串(例如,Active Record 数据验证信息、时间和日期格式)都已国际化。Rails 应用的本地化意味着把这些静态字符串翻译为所需语言。

15.1.1 I18n 库的总体架构

因此,Ruby I18n gem 分为两部分:

  • I18n 框架的公开 API——包含公开方法的 Ruby 模块,定义 I18n 库的工作方式

  • 实现这些方法的默认后端(称为简单后端)

作为用户,我们应该始终只访问 I18n 模块的公开方法,但了解后端的功能也很有帮助。

注意

我们可以把默认的简单后端替换为其他功能更强的后端,这时翻译数据可能会储存在关系数据库、GetText 字典或类似解决方案中。更多介绍请参阅 15.6.1 节

15.1.2 I18n 公开 API

I18n API 中最重要的两个方法是:

translate # 查找文本翻译
localize  # 把日期和时间对象转换为本地格式(本地化)

这两个方法的别名分别为 #t#l,用法如下:

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

对于下列属性,I18n API 还提供了属性读值方法和设值方法:

load_path                 # 自定义翻译文件的路径
locale                    # 获取或设置当前区域
default_locale            # 获取或设置默认区域
available_locales         # 应用可用的区域设置白名单
enforce_available_locales # 强制使用白名单(true 或 false)
exception_handler         # 使用其他异常处理程序
backend                   # 使用其他后端

现在,我们已经掌握了 Rails I18n API 的基本用法,从下一节开始,我们将从头开始国际化一个简单的 Rails 应用。

15.2 Rails 应用的国际化设置

本节介绍为 Rails 应用提供 I18n 支持的几个步骤。

15.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 散列,把翻译信息储存在默认的简单后端中。

I18n 库使用英语作为默认的区域设置,例如,如果未设置为其他区域,那就使用 :en 区域来查找翻译。

注意

经过讨论,I18n 库在选取区域设置的键时最终采取了务实的方式,也就是仅包含语言部分,例如 :en:pl,而不是传统上使用的语言和区域两部分,例如 :en-US:en-GB。很多国际化的应用都是这样做的,例如把 :cs:th:es 分别用于捷克语、泰语和西班牙语。尽管如此,在同一语系中也可能存在重要的区域差异,例如,:en-US 使用 $ 作为货币符号,而 :en-GB 使用 £ 作为货币符号。因此,如果需要,我们也可以使用传统方式,例如,在 :en-GB 字典中提供完整的 "English - United Kingdom" 区域。像 Globalize3 这样的 gem 可以实现这一功能。

Rails 会自动加载翻译文件加载路径(I18n.load_path),这是一个保存有翻译文件路径的数组。通过配置翻译文件加载路径,我们可以自定义翻译文件的目录结构和文件命名规则。

注意

I18n 库的后端采用了延迟加载技术,相关翻译信息仅在第一次查找时加载。我们可以根据需要,随时替换默认后端。

默认的区域设置和翻译的加载路径可以在 config/application.rb 文件中配置,如下所示:

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

在查找翻译文件之前,必须先指定翻译文件加载路径。应该通过初始化脚本修改默认区域设置,而不是 config/application.rb 文件:

# config/initializers/locale.rb

# 指定 I18n 库搜索翻译文件的路径
I18n.load_path += Dir[Rails.root.join('lib', 'locale', '*.{rb,yml}')]

# 应用可用的区域设置白名单
I18n.available_locales = [:en, :pt]

# 修改默认区域设置(默认是 :en)
I18n.default_locale = :pt

15.2.2 跨请求管理区域设置

除非显式设置了 I18n.locale,默认区域设置将会应用于所有翻译文件。

本地化应用有时需要支持多区域设置。此时,需要在每个请求之前设置区域,这样在请求的整个生命周期中,都会根据指定区域,对所有字符串进行翻译。

我们可以在 ApplicationController 中使用 before_action 方法设置区域:

before_action :set_locale

def set_locale
  I18n.locale = params[:locale] || I18n.default_locale
end

上面的例子说明了如何使用 URL 查询参数来设置区域。例如,对于 http://example.com/books?locale=pt 会使用葡萄牙语进行本地化,对于 http://localhost:3000?locale=de 会使用德语进行本地化。

接下来介绍区域设置的几种不同方式。

15.2.2.1 根据域名设置区域

第一种方式是,根据应用的域名设置区域。例如,通过 www.example.com 加载英语(或默认)区域设置,通过 www.example.es 加载西班牙语区域设置。也就是根据顶级域名设置区域。这种方式有下列优点:

  • 区域设置成为 URL 地址显而易见的一部分

  • 用户可以直观地判断出页面所使用的语言

  • 在 Rails 中非常容易实现

  • 搜索引擎偏爱这种把不同语言内容放在不同域名上的做法

ApplicationController 中,我们可以进行如下配置:

before_action :set_locale

def set_locale
  I18n.locale = extract_locale_from_tld || I18n.default_locale
end

# 从顶级域名中获取区域设置,如果获取失败会返回 nil
# 需要在 /etc/hosts 文件中添加如下设置:
#   127.0.0.1 application.com
#   127.0.0.1 application.it
#   127.0.0.1 application.pl
def extract_locale_from_tld
  parsed_locale = request.host.split('.').last
  I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil
end

我们还可以通过类似方式,根据子域名设置区域:

# 从子域名中获取区域设置(例如 http://it.application.local:3000)
# 需要在 /etc/hosts 文件中添加如下设置:
#   127.0.0.1 gr.application.local
def extract_locale_from_subdomain
  parsed_locale = request.subdomains.first
  I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil
end

要想为应用添加区域设置切换菜单,可以使用如下代码:

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

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

尽管这个解决方案具有上面提到的各种优点,但通过不同域名来提供不同的本地化版本(“语言版本”)有时并非我们的首选。在其他各种可选方案中,在 URL 参数(或请求路径)中包含区域设置是最常见的。

15.2.2.2 根据 URL 参数设置区域

区域设置(和传递)的最常见方式,是将其包含在 URL 参数中,例如,在前文第一个示例中,before_action 方法调用中的 I18n.locale = params[:locale]。此时,我们会使用 www.example.com/books?locale=jawww.example.com/ja/books 这样的网址。

和根据域名设置区域类似,这种方式具有不少优点,尤其是 REST 式的命名风格,顺应了当前的互联网潮流。不过采用这种方式所需的工作量要大一些。

从 URL 参数获取并设置区域并不难,只要把区域设置包含在 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_pathroot_url,资源路由辅助方法 books_pathbooks_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 这样的 URL 地址会加载荷兰语区域设置,之后调用 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 地址时就不会抛出 Routing Error 错误了。这样,我们就可以在不指定区域设置时,使用默认的区域设置。

当然,我们需要特别注意应用的根地址﹝通常是“主页(homepage)”或“仪表盘(dashboard)”﹞。像 root to: "books#index" 这样的不考虑区域设置的路由声明,会导致 http://localhost:3001/nl 无法正常访问。(尽管“只有一个根地址”看起来并没有错)

因此,我们可以像下面这样映射 URL 地址:

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

需要特别注意路由的声明顺序,以避免这条路由覆盖其他路由。(我们可以把这条路由添加到 root :to 路由声明之前)

注意

有一些 gem 可以简化路由设置,如 routing_filterrails-translate-routesroute_translator

15.2.2.3 根据用户偏好设置进行区域设置

支持用户身份验证的应用,可能会允许用户在界面中选择区域偏好设置。通过这种方式,用户选择的区域偏好设置会储存在数据库中,并用于处理该用户发起的请求。

def set_locale
  I18n.locale = current_user.try(:locale) || I18n.default_locale
end

15.2.2.4 使用隐式区域设置

如果没有显式地为请求设置区域(例如,通过上面提到的各种方式),应用就会尝试推断出所需区域。

15.2.2.4.1 根据 HTTP 首部推断区域设置

Accept-Language HTTP 首部指明响应请求时使用的首选语言。浏览器根据用户的语言偏好设置设定这个 HTTP 首部,这是推断区域设置的首选方案。

下面是使用 Accept-Language HTTP 首部的一个简单实现:

def set_locale
  logger.debug "* Accept-Language: #{request.env['HTTP_ACCEPT_LANGUAGE']}"
  I18n.locale = extract_locale_from_accept_language_header
  logger.debug "* Locale set to '#{I18n.locale}'"
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 中间件就提供了更好的解决方案。

15.2.2.4.2 根据 IP 地理位置推断区域设置

我们可以通过客户端请求的 IP 地址来推断客户端所处的地理位置,进而推断其区域设置。GeoIP Lite Country 这样的服务或 geocoder 这样的 gem 就可以实现这一功能。

一般来说,这种方式远不如使用 HTTP 首部可靠,因此并不适用于大多数 Web 应用。

15.2.2.5 在会话或 Cookie 中储存区域设置

提醒

我们可能会认为,可以把区域设置储存在会话或 Cookie 中。但是,我们不能这样做。区域设置应该是透明的,并作为 URL 地址的一部分。这样,我们就不会打破用户的正常预期:如果我们发送一个 URL 地址给朋友,他们应该看到和我们一样的页面和内容。这就是所谓的 REST 规则。关于 REST 规则的更多介绍,请参阅 Stefan Tilkov 写的系列文章。后文将讨论这个规则的一些例外情况。

15.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
  before_action :set_locale

  def set_locale
    I18n.locale = params[:locale] || I18n.default_locale
  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>
demo untranslated

15.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>

现在,Rails 在渲染 index 视图时会显示错误信息,告诉我们缺少 :hello_world:hello_flash 这两个键的翻译。

demo translation missing

注意

Rails 为视图添加了 ttranslate)辅助方法,从而避免了反复使用 I18n.t 这么长的写法。此外,t 辅助方法还能捕获缺少翻译的错误,把生成的错误信息放在 <span class="translation_missing"> 元素里。

15.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 区域设置,响应请求时生成的视图会显示英文字符串:

demo translated en

如果我们通过 URL 地址(http://localhost:3000?locale=pirate)把区域设置为 pirate,响应请求时生成的视图就会显示海盗黑话:

demo translated pirate

注意

添加新的区域设置文件后,需要重启服务器。

要想把翻译储存在 SimpleStore 中,我们可以使用 YAML(.yml)或纯 Ruby(.rb)文件。大多数 Rails 开发者会优先选择 YAML。不过 YAML 有一个很大的缺点,它对空格和特殊字符非常敏感,因此有可能出现应用无法正确加载字典的情况。而 Ruby 文件如果有错误,在第一次加载时应用就会崩溃,因此我们很容易就能找出问题。(如果在使用 YAML 字典时遇到了“奇怪的问题”,可以尝试把字典的相关部分放入 Ruby 文件中。)

如果翻译存储在 YAML 文件中,有些键必须转义:

  • 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

15.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 是保留关键字,不能用作变量名。如果误用,Rails 会抛出 I18n::ReservedInterpolationKey 异常。如果没有把翻译所需的插值变量传递给 #translate 方法,Rails 会抛出 I18n::MissingInterpolationArgument 异常。

15.3.4 添加日期/时间格式

现在,我们要给视图添加时间戳,以便演示日期/时间的本地化功能。要想本地化时间格式,可以把时间对象传递给 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"

得到的结果如下:

demo localized pirate

提示

现在,我们可能需要添加一些日期/时间格式,这样 I18n 后端才能按照预期工作(至少应该为 pirate 区域设置添加日期/时间格式)。当然,很可能已经有人通过翻译 Rails 相关区域设置的默认值,完成了这些工作。GitHub 上的 rails-i18n 仓库提供了各种本地化文件的存档。把这些本地化文件放在 config/locales/ 文件夹中即可正常使用。

15.3.5 其他区域的变形规则

Rails 允许我们为英语之外的区域定义变形规则(例如单复数转换规则)。在 config/initializers/inflections.rb 文件中,我们可以为多个区域定义规则。这个初始化脚本包含了为英语指定附加规则的例子,我们可以参考这些例子的格式为其他区域定义规则。

15.3.6 本地化视图

假设应用中包含 BooksControllerindex 动作默认会渲染 app/views/books/index.html.erb 模板。如果我们在同一个文件夹中创建了包含本地化变量的 index.es.html.erb 模板,当区域设置为 :es 时,index 动作就会渲染这个模板,而当区域设置为默认区域时, index 动作会渲染通用的 index.html.erb 模板。(在 Rails 的未来版本中,本地化的这种自动化魔术,有可能被应用于 public 文件夹中的资源)

本地化视图功能很有用,例如,如果我们有大量静态内容,就可以使用本地化视图,从而避免把所有东西都放进 YAML 或 Ruby 字典里的麻烦。但要记住,一旦我们需要修改模板,就必须对每个模板文件逐一进行修改。

15.3.7 区域设置文件的组织

当我们使用 I18n 库自带的 SimpleStore 时,字典储存在磁盘上的纯文本文件中。对于每个区域,把应用的各部分翻译都放在一个文件中,可能会带来管理上的困难。因此,把每个区域的翻译放在多个文件中,分层进行管理是更好的选择。

例如,我们可以像下面这样组织 config/locales 文件夹:

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

这样,我们就可以把模型和属性名同视图中的文本分离,同时还能使用“默认值”(例如日期和时间格式)。I18n 库的不同后端可以提供不同的分离方式。

注意

Rails 默认的区域设置加载机制,无法自动加载上面例子中位于嵌套文件夹中的区域设置文件。因此,我们还需要进行显式设置:

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

15.4 I18n API 功能概述

现在我们已经对 I18n 库有了较好的了解,知道了如何国际化简单的 Rails 应用。在下面几个小节中,我们将更深入地了解相关功能。

这几个小节将展示使用 I18n.translate 方法以及 translate 视图辅助方法的示例(注意视图辅助方法提供的附加功能)。

所涉及的功能如下:

  • 查找翻译

  • 把数据插入翻译中

  • 复数的翻译

  • 使用安全 HTML 翻译(只针对视图辅助方法)

  • 本地化日期、数字、货币等

15.4.1 查找翻译

15.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]

15.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'

15.4.1.3 批量查找和命名空间查找

要想一次查找多个翻译,我们可以传递键的数组作为参数:

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

此外,键可以转换为一组翻译的(可能是嵌套的)散列。例如,下面的代码可以生成所有 Active Record 错误信息的散列:

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

15.4.1.4 惰性查找

Rails 实现了一种在视图中查找区域设置的便捷方法。如果有下述字典:

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

我们就可以像下面这样在 app/views/books/index.html.erb 模板中查找 books.index.title 的值(注意点号):

<%= t '.title' %>

注意

只有 translate 视图辅助方法支持根据片段自动补全翻译作用域的功能。

我们还可以在控制器中使用惰性查找(lazy lookup):

en:
  books:
    create:
      success: Book created!

用于设置闪现信息:

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

15.4.2 复数转换

在英语中,一个字符串只有一种单数形式和一种复数形式,例如,“1 message”和“2 messages”。其他语言(阿拉伯语日语俄语等)则具有不同的语法,有更多或更少的复数形式。因此,I18n API 提供了灵活的复数转换功能。

:count 插值变量具有特殊作用,既可以把它插入翻译,又可以用于从翻译中选择复数形式(根据 CLDR 定义的复数转换规则):

I18n.backend.store_translations :en, inbox: {
  zero: 'no messages', # 可选
  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 标记的是复数。如果数量为零,而且有 :zero 元素,用它的值,而不用 :other 的值。

如果查找键没能返回可转换为复数形式的散列,就会引发 I18n::InvalidPluralizationData 异常。

15.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

15.4.4 使用安全 HTML 翻译

带有 '_html' 后缀的键和名为 'html' 的键被认为是 HTML 安全的。当我们在视图中使用这些键时,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 视图辅助方法支持 HTML 安全翻译文本的自动转换。

demo html safe

15.4.5 Active Record 模型的翻译

我们可以使用 Model.model_name.humanModel.human_attribute_name(attribute) 方法,来透明地查找模型名和属性名的翻译。

例如,当我们添加了下述翻译:

en:
  activerecord:
    models:
      user: Dude
    attributes:
      user:
        login: "Handle"
      # 会把 User 的属性 "login" 翻译为 "Handle"

User.model_name.human 会返回 "Dude",而 User.human_attribute_name("login") 会返回 "Handle"

我们还可以像下面这样为模型名添加复数形式:

en:
  activerecord:
    models:
      user:
        one: Dude
        other: Dudes

这时 User.model_name.human(count: 2) 会返回 "Dudes",而 User.model_name.human(count: 1)User.model_name.human 会返回 "Dude"

要想访问模型的嵌套属性,我们可以在翻译文件的模型层级中嵌套使用“模型/属性”:

en:
  activerecord:
    attributes:
      user/gender:
        female: "Female"
        male: "Male"

这时 User.human_attribute_name("gender.female") 会返回 "Female"

注意

如果我们使用的类包含了 ActiveModel,而没有继承自 ActiveRecord::Base,我们就应该用 activemodel 替换上述例子中键路径中的 activerecord

15.4.5.1 错误消息的作用域

Active Record 验证的错误消息翻译起来很容易。Active Record 提供了一些用于放置消息翻译的命名空间,以便为不同的模型、属性和验证提供不同的消息和翻译。当然 Active Record 也考虑到了单表继承问题。

这就为根据应用需求灵活调整信息,提供了非常强大的工具。

假设 User 模型对 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

如果模型使用了继承,Active Record 还会在继承链中查找消息。

例如,对于继承自 User 模型的 Admin 模型:

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

这样,我们就可以在模型继承链的不同位置,以及属性、模型或默认作用域中,为各种错误消息提供特殊翻译。

15.4.5.2 错误消息的插值

翻译后的模型名、属性名,以及值,始终可以通过 modelattributevalue 插值。

因此,举例来说,我们可以用 "Please fill in your %{attribute}" 这样的属性名来代替默认的 "cannot be blank" 错误信息。

count 方法可用时,可根据需要用于复数转换:

验证选项信息插值

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

:odd

:odd

-

numericality

:even

:even

-

15.4.5.3 为 Active Record 的 error_messages_for 辅助方法添加翻译

在使用 Active Record 的 error_messages_for 辅助方法时,我们可以为其添加翻译。

Rails 自带以下翻译:

en:
  activerecord:
    errors:
      template:
        header:
          one:   "1 error prohibited this %{model} from being saved"
          other: "%{count} errors prohibited this %{model} from being saved"
        body:    "There were problems with the following fields:"

注意

要想使用 error_messages_for 辅助方法,我们需要在 Gemfile 中添加一行 gem 'dynamic_form',还要安装 DynamicForm gem。

15.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!"

要想把参数用于插值,可以在调用邮件程序时使用 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!"

15.4.7 提供 I18n 支持的其他内置方法概述

在 Rails 中,我们会使用固定字符串和其他本地化元素,例如,在一些辅助方法中使用的格式字符串和其他格式信息。本小节提供了简要概述。

15.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_currencynumber_with_precisionnumber_to_percentagenumber_with_delimiternumber_to_human_size 辅助方法使用 number 作用域中的数字格式设置。

15.4.7.2 Active Model 方法

  • model_name.humanhuman_attribute_name 方法会使用 activerecord.models 作用域中可用的模型名和属性名的翻译。像 15.4.5.1 节中介绍的那样,这两个方法也支持继承的类名的翻译(例如,用于 STI)。

  • ActiveModel::Errors#generate_message 方法(在 Active Model 验证时使用,也可以手动使用)会使用上面介绍的 model_name.humanhuman_attribute_name 方法。像 15.4.5.1 节中介绍的那样,这个方法也会翻译错误消息,并支持继承的类名的翻译。

  • ActiveModel::Errors#full_messages 方法使用分隔符把属性名添加到错误消息的开头,然后在 errors.format 中查找(默认格式为 "%{attribute} %{message}")。

15.4.7.3 Active Support 方法

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

15.5 如何储存自定义翻译

Active Support 自带的简单后端,允许我们用纯 Ruby 或 YAML 格式储存翻译。[2]

通过 Ruby 散列储存翻译的示例如下:

{
  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 作为储存的区域设置信息的一部分,例如特殊的日期格式。

15.6 自定义 I18n 设置

15.6.1 使用不同的后端

由于某些原因,Active Support 自带的简单后端只为 Ruby on Rails 做了“完成任务所需的最少量工作”[3],这意味着只有对英语以及和英语高度类似的语言,简单后端才能保证正常工作。此外,简单后端只能读取翻译,而不能动态地把翻译储存为任何格式。

这并不意味着我们会被这些限制所困扰。Ruby I18n gem 让我们能够轻易地把简单后端替换为其他更适合实际需求的后端。例如,我们可以把简单后端替换为 Globalize 的 Static 后端:

I18n.backend = Globalize::Backend::Static.new

我们还可以使用 Chain 后端,把多个后端链接在一起。当我们想要通过简单后端使用标准翻译,同时把自定义翻译储存在数据库或其他后端中时,链接多个后端的方式非常有用。例如,我们可以使用 Active Record 后端,并在需要时退回到默认的简单后端:

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

15.6.2 使用不同的异常处理程序

I18n API 定义了下列异常,这些异常会在相应的意外情况发生时由后端抛出:

MissingTranslationData       # 找不到键对应的翻译
InvalidLocale                # I18n.locale 的区域设置无效(例如 nil)
InvalidPluralizationData     # 传递了 count 参数,但翻译数据无法转换为复数形式
MissingInterpolationArgument # 翻译所需的插值参数未传递
ReservedInterpolationKey     # 翻译包含的插值变量名使用了保留关键字(例如,scope 或 default)
UnknownFileType              # 后端不知道应该如何处理添加到 I18n.load_path 中的文件类型

当后端抛出上述异常时,I18n API 会捕获这些异常,把它们传递给 default_exception_handler 方法。这个方法会再次抛出除了 MissingTranslationData 之外的异常。当捕捉到 MissingTranslationData 异常时,这个方法会返回异常的错误消息字符串,其中包含了所缺少的键/作用域。

这样做的原因是,在开发期间,我们通常希望在缺少翻译时仍然渲染视图。

不过,在其他上下文中,我们可能想要改变此行为。例如,默认的异常处理程序不允许在自动化测试期间轻易捕获缺少的翻译;要改变这一行为,可以使用不同的异常处理程序。所使用的异常处理程序必需是 I18n 模块中的方法,或具有 #call 方法的类。

module I18n
  class JustRaiseExceptionHandler < ExceptionHandler
    def call(exception, locale, key, options)
      if exception.is_a?(MissingTranslationData)
        raise exception.to_exception
      else
        super
      end
    end
  end
end

I18n.exception_handler = I18n::JustRaiseExceptionHandler.new

这个例子中使用的异常处理程序只会重新抛出 MissingTranslationData 异常,并把其他异常传递给默认的异常处理程序。

不过,如果我们使用了 I18n::Backend::Pluralization 异常处理程序,则还会抛出 I18n::MissingTranslationData: translation missing: en.i18n.plural.rule 异常,而这个异常通常应该被忽略,以便退回到默认的英语区域设置的复数转换规则。为了避免这种情况,我们可以对翻译键进行附加检查:

if exception.is_a?(MissingTranslationData) && key.to_s != 'i18n.plural.rule'
  raise exception.to_exception
else
  super
end

默认行为不太适用的另一个例子,是 Rails 的 TranslationHelper 提供的 #t 辅助方法(和 #translate 辅助方法)。当上下文中出现了 MissingTranslationData 异常时,这个辅助方法会把错误消息放到 <span class="translation_missing"> 元素中。

不管是什么异常处理程序,这个辅助方法都能够通过设置 :raise 选项,强制 I18n#translate 方法抛出异常:

I18n.t :foo, raise: true # 总是重新抛出来自后端的异常

15.7 结论

现在,我们已经对 Ruby on Rails 的 I18n 支持有了较为全面的了解,可以开始着手翻译自己的项目了。

如果想参加讨论或寻找问题的解答,可以注册 rails-i18n 邮件列表

15.8 为 Rails I18n 作贡献

I18n 是在 Ruby on Rails 2.2 中引入的,并且仍在不断发展。该项目继承了 Ruby on Rails 开发的优良传统,各种解决方案首先应用于 gem 和真实应用,然后再把其中最好和最广泛使用的部分纳入 Rails 核心。

因此,Rails 鼓励每个人在 gem 或其他库中试验新想法和新特性,并将它们贡献给社区。(别忘了在邮件列表上宣布我们的工作!)

如果在 Ruby on Rails 的示例翻译数据库中没找到想要的区域设置(语言),可以派生仓库,添加翻译数据,然后发送拉取请求

15.9 资源

15.10 作者

  1. 维基百科的定义是:“国际化是指在设计软件时,将软件与特定语言及地区脱钩的过程。当软件被移植到不同的语言及地区时,软件本身不用做内部工程上的改变或修正。本地化则是指在移植软件时,加上与特定区域设置有关的信息和翻译文件的过程。”
  2. 其他后端可能允许或要求使用其他格式,例如,GetText 后端允许读取 GetText 文件。
  3. 其中一个原因是,我们不想为不需要 I18n 支持的应用增加不必要的负载,因此对于英语,I18n 库应该尽可能保持简单。另一个原因是,为所有现存语言的 I18n 相关问题提供一揽子解决方案是不可能的。因此,一个允许被完全替换的解决方案更加合适。这样对特定功能和扩展进行试验就会更容易。