API интернационализации Rails (I18n)

В Ruby гем I18n (краткое наименование для internationalization), поставляемый с Ruby on Rails (начиная с Rails 2.2), представляет простой и расширяемый фреймворк для перевода вашего приложения на отдельный другой язык, иной чем английский, или для предоставления поддержки многоязычности в вашем приложении.

Процесс "интернационализация" обычно означает извлечение всех строк и других специфичных для локали частей (таких как форматы даты и валюты) за рамки вашего приложения. Процесс "локализация" означает предоставление переводов и локализованных форматов для этих частей.

Таким образом, в процессе интернационализации своего приложения на Rails вы должны:

  • Убедиться, что есть поддержка I18n.
  • Сказать Rails где найти словари локали.
  • Сказать Rails как устанавливать, сохранять и переключать локали.

В процессе локализации своего приложения вы, скорее всего, захотите сделать три вещи:

  • Заменить или дополнить локаль Rails по умолчанию - т.е. форматы даты и времени, названия месяцев, имена модели Active Record и т.д.
  • Извлечь строки в вашем приложении в словари ключей - т.е. сообщения flash, статичные тексты в ваших вью и т.д.
  • Где-нибудь хранить получившиеся словари.

Это руководство проведет вас через I18n API, оно содержит консультации как интернационализировать приложения на Rails с самого начала.

После прочтения этого руководства вы узнаете:

  • Как I18n работает в Ruby on Rails
  • Как правильно использовать I18n в RESTful приложении различными способами
  • Как использовать I18n для перевода ошибок Active Record или тем писем Action Mailer
  • О некоторых инструментах для расширения процесса перевода вашего приложения

Фреймворк Ruby I18n предоставляет все необходимые средства для интернационализации/локализации приложения на Rails. Можно также использовать другие различные гемы, добавляющие дополнительные функциональность или особенности. Для получения более подробной информации смотрите гем rails-i18n.

1. Как работает I18n в Ruby on Rails

Интернационализация - это сложная проблема. Естественные языки отличаются во многих отношениях (например, в правилах образования множественного числа), поэтому трудно представить инструменты, решающие сразу все проблемы. По этой причине Rails I18n API сфокусировано на:

  • предоставлении полной поддержки для английского и подобных ему языков
  • легкой настраиваемости и полном расширении для других языков

Как часть этого решения, каждая статичная строка в фреймворке Rails - например, валидационные сообщения Active Record, форматы времени и даты - стали интернационализированными. Локализация приложения на Rails означает определение переведенных значений этих строк на желаемые языки.

Для локализации хранилища и обновления content в приложении (например, перевода сообщений в блоге), смотрите раздел Перевод контента модели.

1.1. Общая архитектура библиотеки

Таким образом, Ruby гем I18n разделен на две части:

  • Публичный API фреймворка I18n - модуль Ruby с публичными методами, определяющими как работает библиотека
  • Бэкенд по умолчанию (который специально называется простым бэкендом), реализующий эти методы

Как у пользователя, у вас всегда будет доступ только к публичным методам модуля I18n, но полезно знать о возможностях бэкенда.

Возможно поменять встроенный простой бэкенд на более мощный, который будет хранить данные перевода в реляционной базе данных, словаре GetText или в чем-то похожем. Смотрите раздел Использование различных бэкендов.

1.2. Публичный I18n API

Наиболее важными методами I18n API являются:

translate # Ищет перевод текстов
localize  # Локализует объекты даты и времени в форматы локали

Имеются псевдонимы #t и #l, их можно использовать следующим образом:

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

Также имеются методы чтения и записи для следующих атрибутов:

load_path                 # Анонсировать ваши пользовательские файлы с переводом
locale                    # Получить и установить текущую локаль
default_locale            # Получить и установить локаль по умолчанию
available_locales         # Разрешенные локали, доступные приложению
enforce_available_locales # Принуждение к разрешенным локалям (true или false)
exception_handler         # Использовать иной exception_handler
backend                   # Использовать иной бэкенд

Итак, давайте интернационализируем простое приложение на Rails с самого начала, в следующих главах!

2. Настройка приложения на Rails для интернационализации

Несколько шагов отделяют вас от получения и запуска поддержки I18n в вашем приложении.

2.1. Конфигурирование модуля I18n

Следуя философии соглашений над конфигурацией, Rails предоставляет приемлемые строки переводов по умолчанию. При необходимости иных строк переводов, они могут быть переопределены.

Rails автоматически добавляет все файлы .rb и .yml из директории config/locales к пути загрузки переводов.

Локаль по умолчанию 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" будет £. Ничто не остановит вас от разделения региональных и других настроек следующим образом: предоставляете полную локаль "English - United Kingdom" в словаре :"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

# где библиотека I18n должна искать наши переводы
I18n.load_path += Dir[Rails.root.join('lib', 'locale', '*.{rb,yml}')]

# Разрешенные локали, доступные приложению
I18n.available_locales = [:en, :pt]

# устанавливаем локаль по умолчанию на что-либо другое, чем :en
I18n.default_locale = :pt

Отметьте, что добавление напрямую в I18n.load_path, вместо конфигурации I18n приложения, не перезапишет переводы из внешних гемов.

2.2. Управление локалью через запросы

Локализованному приложению, вероятно, понадобится поддерживать несколько локалей. Для этого локаль должна быть установлена в начале каждого запроса, чтобы все строки были переведены, используя нужную локаль.

Локаль по умолчанию используется для всех переводов за исключением случаев, когда установлены I18n.locale= или I18n.with_locale.

I18n.locale может вытечь в последующие запросы, обслуживаемые тем же тредом/процессом, если она не устанавливается последовательно в каждом контроллере. Например, выполнение I18n.locale = :es в одном из запросов POST будет влиять на все последующие запросы в контроллерах, не устанавливающих локаль, но только в этом конкретном треде/процессе. Поэтому вместо I18n.locale = можно использовать I18n.with_locale, не имеющий этой проблемы утечки.

Локаль может быть установлена в around_action в ApplicationController:

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). Таким образом, http://localhost:3000?locale=pt загрузит португальскую локализацию, в то время как http://localhost: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

# Получаем локаль из домена верхнего уровня или возвращаем +nil+, если такая локаль недоступна
# Вам следует поместить что-то наподобие этого:
#   127.0.0.1 application.com
#   127.0.0.1 application.it
#   127.0.0.1 application.pl
# в ваш файл /etc/hosts, чтобы попробовать это локально
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)
# Следует поместить что-то вроде:
#   127.0.0.1 gr.application.local
# в ваш файл /etc/hosts, чтобы попробовать это локально
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 в первом примере. В этом случае нам нужны URL, такие как www.example.com/books?locale=ja или www.example.com/ja/books.

В этом подходе есть почти тот же набор преимуществ, как и в назначении локали из имени домена, а именно то, что это RESTful и соответствует остальной части Всемирной паутины. Хотя внедрение этого потребует немного больше работы.

Получение локали из params и соответствующее назначение ее не сложно: включаете ее в каждый URL, и таким образом передаете ее через запросы. Конечно, включение явной опции в каждый URL (т.е. link_to(books_url(locale: I18n.locale))) было бы утомительно и, вероятно, невозможно.

Rails содержит инфраструктуру для "централизации динамических решений об URL" в его ApplicationController#default_url_options, что полезно в этом сценарии: он позволяет нам назначить "defaults" для 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 (который загружает голландскую локаль). Это достижимо с помощью такой же стратегии, как и с default_url_options выше: нужно настроить свои маршруты с помощью scope:

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

Теперь, когда вы вызовете метод books_path, то получите "/en/books" (для локали по умолчанию). URL подобный 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

С таким подходом вы не получите Routing Error при доступе к своим ресурсам как http://localhost:3001/books без локали. Это полезно, когда хочется использовать локаль по умолчанию, если она не определена.

Конечно, нужно специально позаботиться о корневом URL (это обычно "домашняя страница" или "лицевая панель") вашего приложения. URL, такой как http://localhost:3001/nl не заработает автоматически, так как объявление root to: "dashboard#index" в вашем routes.rb не принимает локаль во внимание. (И правильно делает: может быть только один "корневой" URL.)

Вам, вероятно, потребуется связать URL так:

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

Особенно побеспокойтесь относительно порядка ваших маршрутов, чтобы одно объявление маршрутов не "съело" другое. (Вы, возможно, захотите добавить его непосредственно перед объявлением root :to.)

Обратите внимание на различные гемы, которые упрощают работу с роутами: 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 указывает предпочтительный язык для отклика запроса. Браузеры устанавливают это значение заголовка на основании языковых настроек пользователя, что делает его хорошим выбором при определении локали.

Обычной реализацией использования заголовка 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's http_accept_language или промежуточное приложение Rack от Ryan Tomayko's locale предоставляют решения этой проблемы.

2.2.4.2. Определение локали по IP геолокации

IP-адрес клиента, выполняющего запрос, может использоваться для определения региона и его локали. Сервисы, такие как GeoLite2 Country, или гемы, такие как geocoder могут быть использованы для реализации этого подхода.

В целом, этот подход является менее надежным, чем при использовании языка заголовка и не рекомендуется для большинства веб-приложений.

2.2.5. Хранение локали в сессии или куки

Вы можете поддаться искушению хранить выбранную локаль в сессии или куки. Однако, не делайте этого. Локаль должна быть понятной и быть частью URL. В таком случае, вы не сломаете базовые представления людей о вебе: если вы отправляете URL друзьям, то они должны увидеть ту же самую страницу и то же содержимое. Причудливое слово для этого будет то, что вы будете спокойны - RESTful. Читайте более подробно о RESTful подходе в статье Stefan Tilkov. Иногда бывают исключения из этого правила, они описаны ниже.

3. Интернационализация и Локализация

Хорошо! Вы уже инициализировали поддержку I18n в своем приложении на Ruby on Rails, и сообщили ему, какую локаль использовать, и как ее сохранять между запросами.

Дальше нам нужно интернационализировать наше приложение, абстрагируя каждую специфичную к локали часть. Напоследок, нам нужно локализовать приложение, предоставляя необходимые переводы для этих абстракций.

У нас есть следующий пример:

# 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

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

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, переведенный на английский

Если локаль будет установлена через URL на пиратскую локаль (http://localhost:3000?locale=pirate), то в отклике будут рендериться пиратские строки:

пример rails i18n, переведенный на пиратский

Нужно перезагрузить сервер после того, как вы добавили новые файлы локали.

Для хранения переводов в SimpleStore можно использовать файлы YAML (.yml) или чистого Ruby (.rb). 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 есть возможность, называемая интерполяцией переменных, которая позволяет вам использовать переменные в переводе определений и передавать значения этих переменных в метод перевода.

Правильная абстракция показана в следующем примере:

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

Все грамматические и пунктуационные решения принимаются в самом определении, таким образом абстракция может дать верный перевод.

Опции default и scope зарезервированы и не могут быть использованы как переменные. Если перевод использует :default или :scope как интерполяционную переменную, будет вызвано исключение 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 на пиратский

Сейчас вам, возможно, захочется добавить больше форматов для того, чтобы бэкенд I18n работал как нужно (как минимум для локали "pirate"). Конечно, есть большая вероятность, что кто-то еще выполнил всю работу по переводу значений по умолчанию Rails для вашей локали. Смотрите в репозитории rails-i18n на Github архив с различными файлами локали. Когда вы поместите такой файл(ы) в директорию config/locales/, они автоматически станут готовыми для использования.

3.5. Правила словообразования для других локалей

Rails позволяет определить правила словообразования (такие как единственное и множественное число) для локалей, отличных от английской. В config/initializers/inflections.rb можно определить эти правила для нескольких локалей. Инициализатор содержит пример по умолчанию для определения дополнительных правил для английского, следуйте этому формату для других локалей.

3.6. Локализованные вью

Скажем, у вас в приложении есть BooksController. Экшн index рендерит содержимое в шаблоне app/views/books/index.html.erb. Когда вы помещаете локализованный вариант этого шаблона: index.es.html.erb в ту же директорию, Rails будет рендерить содержимое в этот шаблон, когда локаль будет установлена как :es. Когда будет установлена локаль по умолчанию, будет использована обычная вью index.html.erb. (Будущие версии Rails, возможно, перенесут эту возможность автоматической локализации ассетов в public, и т.д.)

Можете использовать эту особенность, например, при работе с большим количеством статичного содержимого, который было бы неудобно вложить в словари YAML или Ruby. Хотя имейте в виду, что любое изменение, которое вы в дальнейшем сделаете в шаблоне, должно быть распространено на все локали.

3.7. Организация файлов локали

При использовании дефолтного SimpleStore вместе с библиотекой i18n, словари хранятся в текстовых файлах на диске. Помещение переводов ко всем частям приложения в один файл на локаль будет трудным для управления. Можно хранить эти файлы в иерархии, которая будет для вас понятной.

К примеру, ваша директория 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

Таким образом можно разделить модель и имена атрибутов модели от текста внутри вью, и все это от "defaults" (т.е. форматов даты и времени). Другие хранилища для библиотеки i18n могут предоставить другие средства подобного разделения.

Механизм загрузки локали по умолчанию в Rails не загружает файлы локали во вложенных словарях, как тут. Поэтому, чтобы это заработало, нужно явно указать Rails смотреть глубже:

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

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]

Тут будет искаться сообщение :record_invalid в сообщениях об ошибке Active Record.

Кроме того, и ключ, и область имен могут быть определены как ключи с точкой в качестве разделителя, как в:

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 является символом, оно будет использовано как ключ и будет переведено. Может быть представлено несколько значений по умолчанию. Будет возвращено первое, которое даст результат.

Т.е., следующее попытается перевести ключ :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"

можно найти значение books.index.title в шаблоне app/views/books/index.html.erb таким образом (обратите внимание на точку):

<%= 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". В других языках: (русском, арабском, японском и многих других) имеются различные правила грамматики, имеющие дополнительные или отсутствующие формы множественного числа. Таким образом, API I18n предоставляет гибкую возможность для форм множественного числа.

У переменной интерполяции :count есть специальная роль в том, что она интерполируется для перевода, и используется для подбора множественного числа для перевода в соответствии с правилами множественного числа, определенными в бэкенде множественного числа. По умолчанию применяются правила множественного числа только для английского языка.

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, рассматривается как единственное число, все другое как множественное. Если количество нулевое, и существует запись :zero, тогда будет использоваться она вместо :other.

Если поиск по ключу не возвратит хэш, подходящий для образования множественного числа, вызовется исключение I18n::InvalidPluralizationData.

4.2.1. Локализованные правила

Гем I18n предоставляет бэкенд множественного числа, который может использоваться для включения правил локализации. Добавьте это в простой бэкенд, затем добавьте алгоритмы для локализации множественного числа в хранилище переводов, как 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'

В качестве альтернативы, отдельный гем 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-безопасный текст перевода доступен только для метода хелпера translate (или t). Это работает во вью и в контроллерах.

демонстрация HTML-безопасности в i18n

4.5. Переводы для моделей Active Record

Можете использовать методы Model.model_name.human и Model.human_attribute_name(attribute) для прозрачного поиска переводов для ваших моделей и имен атрибутов.

Например, когда добавляем следующие переводы:

en:
  activerecord:
    models:
      user: Customer
    attributes:
      user:
        login: "Handle"
      # переводит атрибут "login" у User как "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 предоставляет ряд пространств имен, куда можно поместить ваши переводы для передачи различных сообщений и переводы для определенных моделей, атрибутов и/или валидаций. Также учитывается одиночное наследование таблицы (single table inheritance).

Это дает довольно мощное средство для гибкой настройки ваших сообщений в соответствии с потребностями приложения.

Рассмотрим модель User с валидацией validates_presence_of для атрибута 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

Таким образом, в нашем примере он будет перебирать следующие ключи в указанном порядке и возвратит первый полученный результат:

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

Когда ваши модели дополнительно используют наследование, тогда сообщения ищутся в цепочке наследования.

Например, у вас может быть модель Admin, унаследованная от User:

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. Интерполяция сообщения об ошибке

Переведенное имя модели, переведенное имя атрибута и значение всегда доступны для интерполяции как model, attribute и value соответственно.

Так, к примеру, вместо сообщения об ошибке по умолчанию "cannot be blank" можете использовать имя атрибута как тут: "Please fill in your %{attribute}".

  • Где это возможно, 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 :in :in count
numericality :odd :odd -
numericality :even :even -

4.6. Перевод для тем писем Action Mailer

Если не передать subject в метод 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_select и select_month используют переведенные имена месяцев для заполнения результирующего тега select. Смотрите переводы в date.month_names. datetime_select также ищет опцию order из date.order (если вы передали эту опцию явно). Все хелперы выбора даты переводят prompt, используя переводы в пространстве имен datetime.prompts, если применимы.
  • Хелперы number_to_currency, number_with_precision, number_to_percentage, number_with_delimiter и number_to_human_size используют настройки формата чисел в пространстве имен number.
4.7.2. Методы Active Model
  • human_name и human_attribute_name используют переводы для имен модели и имен атрибутов, если они доступны в пространстве имен activerecord.models. Они также предоставляют переводы для имен унаследованного класса (т.е. для использования вместе с STI), как уже объяснялось выше в "Области сообщения об ошибке".

  • ActiveModel::Errors#generate_message (который используется валидациями Active Model, но также может быть использован вручную) использует human_name и human_attribute_name (смотрите выше). Он также переводит сообщение об ошибке и поддерживает переводы для имен унаследованного класса, как уже объяснялось выше в "Пространства имен сообщений об ошибке".

  • ActiveModel::Error#full_message и ActiveModel::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, позволяет хранить переводы как в формате чистого Ruby, так и в YAML.

Например, представляющий перевод хэш Ruby выглядит так:

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

Эквивалентный файл YAML выглядит так:

pt:
  foo:
    bar: baz

Как видите, в обоих случаях ключ верхнего уровня является локалью. :foo - это ключ пространства имен, а :bar - это ключ для перевода "baz".

Вот "реальный" пример из YAML файла перевода Active Support en.yml:

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 как часть данных локали, например, для специальных форматов дат.

6. Настройка I18n

6.1. Использование различных бэкендов

По некоторым причинам простой бэкенд, поставляющийся с Active Support, осуществляет только "простейшие вещи, в которых возможна работа" Ruby on Rails (или, цитируя Википедию, Интернационализация это процесс разработки программного обеспечения таким образом, что оно может быть адаптировано к различным языкам и регионам без существенных инженерных изменений. Локализация это процесс адаптации программы для отдельного региона или языка с помощью добавления специфичных для локали компонентов и перевод текстов), что означает то, что гарантируется работа для английского и, как побочный эффект, для схожих с английским языков. Также простой бэкенд способен только читать переводы, а не динамически хранить их в каком-либо формате.

Впрочем, это не означает, что вы связаны этими ограничениями. Гем Ruby I18n позволяет с легкостью заменить простой бэкенд на что-то иное, более предпочтительное для ваших нужд, передавая экземпляр бэкенда в сеттер I18n.backend=:

Например, можно заменить простой бэкенд на бэкенд Chain для связывания нескольких бэкендов вместе. Это полезно, когда используются стандартные переводы с помощью простого бэкенда, а хранятся собственные переводы приложения в базе данных или других бэкендах.

С помощью бэкенда Chain можно использовать бэкенд Active Record и вернуться к простому бэкенду (по умолчанию):

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

6.2. Использование различных обработчиков исключений

API I18n определяет следующие исключения, вызываемые бэкендами, когда происходят соответствующие неожидаемые условия:

MissingTranslationData       # не обнаружен перевод для запрашиваемого ключа
InvalidLocale                # локаль, установленная I18n.locale, невалидна (например, nil)
InvalidPluralizationData     # была передана опция count, но данные для перевода не могут быть возведены во множественное число
MissingInterpolationArgument # перевод ожидает интерполяционный аргумент, который не был передан
ReservedInterpolationKey     # перевод содержит зарезервированное имя интерполяционной переменной (т.е. scope, default)
UnknownFileType              # бэкенд не знает, как обработать тип файла, добавленного в I18n.load_path

API I18n поймает все эти исключения, когда они были вызваны в бэкенде, и передаст их в метод default_exception_handler. Этот метод вызовет заново все исключения, кроме исключений MissingTranslationData. Когда было вызвано исключение MissingTranslationData, он возвратит строку сообщения об ошибке исключения, содержащую отсутствующие ключ/пространство имен.

Причиной для этого является то, что при разработке вам обычно хочется, чтобы вью рендерились несмотря на отсутствующие переводы.

Впрочем, в иных ситуациях, возможно, захочется изменить это поведение. Например, обработка исключений по умолчанию не позволяет просто ловить отсутствующие переводы во время автоматических тестов. Для этой цели может быть определен иной обработчик исключений. Определенный обработчик исключений должен быть методом в модуле I18n или классом с методом call:

module I18n
  class JustRaiseExceptionHandler < ExceptionHandler
    def call(exception, locale, key, options)
      if exception.is_a?(MissingTranslation)
        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?(MissingTranslation) && key.to_s != 'i18n.plural.rule'
  raise exception.to_exception
else
  super
end

Другим примером, когда поведение по умолчанию является менее желательным, является Rails TranslationHelper, который предоставляет метод #t (то же самое, что #translate). Когда в этом контексте происходит исключение MissingTranslationData хелпер оборачивает сообщение в span с классом CSS translation_missing.

Чтобы это осуществить, хелпер заставляет I18n#translate вызвать исключения, независимо от того, какой обработчик исключений установлен, определяя опцию :raise:

I18n.t :foo, raise: true # всегда перевызывает исключения из бэкенда

7. Перевод контента модели

API I18n, описанный в этом руководстве, в первую очередь предназначен для перевода строк интерфейса. Если необходимо перевести контент модели (например, сообщений в блоге), может понадобится другое решение, помогающее в этом.

Несколько гемов, которые могут помочь:

  • Mobility: Предоставляет поддержку для хранения переводов во многих форматах, включая таблицы перевода, столбцы JSON (PostgreSQL) и т.д.
  • Traco: Переводимые столбцы, хранимые в самой таблице моделей

8. Заключение

С этого момента у вас должно быть хорошее понимание, как работает поддержка I18n в Ruby on Rails, и вы должны быть готовы начать переводить свой проект.

9. Вклад в Rails I18n

Поддержка I18n в Ruby on Rails была представлена в релизе 2.2 и до сих пор развивается. Проект следует хорошим традициям разработки Ruby on Rails в виде первоначального развития в виде отдельных гемов и реальных приложений, и только затем извлечения наилучших широко используемых особенностей для включения в ядро.

Поэтому каждый поощряется экспериментировать с новыми идеями и особенностями в гемах или других библиотеках и делать их доступными сообществу. (Не забудьте анонсировать свою работу в рассылке!)

Если вы обнаружите, что ваша локаль (язык) отсутствует в данных примеров переводов репозитория Ruby on Rails, сделайте fork репозитория, добавьте ваши данные и пошлите pull request.

10. Ресурсы

  • Группа Google: rails-i18n - Рассылка проекта.
  • GitHub: rails-i18n - Репозиторий кода и трекер проблем для проекта rails-i18n. Много важного можно найти в примере переводов для Rails, в большинстве случаев это будет работать и в вашем приложении.
  • GitHub: i18n - Репозиторий кода и трекер проблем для гема i18n.

11. Авторы