更多内容请访问 rubyonrails.org:

Rails 国际化 (I18n) API

随 Ruby on Rails (从 Rails 2.2 开始) 发布的 Ruby I18n (国际化的缩写) gem 提供了一个易于使用且可扩展的框架,用于将您的应用程序翻译成除英语之外的单一自定义语言为您的应用程序提供多语言支持

“国际化”过程通常意味着将所有字符串和其他特定于区域设置的位(例如日期或货币格式)从您的应用程序中抽象出来。“本地化”过程意味着为这些位提供翻译和本地化格式。1

因此,在对 Rails 应用程序进行国际化的过程中,您需要

  • 确保您支持 I18n。
  • 告诉 Rails 在哪里可以找到区域设置字典。
  • 告诉 Rails 如何设置、保留和切换区域设置。

在对应用程序进行本地化的过程中,您可能需要执行以下三件事

  • 替换或补充 Rails 的默认区域设置 - 例如日期和时间格式、月份名称、Active Record 模型名称等。
  • 将应用程序中的字符串抽象为带键的字典 - 例如闪存消息、视图中的静态文本等。
  • 将生成的字典存储在某个地方。

本指南将引导您了解 I18n API,并包含一个关于如何从头开始国际化 Rails 应用程序的教程。

阅读本指南后,您将了解

  • I18n 在 Ruby on Rails 中的工作原理
  • 如何在 RESTful 应用程序中以各种方式正确使用 I18n
  • 如何使用 I18n 翻译 Active Record 错误或 Action Mailer 电子邮件主题
  • 一些其他工具可进一步推进应用程序的翻译过程

Ruby I18n 框架为您提供了国际化/本地化 Rails 应用程序所需的所有方法。您还可以使用各种可用的 gem 添加额外的功能或特性。有关更多信息,请参阅 rails-i18n gem

1. Ruby on Rails 中的 I18n 工作原理

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

  • 开箱即用地支持英语和类似语言
  • 使其他语言的定制和扩展变得容易

作为此解决方案的一部分,Rails 框架中的每个静态字符串——例如 Active Record 验证消息、时间和日期格式——都已国际化。Rails 应用程序的本地化意味着为这些字符串在所需语言中定义翻译值。

要本地化、存储和更新应用程序中的内容(例如翻译博客文章),请参阅翻译模型内容一节。

1.1. 库的整体架构

因此,Ruby I18n gem 分为两部分

  • I18n 框架的公共 API - 一个包含定义库工作方式的公共方法的 Ruby 模块
  • 一个实现这些方法的默认后端(有意命名为 Simple 后端)

作为用户,您应该始终只访问 I18n 模块上的公共方法,但了解后端的功能很有用。

可以将随附的 Simple 后端替换为功能更强大的后端,后者会将翻译数据存储在关系数据库、GetText 字典或类似的地方。请参阅下面的 使用不同的后端 一节。

1.2. 公共 I18n API

I18n API 最重要的方法是

translate # Lookup text translations
localize  # Localize Date and Time objects to local formats

它们有别名 #t 和 #l,因此您可以这样使用它们

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

还有以下属性的属性读取器和写入器

load_path                 # Announce your custom translation files
locale                    # Get and set the current locale
default_locale            # Get and set the default locale
available_locales         # Permitted locales available for the application
enforce_available_locales # Enforce locale permission (true or false)
exception_handler         # Use a different exception_handler
backend                   # Use a different 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 哈希将翻译存储在默认(简单)后端中。

I18n 库将使用英语作为默认区域设置,即如果未设置其他区域设置,则 :en 将用于查找翻译。

i18n 库对区域设置键采用实用方法(经过一些讨论),只包含区域设置(“语言”)部分,例如 :en:pl,而不是地区部分,例如 :"en-US":"en-GB",这些传统上用于区分“语言”和“地区设置”或“方言”。许多国际应用程序只使用区域设置的“语言”元素,例如 :cs:th:es(分别用于捷克语、泰语和西班牙语)。然而,不同语言组内部也存在可能很重要的地区差异。例如,在 :"en-US" 区域设置中,您将使用 $ 作为货币符号,而在 :"en-GB" 中,您将使用 £。没有什么能阻止您以这种方式分离地区和其他设置:您只需在 :"en-GB" 字典中提供完整的“英语 - 英国”区域设置。

翻译加载路径I18n.load_path)是一个文件路径数组,这些文件将自动加载。配置此路径允许自定义翻译目录结构和文件命名方案。

后端在首次查找翻译时延迟加载这些翻译。即使已经宣布了翻译,该后端也可以更换为其他后端。

您可以在 config/application.rb 中更改默认区域设置并配置翻译加载路径,如下所示

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

必须在查找任何翻译之前指定加载路径。要从初始化程序而不是 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 将不会覆盖来自外部 gem 的翻译。

2.2. 跨请求管理区域设置

本地化应用程序可能需要支持多种区域设置。为此,应在每个请求开始时设置区域设置,以便在请求的整个生命周期内使用所需的区域设置翻译所有字符串。

除非使用 I18n.locale=I18n.with_locale,否则所有翻译都使用默认区域设置。

如果 I18n.locale 未在每个控制器中一致设置,它可能会泄露到同一线程/进程处理的后续请求中。例如,在一个 POST 请求中执行 I18n.locale = :es 将对所有后来不设置区域设置的控制器请求产生影响,但仅限于该特定线程/进程。因此,您可以使用 I18n.with_locale 而不是 I18n.locale =,后者没有此泄露问题。

区域设置可以在 ApplicationControlleraround_action 中设置

around_action :switch_locale

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

此示例演示了如何使用 URL 查询参数设置区域设置(例如 http://example.com/books?locale=pt)。使用此方法,https://:3000?locale=pt 会渲染葡萄牙语本地化,而 https://:3000?locale=de 会加载德语本地化。

区域设置可以使用许多不同的方法之一进行设置。

2.2.1. 从域名设置区域设置

一种选择是从运行您的应用程序的域名设置区域设置。例如,我们希望 www.example.com 加载英语(或默认)区域设置,而 www.example.es 加载西班牙语区域设置。因此,顶级域名用于区域设置。这有几个优点

  • 区域设置是 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 it.application.local
# in your /etc/hosts file to try this out locally
#
# Additionally, you need to add the following configuration to your config/environments/development.rb:
#   config.hosts << 'it.application.local:3000'
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 参数(或请求路径)中。

2.2.2. 从 URL 参数设置区域设置

设置(和传递)区域设置最常见的方法是将其包含在 URL 参数中,就像我们在第一个示例中的 I18n.with_locale(params[:locale], &action) around_action 中所做的那样。在这种情况下,我们希望有类似 www.example.com/books?locale=jawww.example.com/ja/books 的 URL。

这种方法与从域名设置区域设置具有几乎相同的一组优点:即它是 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_pathroot_url,资源路由,如 books_pathbooks_url 等)都将自动在查询字符串中包含区域设置,如下所示:https://:3001/?locale=ja

您可能对此感到满意。然而,当区域设置在应用程序中每个 URL 的末尾“悬挂”时,它会影响 URL 的可读性。此外,从架构的角度来看,区域设置通常在层次上高于应用程序域的其他部分:URL 应该反映这一点。

您可能希望 URL 看起来像这样:http://www.example.com/en/books(加载英语区域设置)和 http://www.example.com/nl/books(加载荷兰语区域设置)。这可以通过上面提到的“覆盖 default_url_options”策略实现:您只需使用 scope 设置您的路由

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

现在,当您调用 books_path 方法时,您应该会得到 "/en/books"(对于默认区域设置)。然后,像 https://: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

通过这种方法,当您在没有区域设置的情况下访问 https://:3001/books 等资源时,不会收到 Routing Error。当您想在未指定区域设置时使用默认区域设置时,这很有用。

当然,您需要特别注意应用程序的根 URL(通常是“主页”或“仪表板”)。像 https://:3001/nl 这样的 URL 不会自动工作,因为 routes.rb 中的 root to: "dashboard#index" 声明没有考虑区域设置。(这是正确的:只有一个“根”URL。)

您可能需要映射如下 URL

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

请特别注意路由的顺序,以确保此路由声明不会“吞噬”其他路由。(您可能希望将其直接添加到 root :to 声明之前。)

查看一个简化路由工作的 gem: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. 从语言头推断区域设置

Accept-Language HTTP 头指示请求响应的首选语言。浏览器根据用户的语言偏好设置此头部值,使其成为推断区域设置时的一个很好的首选。

使用 Accept-Language 头的一个简单实现将是

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 可用于实现此方法。

通常,这种方法远不如使用语言头可靠,并且不建议用于大多数 Web 应用程序。

2.2.5. 从会话或 Cookie 存储区域设置

您可能会倾向于将选择的区域设置存储在会话cookie中。但是,请不要这样做。区域设置应该是透明的,并且是 URL 的一部分。这样,您就不会破坏人们对万维网本身的基本假设:如果您将 URL 发送给朋友,他们应该看到与您相同的页面和内容。一个花哨的词来形容这一点就是您正在遵循RESTful原则。在Stefan Tilkov 的文章中阅读更多关于 RESTful 方法的信息。有时此规则存在例外,这些例外将在下面讨论。

3. 国际化和本地化

好的!现在您已经为 Ruby on 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>

现在,当此视图被渲染时,它将显示一条错误消息,告诉您缺少键 :hello_world:hello_flash 的翻译。

rails i18n demo translation missing

Rails 在您的视图中添加了一个 t (translate) 辅助方法,这样您就不必一直拼写 I18n.t。此外,此辅助方法会捕获缺失的翻译并将生成的错误消息包装到 <span class="translation_missing"> 中。

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 区域设置,响应渲染英语字符串

rails i18n demo translated to English

如果通过 URL 将区域设置设置为海盗区域设置 (https://:3000?locale=pirate),则响应会渲染海盗字符串

rails i18n demo translated to pirate

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

您可以使用 YAML (.yml) 或纯 Ruby (.rb) 文件在 SimpleStore 中存储您的翻译。YAML 是 Rails 开发者首选的选项。但是,它有一个很大的缺点。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

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 附带了一个名为变量插值的功能,允许您在翻译定义中使用变量,并将这些变量的值传递给翻译方法。

以下示例显示了正确的抽象

<!-- 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,则会引发 I18n::MissingInterpolationArgument 异常。

3.4. 添加日期/时间格式

好的!现在让我们为视图添加一个时间戳,这样我们也可以演示日期/时间本地化功能。要本地化时间格式,您可以将 Time 对象传递给 I18n.l 或(最好)使用 Rails 的 #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>

在我们的海盗翻译文件中,让我们添加一个时间格式(它已经在 Rails 的英语默认设置中)

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

这样你会得到

rails i18n demo localized time to pirate

现在您可能需要添加一些更多的日期/时间格式,以便 I18n 后端按预期工作(至少对于“海盗”区域设置)。当然,很有可能有人已经完成了所有工作,为您的区域设置翻译了 Rails 的默认值。请参阅 GitHub 上的 rails-i18n 仓库,获取各种区域设置文件的存档。当您将此类文件放入 config/locales/ 目录时,它们将自动可供使用。

3.5. 其他区域设置的词形变化规则

Rails 允许您为英语以外的区域设置定义词形变化规则(例如单复数规则)。在 config/initializers/inflections.rb 中,您可以为多个区域设置定义这些规则。初始化程序包含一个默认示例,用于指定英语的其他规则;您可以根据需要为其他区域设置遵循该格式。

3.6. 本地化视图

假设您的应用程序中有一个 BooksController。您的 index action 在 app/views/books/index.html.erb 模板中渲染内容。当您在同一目录中放置此模板的本地化变体index.es.html.erb 时,当区域设置为 :es 时,Rails 将在此模板中渲染内容。当区域设置为默认区域设置时,将使用通用的 index.html.erb 视图。(未来的 Rails 版本很可能将这种自动本地化带到 public 中的资产等。)

您可以使用此功能,例如,当处理大量静态内容时,将其放入 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

这样,您可以将模型和模型属性名称与视图内的文本以及所有这些与“默认值”(例如日期和时间格式)分开。i18n 库的其他存储可以提供不同的分离方式。

4. I18n API 功能概述

您现在应该对 i18n 库的使用有了很好的理解,并且知道如何国际化一个基本的 Rails 应用程序。在接下来的章节中,我们将更深入地介绍其功能。

这些章节将展示使用 I18n.translate 方法以及 translate 视图辅助方法的示例(注意视图辅助方法提供的额外功能)。

涵盖的功能包括

  • 查找翻译
  • 将数据插入翻译中
  • 翻译的复数化
  • 使用安全的 HTML 翻译(仅限视图辅助方法)
  • 日期、数字、货币等的本地化

4.1. 查找翻译

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

翻译通过键查找,键可以是符号或字符串,所以这些调用是等效的

I18n.t :message
I18n.t "message"

translate 方法还接受一个 :scope 选项,该选项可以包含一个或多个附加键,这些键将用于指定翻译键的“命名空间”或作用域

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 值是一个 Symbol,它将用作键并进行翻译。可以提供多个值作为默认值。第一个产生值的将被返回。

例如,以下首先尝试翻译键 :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"]

此外,一个键可以翻译成一个(可能嵌套的)哈希,其中包含分组的翻译。例如,可以通过以下方式接收所有 Active Record 错误消息作为哈希

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

如果您想对大量哈希翻译执行插值,则需要将 deep_interpolation: true 作为参数传递。当您有以下字典时

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 实现了一种在视图内部查找区域设置的便捷方法。当您有以下字典时

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

您可以在 app/views/books/index.html.erb 模板内部这样查找 books.index.title 的值(注意点号)

<%= t '.title' %>

按局部自动翻译作用域仅在 translate 视图辅助方法中可用。

“惰性”查找也可以在控制器中使用

en:
  books:
    create:
      success: Book created!

这对于设置闪存消息很有用,例如

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 用作复数。如果计数为零,并且存在 :zero 条目,则将使用它而不是 :other

如果查找键没有返回适合复数化的哈希,则会引发 I18n::InvalidPluralizationData 异常。

4.2.1. 特定于区域设置的规则

I18n gem 提供了一个复数化后端,可用于启用特定于区域设置的规则。将其包含在 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'

或者,可以使用单独的 gem rails-i18n 来提供更全面的特定于区域设置的复数化规则集。

4.3. 设置和传递区域设置

区域设置可以伪全局地设置为 I18n.locale(它使用 Thread.current 的方式与例如 Time.zone 相同),也可以作为选项传递给 #translate#localize

如果没有传递区域设置,则使用 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,后者默认为 :en。默认区域设置可以这样设置

I18n.default_locale = :de

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) %>

另一方面,安全字符串会逐字插值。

自动将 HTML 安全翻译文本转换为 HTML 安全翻译文本仅在 translate(或 t)辅助方法中可用。这在视图和控制器中都有效。

i18n demo HTML safe

4.5. Active Record 模型的翻译

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

例如,当您添加以下翻译时

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"。

您还可以为模型名称设置复数形式,如下所示

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

那么 User.model_name.human(count: 2) 将返回 "Customers"。使用 count: 1 或不带参数将返回 "Customer"。

如果您需要访问给定模型中的嵌套属性,则应在翻译文件的模型级别下将这些属性嵌套在 model/attribute

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

那么 User.human_attribute_name("role.admin") 将返回 "Admin"。

如果您使用的类包含 ActiveModel 且不继承自 ActiveRecord::Base,则将上述键路径中的 activerecord 替换为 activemodel

4.5.1. 错误消息作用域

Active Record 验证错误消息也可以轻松翻译。Active Record 提供了几个命名空间,您可以在其中放置消息翻译,以便为某些模型、属性和/或验证提供不同的消息和翻译。它还透明地考虑了单表继承。

这为您提供了相当强大的方法,可以灵活地调整消息以满足应用程序的需求。

考虑一个具有名称属性验证的用户模型,如下所示

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

在这种情况下,错误消息的键是 :blank。因此,在我们的示例中,它将按此顺序尝试以下键并返回第一个结果

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

更抽象地解释,它按以下列表的顺序返回第一个匹配的键。

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

当您的模型额外使用继承时,消息会在继承链中查找。

例如,您可能有一个继承自 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

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

4.5.2. 错误消息插值

翻译后的模型名称、翻译后的属性名称和值始终可用作插值变量 modelattributevalue

因此,例如,您可以这样使用属性名称而不是默认错误消息 "can't be blank""Please fill in your %{attribute}"

  • count(如果可用)可用于复数化
验证 带选项 消息 插值
确认 - :confirmation 属性
接受 - :accepted -
presence - :blank -
不存在 - :present -
长度 :within, :in :too_short 计数
长度 :within, :in :too_long 计数
长度 :is :wrong_length 计数
长度 :minimum :too_short 计数
长度 :maximum :too_long 计数
唯一性 - :taken -
format(格式) - :invalid -
包含 - :inclusion -
排除 - :exclusion -
关联 - :invalid -
非可选关联 - :required -
数值性 - :not_a_number -
数值性 :greater_than :greater_than 计数
数值性 :greater_than_or_equal_to :greater_than_or_equal_to 计数
数值性 :equal_to :equal_to 计数
数值性 :less_than :less_than 计数
数值性 :less_than_or_equal_to :less_than_or_equal_to 计数
数值性 :other_than :other_than 计数
数值性 :only_integer :not_an_integer -
数值性 :in :in 计数
数值性 :odd :odd -
数值性 :even :even -
比较 :greater_than :greater_than 计数
比较 :greater_than_or_equal_to :greater_than_or_equal_to 计数
比较 :equal_to :equal_to 计数
比较 :less_than :less_than 计数
比较 :less_than_or_equal_to :less_than_or_equal_to 计数
比较 :other_than :other_than 计数

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

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

Rails 在几个辅助方法中使用了固定字符串和其他本地化,例如格式字符串和其他格式信息。以下是简要概述。

4.7.1. Action View 辅助方法

  • distance_of_time_in_words 翻译并复数化其结果,并插入秒、分钟、小时等的数量。请参阅 datetime.distance_in_words 翻译。

  • datetime_selectselect_month 使用翻译的月份名称来填充生成的选择标签。请参阅 date.month_names 获取翻译。datetime_select 还会从 date.order 中查找 order 选项(除非您显式传递该选项)。所有日期选择辅助方法都使用 datetime.prompts 作用域中的翻译来翻译提示(如果适用)。

  • number_to_currencynumber_with_precisionnumber_to_percentagenumber_with_delimiternumber_to_human_size 辅助方法使用位于 number 作用域中的数字格式设置。

4.7.2. Active Model 方法

  • model_name.humanhuman_attribute_name 如果 activerecord.models 作用域中可用,则使用模型名称和属性名称的翻译。它们还支持继承类名称的翻译(例如与 STI 一起使用),如上面“错误消息作用域”中所述。

  • ActiveModel::Errors#generate_message(由 Active Model 验证使用,也可以手动使用)使用 model_name.humanhuman_attribute_name(参见上文)。它还会翻译错误消息,并支持继承类名称的翻译,如上面“错误消息作用域”中所述。

  • ActiveModel::Error#full_messageActiveModel::Errors#full_messages 使用从 errors.format 查找的格式(默认:"%{attribute} %{message}")将属性名称添加到错误消息中。要自定义默认格式,请在应用程序的区域设置文件中覆盖它。要按模型或按属性自定义格式,请参阅config.active_model.i18n_customize_full_message

4.7.3. Active Support 方法

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

5. 如何存储您的自定义翻译

Active Support 随附的 Simple 后端允许您以纯 Ruby 和 YAML 格式存储翻译。2

例如,一个提供翻译的 Ruby Hash 可能看起来像这样

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

等效的 YAML 文件将如下所示

pt:
  foo:
    bar: baz

如您所见,在两种情况下,顶层键都是区域设置。:foo 是命名空间键,:bar 是翻译“baz”的键。

这是 Active Support en.yml 翻译 YAML 文件中的一个“真实”示例

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

因此,所有以下等效查找都将返回 :short 日期格式 "%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 lambdas 作为区域设置数据的一部分存储,例如用于特殊日期格式。

6. 自定义您的 I18n 设置

6.1. 使用不同的后端

出于多种原因,Active Support 随附的 Simple 后端仅为 Ruby on Rails 完成了“最简单可能的工作”3……这意味着它仅保证适用于英语,并且作为副作用,适用于与英语非常相似的语言。此外,简单后端只能读取翻译,但不能动态地将它们存储为任何格式。

但这并不意味着您受限于这些限制。Ruby I18n gem 使替换 Simple 后端实现变得非常容易,您可以将其替换为更适合您需求的后端,方法是将后端实例传递给 I18n.backend= setter。

例如,您可以用 Chain 后端替换 Simple 后端,以将多个后端链接在一起。当您想使用 Simple 后端进行标准翻译,但将自定义应用程序翻译存储在数据库或其他后端中时,这很有用。

使用 Chain 后端,您可以使用 Active Record 后端并回退到(默认)Simple 后端

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

6.2. 使用不同的异常处理程序

I18n API 定义了以下异常,当发生相应的意外情况时,后端将引发这些异常

异常 原因
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_translationstrue,则会从视图和控制器中引发 I18n::MissingTranslationData 错误。如果值为 :strict,模型也会引发错误。在测试环境中启用此功能是个好主意,这样您就可以捕获请求缺失翻译的地方。

如果 config.i18n.raise_on_missing_translationsfalse(所有环境中的默认值),则会打印异常的错误消息。这包含缺失的键/作用域,以便您可以修复代码。

如果您想进一步自定义此行为,您应该将 config.i18n.raise_on_missing_translations = false 设置为 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. 翻译模型内容

本指南中描述的 I18n API 主要用于翻译界面字符串。如果您正在寻找翻译模型内容(例如博客文章),您将需要一个不同的解决方案来帮助解决此问题。

有几个 gem 可以帮助解决这个问题

  • Mobility:支持以多种格式存储翻译,包括翻译表、JSON 列 (PostgreSQL) 等。
  • Traco:可翻译列存储在模型表本身中

8. 结论

至此,您应该对 Ruby on Rails 中的 I18n 支持工作原理有了很好的概述,并已准备好开始翻译您的项目。

9. 为 Rails I18n 贡献

Ruby on Rails 中的 I18n 支持是在 2.2 版本中引入的,并且仍在不断发展。该项目遵循 Ruby on Rails 良好的开发传统,即首先在 gem 和实际应用程序中发展解决方案,然后才挑选出最广泛有用的最佳特性以包含在核心中。

因此,我们鼓励大家在 gem 或其他库中尝试新想法和功能,并将其提供给社区。(不要忘记在我们的邮件列表上宣布您的工作!)

如果您发现您的区域设置(语言)在我们 Ruby on Rails 的示例翻译数据仓库中缺失,请fork该仓库,添加您的数据,并发送拉取请求

10. 资源

  • GitHub: rails-i18n - rails-i18n 项目的代码仓库和问题跟踪器。最重要的是,您可以找到大量适用于 Rails 的示例翻译,这些翻译在大多数情况下都应该适用于您的应用程序。
  • GitHub: i18n - i18n gem 的代码仓库和问题跟踪器。

11. 作者

12. 脚注

1 或者,引用维基百科“国际化是设计软件应用程序的过程,使其无需工程更改即可适应各种语言和地区。本地化是通过添加特定于区域设置的组件和翻译文本来使软件适应特定地区或语言的过程。”

2 其他后端可能允许或要求使用其他格式,例如 GetText 后端可能允许读取 GetText 文件。

3 其中一个原因是,我们不希望给不需要任何 I18n 功能的应用程序带来不必要的负载,因此我们需要尽可能保持 I18n 库对英语来说尽可能简单。另一个原因是,为所有现有语言的所有 I18n 相关问题实现一个通用解决方案实际上是不可能的。因此,一个允许我们轻松替换整个实现的解决方案无论如何都是合适的。这也使得试验自定义功能和扩展变得容易得多。



回到顶部