Rails translation missing error

Header photo by @joaosilas This post has also been translated into Spanish by Ibidem Group....

Cover image for The Basics of Rails I18n - Translate errors, models, and attributes

Risa Fujii

Risa Fujii

Posted on Jan 5, 2021

• Updated on May 6, 2021

Header photo by @joaosilas

This post has also been translated into Spanish by Ibidem Group. Traducción a Español aquí: https://www.ibidemgroup.com/edu/traducir-rails-i18n/

The i18n (internationalization) API is the standard way to support localization in Rails. The official guide has all the information you need, but it’s also very long. This post is based on the notes I took when I was first learning how to set up i18n, and my goal here is to provide a more approachable walkthrough. This post covers the YAML file setup for error messages and model names/attribute names, and lookup using the I18n.t method.

Why I wrote this post

My first exposure to i18n was the rails-i18n gem, which provides default translations for commonly used error messages, days of the week, etc. While you can install this gem and call it a day, knowing how to set up i18n yourself is necessary if:

  • you want to use translations that are different from those provided by rails-i18n
  • you want to include languages not covered by rails-i18n
  • you only need translations for a few languages, and don’t want to install the whole gem including dozens of languages

In my case, it was the third reason — I only needed translations for Japanese.

Table of Contents

  • 0. Setting your default locale
  • 1. Translating model and attribute names

    • 1.1 Defining your translations
    • 1.2 Accessing model and attribute translations
  • 2. Translating ActiveRecord errors

    • 2.1 Error message breakdown
    • 2.2 Defining your translations

0. Setting your default locale

I set mine to Japanese.

# config/application.rb
config.i18n.default_locale = :ja

Enter fullscreen mode

Exit fullscreen mode

1. Translating model and attribute names

Docs: https://guides.rubyonrails.org/i18n.html#translations-for-active-record-models

1.1 Defining your translations

First, define translations for your model names and attributes in a YAML file like below. This example is for a User model with two attributes.

# config/locales/ja.yml
ja:
  activerecord:
    models:
      user: 'ユーザ' # <locale>.activerecord.models.<model name>
    attributes:
      user:
        name: '名前' # <locale>.activerecord.attributes.<model name>.<attribute name>
        password: 'パスワード'

Enter fullscreen mode

Exit fullscreen mode

1.2 Accessing model and attribute translations

# How to look up translations for model names
User.model_name.human
=> "ユーザ"

# How to look up translations for attributes (you can use symbols or strings)
User.human_attribute_name('name')
=> "名前"

User.human_attribute_name(:name)
=> "名前"

Enter fullscreen mode

Exit fullscreen mode

2. Translating ActiveRecord errors

Docs: https://guides.rubyonrails.org/i18n.html#error-message-scopes

2.1 Error message breakdown

Translating error messages is a bit more complicated than models. Let’s talk about the error message structure first. ActiveRecord has some built-in validation errors that are raised if your record is invalid. Consider this example:

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

Enter fullscreen mode

Exit fullscreen mode

If your locale is :en, this error message is returned when you try to create an invalid record.

User.create!(name: '')
=> ActiveRecord::RecordInvalid: Validation failed: Name can't be blank

Enter fullscreen mode

Exit fullscreen mode

This message actually consists of a few parts.

1. ActiveRecord::RecordInvalid:

The error type, which is the same regardless of your locale. No translation is required.

2. Validation failed:

The description for this RecordInvalid error in English, defined in the Rails gem’s en.yml (source code). Translation is required for your locale(s).

3. Name can't be blank

This is the error message for records that violate presence: true. It consists of two parts — the attribute Name and message can't be blank (source code). It turns out, the default error message format is an interpolation of two elements: "%{attribute} %{message}" (source code). Translation is required for your locale(s).

Note: The translation for attribute will be taken from the model attribute translations defined in Section 1. If there’s no translation, Rails will print the attribute name in English.

What happens if you change the locale from :en to :ja without defining the corresponding translations? The error message is returned as translation missing.

User.create!(name: '')
# => ActiveRecord::RecordInvalid: translation missing: ja.activerecord.errors.messages.record_invalid

Enter fullscreen mode

Exit fullscreen mode

So let’s look at how to provide translations next.

2.2 Defining your translations

According to the official guide, there are a few places that Rails will look in order to find a translation for your error, in this order.

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

This means that if you want to set model-specific or attribute-specific error messages, you can do so too. But in this case, let’s say we want the record_invalid and blank to be translated the same way regardless of the model. Here is a sample configuration:

# config/locales/ja.yml

ja:
  activerecord:
    errors:
      messages:
        record_invalid: 'バリデーションに失敗しました: %{errors}'
  errors:
    format: '%{attribute}%{message}'
    messages:
      # You should also include translations for other ActiveRecord errors such as "empty", "taken", etc.
      blank: 'を入力してください'

Enter fullscreen mode

Exit fullscreen mode

About the rails-i18n gem

The configuration above is taken from the ja.yml file in the rails-i18n gem which I mentioned in the intro. Installing this gem is a quick way to set up default translations. It does not come pre-installed in your Rails project, so check the documentation for more details on installation and usage.

2.3 Accessing your translations with I18n.t

Now that you’ve provided translations for error messages, Rails will actually print the error messages instead of translation missing.

The next question is, how can you look up the translations you defined? For example, what if you want to assert that some message is being raised in a test?

test 'user is invalid if name is blank' do
  invalid_user = User.new(name: '')
  assert invalid_user.errors.messages[:name].include?(<cannot be blank message>)
end

Enter fullscreen mode

Exit fullscreen mode

This is where the very convenient I18n.t method comes in. The t stands for «translate», and it allows you to access any translation defined in your YAML files. For this example, we want to access the errors.messages.blank message (refer to 2.2 for the YAML file). There are two ways to do this.

I18n.t('errors.messages.blank')
# => "を入力してください"

I18n.t('blank', scope: ['errors', 'messages'])
# => "を入力してください"

Enter fullscreen mode

Exit fullscreen mode

Just like that, you can look up any translation you’ve defined!

Note: You can look up model names and attribute names without using the human method too, like I18n.t('activerecord.models.user').

test 'user is invalid if name is blank' do
  invalid_user = User.create(name: '')
  expected_error = I18n.t('errors.messages.blank')
  assert invalid_user.errors.messages[:name].include?(expected_error)
end

Enter fullscreen mode

Exit fullscreen mode

2.4 Looking up errors with string interpolation

https://guides.rubyonrails.org/i18n.html#error-message-interpolation

If you take a look at any of the YAML files in the rails-i18n gem, you may notice that some messages use string interpolation. For example, if your validation error message is for greater_than, you would want to say must be greater than %{count}, and fill in the number for count. Rails will fill it in for you when the actual error is raised, but how can we fill in the count when you look up the error message using I18n.t?

I18n.t('errors.messages.greater_than')
# => "は%{count}より大きい値にしてください"

Enter fullscreen mode

Exit fullscreen mode

You can just pass it in as an argument:

I18n.t('errors.messages.greater_than', count: 5)
# => "は5より大きい値にしてください"

Enter fullscreen mode

Exit fullscreen mode


I know this doesn’t come close to covering everything you can do with i18n in Rails, but I hope it provides a useful introduction. Thanks for reading!

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

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

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

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

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

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

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

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

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

Наиболее важными методами 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 с самого начала, в следующих главах!

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

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

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

Локаль по умолчанию en.yml в этой директории содержит образец строки перевода:

Это означает, что в локали :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 приложения, не перезапишет переводы из внешних гемов.

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

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

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

Одним из вариантов, которым можно установить локаль, является доменное имя, на котором запущено ваше приложение. Например, мы хотим, чтобы 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 (или пути запроса).

Наиболее обычным способом назначения (и передачи) локали будет включение ее в параметры 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.)

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

around_action :switch_locale

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

Когда локаль не была установлена явно для запроса (например, с помощью одного из представленных выше методов), приложение должно попытаться определить требуемую локаль.

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 предоставляют решения этой проблемы.

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

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

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

Хорошо! Вы уже инициализировали поддержку 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

В нашем коде есть две строки на английском, которые будут рендериться пользователям в нашем отклике («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">.

Добавим отсутствующие переводы в файлы словарей:

# 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

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

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

Хорошо! Теперь давайте добавим временную метку во вью, чтобы продемонстрировать особенности локализации даты/времени. Чтобы локализовать формат даты, нужно передать объект 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/, они автоматически станут готовыми для использования.

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

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

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

При использовании дефолтного 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}')]

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

Эти главы покажут примеры использования как метода I18n.translate, так и метода хелпера вью translate (отметив дополнительные функции, предоставленными методом хелпера вью).

Раскроем особенности такие, как:

  • поиск переводов
  • интерполяция данных в переводы
  • множественное число у переводов
  • использование HTML-безопасных переводов (только метод хелпера вью)
  • локализация дат, номеров, валют и т.п.

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

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]

Когда задана опция :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'

Чтобы найти несколько переводов за раз, может быть передан массив ключей:

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

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

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

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

Автоматическое ограничение перевода доступно только из вспомогательного метода вью translate.

«Ленивый» поиск также может быть использован в контроллерах:

en:
  books:
    create:
      success: Book created!

Это может быть полезным для установки сообщений флеш:

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

Во многих языках — включая английский — есть только две формы, единственного числа и множественного числа, для заданной строки, т.е. «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.

Гем 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 может быть использован для обеспечения более полного набора локализованных правил множественного числа.

Локаль может быть либо установленной псевдо-глобально в 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

Ключи с суффиксом _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

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

Сообщение об ошибке валидации 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

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

Переведенное имя модели, переведенное имя атрибута и значение всегда доступны для интерполяции как 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

Если не передать 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!"

Rails использует фиксированные строки и другие локализации, такие как формат строки и другая информация о формате, в ряде хелперов. Вот краткий обзор.

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

  • Array#to_sentence использует настройки формата, которые заданы в пространстве имен support.array.

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

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

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

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

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

По некоторым причинам простой бэкенд, поставляющийся с 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)

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: Переводимые столбцы, хранимые в самой таблице моделей

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

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

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

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

  • Группа Google: rails-i18n — Рассылка проекта.
  • GitHub: rails-i18n — Репозиторий кода и трекер проблем для проекта rails-i18n. Много важного можно найти в примере переводов для Rails, в большинстве случаев это будет работать и в вашем приложении.
  • GitHub: i18n — Репозиторий кода и трекер проблем для гема i18n.
  • Sven Fuchs (первоначальный автор)
  • Karel Minařík

Verifying that all user-facing copy in your Rails application is localized can be a challenge. While manual validation may work for smaller sites, for larger, more complex applications it can be practically a fool’s errand. If your situation happens to sound more like the latter, don’t lose hope! In this post I am going to cover a few of the methods to easily and automatically test your Rails app for missing translations.

Fail Early

If you’re using Rails 4.1.0 or above I recommend adding some basic config to your test.rb and development.rb Rails environments.

# config/environments/test.rb
# config/environments/development.rb
Rails.application.configure do |config|
  config.action_view.raise_on_missing_translations = true
end

This config will make the t() translation helper raise an exception on encountering a missing translation rather than falling back to its default behavior.

This can be especially helpful for those cases where it may not be so obvious that you have a missing translation. For example, even if t(‘.hello’) appears to be translated, it may actually be outputting <span class=”translation_missing”>Hello</span>. Though this missing translation would be hidden from your English speaking users, that won’t be the case for everyone else. For this reason I prefer for my translation and localization helpers to fail early and fail loudly.

For information regarding raising exceptions on missing translations in past versions of Rails, I recommend reading Thoughtbot’s post on Foolproof I18n Setup in Rails.

Perform Static Code Analysis

The next line of defense in our war on missing translations is through the usage of a static i18n analysis tool such as the i18n-tasks gem. Although i18n-tasks has many great features, the one that I have found most valuable is its ability to find missing translations in both Ruby and Javascript code (assuming you’re using i18n-js) across all of your locale files. The beauty of static code analysis is that it will cover all of your code even if you may not quite have 100% test coverage.

To start using the i18n-tasks gem, just follow the installation instructions in the gem’s README.

In order to avoid false positives, you may need to add a few lines of additional configuration to the i18n-tasks.yml file. For example, if you’re invoking the translation helper from any non-view files (eg. presenters, helpers, etc.) and relying on relative roots (like ‘.help’), it’s important that you add these root paths to your i18n-tasks.yml file.

Additionally, if you’re relying on any gems that include their own locale files, you will want to add these to the search paths in your config as well.

# i18n-tasks.yml
# ...other config...
data:
  read:
    - "<%= %x[bundle show gem_name].chomp %>/config/locales/%{locale}.yml"
    - config/locales/%{locale}.yml
# ...other config...
search:
  relative_roots:
    - app/views
    - app/presenters
    - app/helpers
# ...other config...

Feel free to use ERB in the i18n-tasks.yml file since the gem will automatically parse it with Erubis before loading the YAML config.

One caveat to i18n-tasks (and static code analysis in general) is that it won’t work well for dynamically generated code. For our purposes, that means that the gem will fail to detect missing translations resulting from dynamic translation keys (ie. t(“errors.#{ error_name }.description”)). Now although I understand the appeal of dynamic translation keys, I advise being explicit with your translation keys. Just skip the interpolation altogether and use a case statement to select the correct key. Sure your codebase will be a few lines longer and your code will be slightly more verbose, but you’ll help mitigate the risk of having one of you users encounter a missing translation in production.

Write Automated Feature Specs

You may find it valuable to have some automated feature specs hitting your application in all of your supported locales using RSpec, Capybara, and capybara-webkit (this is a WebKit driver for Capybara that will allow your feature specs to execute your application’s Javascript code).

Keep in mind that a comprehensive suite of feature specs will be expensive both in terms of developer time and execution time, so it really is a judgement call as to whether or not they’ll provide enough value to your project to justify the expense. However, if you do decide to write some, remember that they will likely be slow so I recommend reserving them for your Jenkins/Travis CI builds or whatever form of continuous integration you use.

For those of you still using RSpec 2.x, I recommend following the setup instructions on this blog post. If you’ve been proactive and upgraded to RSpec 3.x, I still recommend following those instructions, but just know that you’ll have to make a few minor tweaks to get it working correctly. Just follow an RSpec 3 upgrade guide and the process shouldn’t be too painful. Also, just as a heads up, the code samples that follow were written using rspec 3.1.0, capybara 2.4.3, and capybara-webkit 1.3.0.

Once you’ve got everything set up, there are several ways to go about finding missing translations in your application. First, especially for your most critical content, you may want to explicitly test that it is translated. For example, say you want to test that your site is displaying the appropriate welcome message on its home page based on the language settings of your user’s browser. Here is one way of doing that:

# spec/features/home_page_spec.rb
require 'rails_helper'

RSpec.describe 'user views home page', :type => :feature do
  # Configure capybare to set the 'Accept-Language' header to the appropriate locale
  # and set the locale that is used for the expected result comparison.
  before do
    page.driver.header 'Accept-Language', locale
    I18n.locale = locale
  end

  context 'when the user has set their locale to :en' do
    let(:locale) { :en }

    it 'displays a translated welcome message to the user', :js => true do
      visit(root_path)
      expect(page).to have_content I18n.t('home.index.welcome')
    end
  end

  context 'when the user has set their locale to :zh' do
    let(:locale) { :zh }

    it 'displays a translated welcome message to the user', :js => true do
      visit(root_path)
      expect(page).to have_content I18n.t('home.index.welcome')
    end
  end
end

If you are less concerned about specific translations and just want to be alerted of missing translations in general, then I suggest just using Capybara to check the page body for the indicators that the translation libraries attach such as translation_missing class that’s inserted by the regular Rails t() helper or the [missing “en.whatever” translation] text that i18n-js inserts. One way of doing this is by writing a custom RSpec Matcher. Below is an example of what a basic missing translations matcher could look like.

# spec/support/missing_translations.rb
require 'rspec/expectations'

RSpec::Matchers.define :have_missing_translations do

  match do |actual|
    # Default missing translation fallback for i18n-js
    missing_i18n_js = /[missing "S*" translation]/

    # Default missing translation fallback for the Rails t() helper
    missing_rails_t = /class="translation_missing"/

    # Default missing translation fallback for I18n.t
    missing_i18n_t  = /translation missing: S*.S*/

    !!(actual.body.match(missing_rails_t) ||
       actual.body.match(missing_i18n_t)  ||
       actual.body.match(missing_i18n_js))
  end

  failure_message do
    'expected page to have missing translations'
  end

  failure_message_when_negated do
    'expected page to not have missing translations'
  end
end

Simply require 'support/missing_translations' in your spec_helper.rb` file and you’re ready to use it throughout your specs.

# spec/features/home_page_spec.rb
require 'rails_helper'

RSpec.describe 'user views home page', :type => :feature do
  # Configure capybare to set the 'Accept-Language' header to the appropriate locale
  # and set the locale that is used for the expected result comparison.
  before do
    page.driver.header 'Accept-Language', locale
    I18n.locale = locale
  end

  context 'when the user has set their locale to :en' do
    let(:locale) { :en }

    it 'should not have missing translations', :js => true do
      visit(root_path)
      expect(page).not_to have_missing_translations
    end
  end

  context 'when the user has set their locale to :zh' do
    let(:locale) { :zh }

    it 'should not have missing translations', :js => true do
      visit(root_path)
      expect(page).not_to have_missing_translations
    end
  end
end

Well that just about wraps it up. Like I said earlier, testing that everything in your app is properly localized is hard. The techniques above can make it just a little bit easier, though. I’d like to finish this post with some links to addition i18n resources that you may find helpful. :)

Additional i18n Resources

  • i18n on Rails: A Twitter Approach
  • Twitter CLDR
  • W3C Internationalization Article List

Contents

  • 1 Localisation i18n translations
  • 2 Locale
  • 3 Translating user content
  • 4 Enums
  • 5 Tips

Localisation i18n translations

Tips https://devhints.io/rails-i18n
Translate models using activerecord
https://guides.rubyonrails.org/i18n.html#translations-for-active-record-models
so you can use

User.model_name.human
# 'asd'.pluralize # asds so for english that is ok but if you use i18n, than use
# note that you have to write translations in yml.file if you use human(count)
User.model_name.human(count: 2)
# attribute
User.human_attribute_name(:email)

To translate active record messages for specific attributes, you can overwrite
messages for specific model and attributes (default ActiveRecord messages taken)
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/locale/en.yml#L23
https://apidock.com/rails/v4.2.7/ActiveModel/Errors/generate_message
For submit buttons default title is defined on submit_default_value https://github.com/rails/rails/blob/main/actionview/lib/action_view/helpers/form_helper.rb#L2573
(I found it using byebug and step) so to use in test

click_on "#{:update.to_s.humanize} #{User.model_name.human}"

Also you can change format errors.format: Polje "%{attribute}" %{message}
https://github.com/rails/rails/blob/master/activemodel/lib/active_model/locale/en.yml#L4

And you can change attribute name activerecord.attributes.user.email: имејл
To translate also plurals you can use User.model_name.human(count: 2). For
attributes you can use User.human_attribute_name("email")
link
For ApplicationRecord translate activerecord.
For form objects include ActiveModel::Model you should translate
activemodel. There you can use t('successfully') instead
I18n.t('successfully') if you include AbstractController::Translation
This will also translate error messages, for example
landing_signup.errors.add(:current_city) = 'x' will result in message like
landing_signup.errors.full_messages.to_sentence # 'Који је твој град ? x'

# config/locales/activerecord_activemodels.en.yml
en:
  activerecord:
    models:
      user:
        zero: No dudes
        one: Dude
        other: Dudes
      customer:
        one: корисник
        other: корисници
        accusative: корисника
        some_customer_message: Моја порука
  activemodel:
    attributes:
      landing_signup:
        current_city: Који је твој град ?
    errors:
      messages:
        group_not_exists_for_age: Не постоји група (%{age}год) на овој локацији
      models:
        landing_signup:
          attributes:
            current_city:
              blank: Не може бити празно ?

Separate translations into different files (for example
activerecord_activemodels.sr.yml) include them with:

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

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

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

To raise error when translation is missing .translation_missing class

# config/application.rb
config.action_view.raise_on_missing_translations = true

This way you can change default_locale and run system tests and you will get
exception if some locale is missing.

No need to write quotes in yml unless you have:

  • colon : , than simply escape it’s meaning and use quotes name: "name: Ime"
  • start with %{} or , or .
  • have answer: Yes or answer: No, is somehow casts to true or false
  • have double quotes inside processing: <i class="fa fa-spinner fa-spin
    datatable-spinner"></i>Обрада...
    . Than you need to switch to single quotes.
    Note that it does not help to wrap double quotes with single quote, this won’t
    work: processing: '<i class="c"></i>' since you have double quote inside.

When debugging SyntaxError: [stdin]:60:33: unexpected identifier you should
run rake tmp:clear so all yml end .erb files are compiled again.

If you want to reuse same translation you can use alias, but only inside same
file, so in case of form object, you can add to activerecord_models.yml

sr:
  activerecord:
    attributes:
      subscriber: &subscriber_attributes
        name: Назив
      project: &project_attributes
        name: Назив
  activemodel:
    attributes:
      project_task_notification:
        <<: *project_attributes
        <<: *subscriber_attributes
        project_name: Назив

For custom errors can be different for each attribute or same. Can also accept
param, for example

    errors.add :from_group_age, :group_not_exists_for_age, age: age

https://stackoverflow.com/questions/6166064/i18n-pluralization
For serbian you can provide pluralization

# config/locales/plurals.rb
# https://github.com/svenfuchs/i18n/blob/master/test/test_data/locales/plurals.rb
serbian = {
  i18n: {
    plural: {
      keys: %i[one few many other],
      rule: lambda { |n|
        if n % 10 == 1 && n % 100 != 11
          :one
        elsif [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100)
          :few
        # elsif (n % 10).zero? || [5, 6, 7, 8, 9].include?(n % 10) || [11, 12, 13, 14].include?(n % 100)
        #   :many
        # there are no other integers, use :many if you need to differentiate
        # with floats
        else
          :other
        end
      }
    }
  }
}
{
  sr: serbian,
  'sr-latin': serbian,
}
# config/initializers/pluralization.rb
require 'i18n/backend/pluralization'
I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)

If you send count parameter t('sent_messages', count: 2) it will try to pick
specific item based on number

# config/locales/sr.yml
sr:
  sent_messages:
    # 1, 21, 31 ...
    one: %{count} порука је послата
    # 2, 3, 4, 22, 23, 24, 32, 33, 34 ...
    few: %{count} поруке су послате
    # all other integers: 5, 6, ... 9, 10, 11, 12, 13, 14 ... 20, 25, ...
    many: %{count} порука је послато

Note that you have to provide few translation for all words, since it could
happend that count is 2 and translation is missing.

I18n.t 'sent_messages', count: 15
# or if you want to translate model
"#{chat.moves.size} #{Move.model_name.human count: chat.moves.size}"

You can translate to any language with

I18n.t 'sent_messages', locale: :sr

Example for Serbian localizations translations:

# config/locales/sr.yml
sr:
  # https://github.com/rails/rails/blob/master/activemodel/lib/active_model/locale/en.yml#L8
  errors:
    format: Поље "%{attribute}" %{message}
    messages:
      blank: не сме бити празно
      invalid: није исправно
  neo4j:
    errors:
      messages:
        required: мора постојати
        taken: је већ заузет
    models:
      user: корисник
      location:
        one: локација
        other: локације
    attributes:
      user:
        email: Имејл
        password: Лозинка
        password_confirmation: Потврда лозинке
        remember_me: Запамти ме

When you use .capitalize, .titleize or .upcase than you need first to call
.mb_chars. For example

'ž'.upcase
=> "ž"

'ž'.mb_chars.upcase.to_s
 => "Ž"

Some common words translations can be found
https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/en.yml

To translate with accusative you need to joins strings or use param in
translation

module TranslateHelper
  # there are two ways of calling this helper:
  # t_crud 'are_you_sure_to_remove_item', item: @move
  # t_crud 'edit', User
  def t_crud(action, model_class)
    if model_class.class == Hash
      t(action, item: t("neo4j.models.#{model_class[:item].name.downcase}.accusative"))
    else
      "#{t(action)} #{t("neo4j.models.#{model_class.name.downcase}.accusative")}"
    end
  end
end

Translate latin to cyrilic with https://github.com/dalibor/cyrillizer You need
to set language in config

# Gemfile
# translate cyrillic
gem 'cyrillizer'
# config/initializers/cyrillizer.rb
Cyrillizer.language = :serbian

In console

'my string'.to_cyr
 => "мy стринг"

Note that some chars looks the same but are not when rendered on html page

 # for example first line is not correct link a href
 <a href='%{confirmation_url}'>Поново пошаљи упутство за потврду</а>"
 <a href='%{confirmation_url}'>Поново пошаљи упутство за потврду</a>"

To check if word is using cyr you can use (note that it looks the same but are
not same charcters)

Cyrillizer.alphabet.keys.include? 'a'
 => false 
2.6.3 :026 > Cyrillizer.alphabet.keys.include? 'а'
 => true 

Locale

When changing locale I18n.locale = :sr in some methods, note that this is
global variable in thread, so when you have 5 puma threads than on GET requests
(simply refresh couple of times) you will get different locales.
Here is my Rails controbution to guide about it https://github.com/rails/rails/pull/34911
One way is to use I18n.with_locale for example

class UserMailer < ActionMailer::Base
  default from: '[email protected]'

  def invitation(user)
    I18n.with_locale(user.locale) do
      mail subject: t('invitation'), to: user.email
    end
  end
end

For controller, you need to use around filters

  around_action :set_locale_from_session

  def set_locale_from_session
    if session[:locale].present?
      I18n.with_locale session[:locale].to_sym do
        yield
      end
    else
      yield
    end
  end

Translating user content

https://github.com/shioyama/mobility#quickstart

# Gemfile
# translation
gem 'mobility', '~> 0.8.6'

# this will generate config/initializers/mobility.rb
rails g mobility:install

# app/models/activity.rb
class Activity < ApplicationRecord
  extend Mobility
  translates :name
end

# in migration add default value
  create_table :activities, id: :uuid do |t|
    t.json :name, default: {}

For google translate look for two scripts, one for vim and one for whole yml.
https://github.com/duleorlovic/config/tree/master/ruby

fallbacks

club.name fallback: false
club.name fallback: [:en]

If fallback is not false than longer translate will fallback to short
automatically ('sr-latin' to :sr)

Note that passing locale options to reader or using locale accessors will
disable fallbacks

word.meaning(locale: :de)
#=> nil
word.meaning_de
#=> nil
Mobility.with_locale(:de) { word.meaning }
# if in model we have translate :meaning, fallbacks: { de: :ja}
#=> "(名詞):動きやすさ、可動性"

Global fallback

# config/initializers/mobility.rb
  config.default_options[:fallbacks] = { sr: :en, en: :sr }

Dynamic fallback https://github.com/shioyama/mobility/pull/328
https://github.com/shioyama/mobility/issues/314

You can search find_by using @> and passing json

Activity.where("name @> ?",{en: 'Climbing'}.to_json)
# or using i18n scope
Activity.i18n.where(name: 'Climing')

# or using specific locale
Mobility.with_locale(:en) do
  Mobility.locale # => :en
  Activity.i18n.find_by(name: 'Climbing')
end

Enums

# app/models/user.rb
class User < ApplicationRecord
  enum status: [:active, :pending, :archived]
end

# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  def self.human_enum_name(enum_name, enum_value)
    I18n.t("activerecord.attributes.#{model_name.i18n_key}.#{enum_name.to_s.pluralize}.#{enum_value}")
  end
end


# config/locales/activerecord.en.yml
en:
  activerecord:
    attributes:
      user:
        statuses:
          active: "Active"
          pending: "Pending"
          archived: "Archived"

# usage:
User.human_enum_name(:status, :pending)
User.human_enum_name(:status, user.status)

Tips

  • if you want to see which translation is used (if there is parent default
    value) you can try https://github.com/fphilipe/i18n-debug
  • for big content pages, you can translate using page.sr.html.erb instead of
    yml translations
  • fallbacks, if can not find the key in curreny localy, it can search in
    fallback locale. In this case you can translate only specific keys

    # config/application.rb
    config.i18n.fallbacks = { en_GB: [:en] }
    
    # config/locales/en_GB.yml
    en_GB:
      soccer: Footbal
    
    en:
      soccer: Soccer
      words: Words
    
    # logs from i18n-debug
    en_GB.show.soccer => 'Footbal'
    en_GB.show.words => nil
    en.show.words => 'Words'
    
  • To see Rails default datetime formats go to
    https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/en.yml
    to see current translation you can use

    I18n.translate 'date.formats.default`
    => "%Y-%m-%d"
    

    Use can check formats https://apidock.com/ruby/DateTime/strftime with

    Time.zone.now.strftime '%e %B'
    
  • to localise date and timestamps you can use I18n.l Time.now, format: :formal

    en:
      time:
        formats:
        default: '%H:%M'
        formal: The time is %H:%M
    
    
  • helper.number_with_precision, distance_of_time_in_words_to_now,
    number_to_currency, number_to_human, number_to_human_size are some
    helpers that uses translations
  • use _html suffix when you use tags in translated content. For example
    title_html: Hi <b>man</b> and <%= t('title_html') %> will not escape and
    you do not need to use html_safe or raw
  • to add route instead of subdomain, use https://stackoverflow.com/a/8237800
    # config/routes.rb
      devise_for :users, only: :omniauth_callbacks, controllers: {
        omniauth_callbacks: 'devise/my_omniauth_callbacks',
      }
      scope '(:locale)', locale: /#{I18n.available_locales.join("|")}/ do
        root 'pages#home'
    
        devise_for :users, skip: :omniauth_callbacks, controllers: {
          registrations: 'devise/my_registrations',
        }
      end
    
    # config/
    

In July we published a series of articles on internationalization for PHP, this month we will focus on tutorials about internationalization for Ruby, starting out with a comprehensive tutorial on getting started with the i18n gem and various frameworks. If you missed any of the prior articles, you can have a look here:

  • PHP i18n mechanisms
  • PHP i18n with gettext
  • i18n in 5 most popular PHP frameworks
  • i18n in Laravel and FuelPHP

Localization and Internationalization are recurring themes in our blog posts, if you are not 100% sure what they refer to, you can find an explanation in this article.

ruby

For August, we have prepared a series of articles, and to start out we will demonstrate how internationalization can be achieved in the Ruby world. Ruby is a dynamic, reflective, general-purpose object-oriented programming language that combines syntax inspired by Perl with Smalltalk-like features. It was also influenced by Eiffel and Lisp. Ruby was first designed and developed in the mid-1990s by Yukihiro «Matz» Matsumoto in Japan. [1]

The topics covered in this article are:

  • i18n for plain Ruby & the i18n gem
  • i18n for Ruby on Rails
  • i18n for Sinatra
  • i18n for Padrino

Since the emergence of web application frameworks like Ruby on Rails and DSL-s like Sinatra, Ruby has been used to reach a wide international public. Early in August 2013, there were nearly 200.000 websites world-wide using Ruby on Rails [2] alone. There is no better way of reaching a wide international audience then in their own individual languages, and this series of articles will show you how. Let’s start with the basics and then we will move on to the details.

Internationalization for Ruby: The Ruby i18n gem

One of the most popular Ruby gems for internationalization is Ruby I18n. It allows translation and localization, interpolation of values to translations, pluralization, customizable transliteration to ASCII, flexible defaults, bulk lookup, lambdas as translation data, custom key/scope separator, and custom exception handlers.

The gem is split in two parts: the public API and a default backend (named Simple backend). Other backends can be used, such as Chain, ActiveRecord, KeyValue or a custom backend can be implemented.

YAML (.yml) or plain Ruby (.rb) files are used for storing translations in SimpleStore, but YAML is the preferred option among Ruby developers.

Internationalization and the YAML resource file format

YAML is a human-readable data serialization format. Its syntax was designed to be easily mapped to data types common to most high-level languages (lists, associative arrays and scalars). Unlike some other formats, YAML has a well defined standard.

Key features of YAML resource file format are:

  • the information is stored in key-value pairs delimited with colon ( : )
  • keys can be (and usually are) nested (scoped)
  • i18n expects the root key to correspond to the locale of the content, for example ‘en-US’ or ‘de’
  • the «leaf key» (the one that has no «children» keys) has to have some value
  • values can be escaped
  • correct and consistent line indentation is important for preserving the key hierarchy
  • lines starting with a hash sign ( # ) preceded with any number of white-spaces are ignored by the parser (treated as a comment)
  • place-holder syntax is: %{name}, where “name” can consist of multiple non-white-space characters
  • UTF-8 encoding is usually used for YAML resource files

Before we move on to demonstrate the I18n methods, lets first create an example yaml file that we will load and test:

GIST

Installation and setup

gem install i18n

After the gem installation, change the directory to the location where the sample yaml file was saved and start the irb (interactive ruby shell). The first step is requiring the library:

2.0.0p247 :001 > require 'i18n'  => true

Next, we can check the current locale. By default, it is English.

2.0.0p247 :002 > I18n.locale  => :en

Changing it to something else is easy:

2.0.0p247 :003 > I18n.locale = :de  => :de

Translation lookup

Translation lookup is done via the translate method of I18n. There is also a shorter alias available: I18n.t. Let’s now try to lookup one of the phrases from our yaml file example:

2.0.0p247 :004 > I18n.translate :world, :scope => 'greetings.hello'  => "translation missing: en.hello.world"

The translation is missing, because we have not loaded the file. Lets load all the .yaml and .rb files in the current directory:

2.0.0p247 :005 > I18n.load_path = Dir['./*.yml', './*.rb']  => ["./en.yml"]

and then we retry accessing the English translation with the key ‘world’:

2.0.0p247 :006 > I18n.translate :world, :scope => 'greetings.hello'  => "Hello world!"

When we asked for this translation, we did not pass any locale, so I18n.locale was used. A locale can be explicitly passed:

2.0.0p247 :007 > I18n.translate :world, :scope => 'greetings.hello', :locale => :en  => "Hello world!"

When passing the phrase key, a symbol or a string can be used, and a scope can be an array or dot-separated. Also all combinations of these are valid, so the following calls are equivalent:

I18n.translate 'greetings.hello.world' I18n.translate 'hello.world', :scope => :greetings I18n.translate 'hello.world', :scope => 'greetings' I18n.translate :world, :scope => 'greetings.hello' I18n.translate :world, scope: [:greetings, :hello]

When a :default option is given, its value will be returned if the translation is missing. If the :default value is a symbol, it will be used as a key and translated. Multiple values can be provided as default. The first one that results in a value will be returned. For example, the following first tries to translate the key :missing and then the key :also_missing. As both do not yield a result, the string «Not here» will be returned:

2.0.0p247 :008 > I18n.translate :missing, default: [:also_missing, 'Not here'] => 'Not here'

Variables can be interpolated to the translation like this:

2.0.0p247 :009 > I18n.translate :user, :scope => [:greetings, :hello], :user => 'Ela'  => "Hello Ela!"

To look up multiple translations at once, an array of keys can be passed:

2.0.0p247 :010 > I18n.translate [:world, :friend], :scope => [:greetings, :hello]  => ["Hello World!", "Hello Friend!"]

Also, a key can translate to a (potentially nested) hash of grouped translations:

2.0.0p247 :011 > I18n.translate :hello, :scope => [:greetings] => {:world=>"Hello World!", :user=>"Hello %{user}", :friend=>"Hello Friend!"}

Pluralization options in internationalization for Ruby

In English there is only one singular and one plural form for a given string, e.g. «1 message» and «2 messages». Other languages (Arabic, Japanese, Russian and many more) have different grammars that have additional or fewer plural forms. Thus, the I18n API provides a flexible pluralization feature.

The :count interpolation variable has a special role in that it both is interpolated to the translation and used to pick a pluralization from the translations according to the pluralization rules:

2.0.0p247 :012 > I18n.translate :messages, :scope => :inbox, :count => 1 => "You have one message in your inbox." 2.0.0p247 :013 > I18n.translate :messages, :scope => :inbox, :count => 39  => "You have 39 messages in your inbox."

The algorithm for pluralizations in :en is as simple as: entry[count == 1 ? 0 : 1]. The translation denoted as ‘one' is regarded as singular, the ‘other’ is used as plural (including the count being zero).

Setting up Date and Time Localization

To localize the time format, the Time object should be passed to I18n.localize. A format can be picked by passing the :format option — by default the :default format is used.

2.0.0p247 :014 > I18n.localize Time.now  => "Wed, 14 Aug 2013 13:34:49 +0200" 2.0.0p247 :015 > I18n.localize Time.now, :format => :short  => "14 Aug 13:34"

Instead of I18n.localize, a shorter alias can be used: I18n.l.

i18n — the default internationalization solution for Ruby on Rails

![rails logo-150×150](//a.storyblok.com/f/42908/150×150/21a3c118a7/rails_logo-150×150.png)

I18n is the default internationalization solution for Ruby on Rails and it is localized with the use of the rails-i18n gem.

In accordance with the RoR philosophy of convention over configuration, Rails applications come with some reasonable defaults already set.

For example, instead of doing this manually:

2.0.0p247 :001 > require 'i18n'
2.0.0p247 :002 > I18n.locale = :en
2.0.0p247 :003 > I18n.default_locale = :en
2.0.0p247 :004 > I18n.load_path = Dir['./*.yml']

Rails adds all .rb and .yml files from the config/locales directory to translations load path, automatically. The I18n library will use English as a default locale (if different locale is not set, :en will be used for looking up translations).

By default, Rails expects that all the resource files are kept in config/locales. On the other hand, we prefer to keep them organized in the subdirectories that correspond to the locale names. You might find some other mode of organization better suited, for example separating the models localization from the views localization.

Let’s change some settings by overriding the defaults in application.rb:

  • let’s organize the resource files in the subdirectories corresponding to locales instead of storing everything in config/locales,
  • set :de as the default locale and
  • :en, :de and :fr as available locales
config.i18n.load_path += Dir[Rails.root.join('config/locales/**/*.{rb,yml}').to_s] config.i18n.default_locale = :de config.i18n.available_locales = [:en, :de, :fr]

Rails is localized to numerous locales (meaning, all the static text originating from Rails). For a complete list of available locales and information on missing translations or pluralization, you can check this page.

For multilingual applications it is necessary to allow the user to change the current locale and to keep track of this choice. The chosen locale can be stored in a session or a cookie, but this practice is not recommended. The reason is, that locales should be RESTful — transparent and a part of the URL. For example, when a user saves or shares a link to a page that he viewed in a non default locale, visiting that link should show the page in that same locale and not fall back to the default.

The information on the current locale can be passed through:

  • URL query parameter  ( http://example.com/?locale=sr )
  • URL path  ( http://example.com/sr/ )
  • domain name  ( http://example.sr )
  • subdomain name  ( http://sr.example.com )
  • client supplied information

Passing the locale as a query parameter within the URL

If the locale information is passed in the URL as a query parameter, setting the locale can be done in before_action (before_filter prior to Rails 4) in the ApplicationController:

before_action :set_locale

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

This requires passing the locale as a URL query parameter and adding it to all the links within the application.

Doing this manually ( for example: link_to( books_url :locale => I18n.locale ) ) is not very convenient. Fortunately, Rails comes with a helper method that can be overridden: ApplicationController#default_url_options:

# app/controllers/application_controller.rb def default_url_options(options={})   { :locale => I18n.locale } end

As a result, every helper method dependent to url_for (e.g. helpers for named routes like root_path or root_url, resource routes like books_path or books_url, etc.) will now automatically include the locale in the query string, like this: http://localhost:3000/?locale=sr.

Passing the locale as a part of the URL path

It is much nicer and cleaner to have the locale information at the beginning of the path instead of the end: http://localhost:3000/sr/ vs http://localhost:3000/?locale=sr. This is achievable with the «over-riding default_url_options» strategy as previously demonstrated. The routes just need to be set up with the scoping option:

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

The use of the optional path scope will allow the locale information to be omitted for the default locale without causing the Routing Error.

Passing the locale as a domain name or a subdomain

Setting the locale from the domain name or subdomain makes the locale of the current page very obvious and search engines also like this approach.

It is easy to implement it in Rails by adding a before_action to ApplicationController:

before_action :set_locale

def set_locale
#extracting from the domain name
  I18n.locale = extract_locale_from_tld || I18n.default_locale

#extracting from subdomain:
  #I18n.locale = extract_locale_from_subdomain || I18n.default_locale
end

def extract_locale_from_tld
  parsed_locale = request.host.split('.').last
  I18n.available_locales.include?(parsed_locale.to_sym) ? parsed_locale  : nil
end

def extract_locale_from_subdomain   parsed_locale = request.subdomains.first   I18n.available_locales.include?(parsed_locale.to_sym) ? parsed_locale : nil end

Setting the locale from client-supplied information

Information other than the page URL can be used to set the appropriate locale for the current user. For example, if the user has saved his preferred locale in the user profile of the web application or web service, after the log in, the current locale can be set:

I18n.locale = current_user.locale

Each HTTP request contains information that can also be used, for example the preferred language set in the browser or the geographical information inferred from the IP address.

A trivial implementation of using an Accept-Language header would be:

def set_locale
  I18n.locale = extract_locale_from_accept_language_header
end

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

For production environments perhaps a more complex plugin or rack middleware would be more suitable.

Another way of choosing the locale from client information would be to use a database for mapping the client IP to the region, such as GeoIP Lite Country. The mechanics of the code would be very similar to the accept-language example — first the database would be queried for the user’s IP, and then the preferred locale looked up for the country/region/city returned.

Translation lookup and date/time Localization with Ruby on Rails

The use of the I18n.translate and I18n.localize methods was described in detail in the previous sections on Internationalization for plain Ruby. In addition to that, Rails adds t (translate) and l (localize) helper methods to controllers and views so that spelling out I18n.t and I18n.l all the time is not necessary. These helpers will catch missing translations and wrap the resulting error message into a .

#instead of I18n.translate :hello t :hello #instead of I18n.localize Time.now l Time.now

Inflection Rules For Locales Other then English

Rails 4.0 allows you to define inflection rules (such as rules for singularization and pluralization) for locales other than English. In config/initializers/inflections.rb, you can define these rules for multiple locales. The initializer contains a default example for specifying additional rules for English; follow that format for other locales as you see fit.

Localized Views in Rails

If there is a view template index.html.erb present in the views directory, it is possible to put a localized variant of this template: index.de.html.erb in the same directory, and Rails will render it when the locale is set to :de. When the locale is set to the default locale, the generic index.html.erb view will be used.

This feature can be useful when working with a large amount of static content.

Using Safe HTML Translations in Ruby on Rails

Keys with a ‘_html’ suffix and keys named ‘html’ are marked as HTML safe. They should be used without escaping.

# config/locales/en.yml en:   welcome: welcome!   hello_html: hello!   title:     html: title!
# app/views/home/index.html.erb 

<%= t('welcome') %>

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

<%= t('hello_html') %%>

<%= t('title.html') %%>

The output would be something like this:

welcome! welcome! hello! title!

Translations for Active Record Models

Methods Model.model_name.human and Model.human_attribute_name(attribute) can be used to transparently look up translations for model and attribute names. For example when the following translations are added:

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

The User.model_name.human will return «Dude» and User.human_attribute_name(«login») will return «Handle».

Error Message Scopes

Active Record gives a several namespaces for placing message translations in order to provide different messages and translation for certain models, attributes, and/or validations. It also transparently takes single table inheritance into account.

For example, if there is an ActiveRecord model «User» that has the :presence validation for :name, the key for the message would be :blank. ActiveRecord will look up for this key in several namespaces, in this order:

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

If the models are using inheritance, then the messages are also looked up in the inheritance chain.

Error Message Interpolation

The translated model name, translated attribute name, and value are always available for interpolation. So, instead of the default error message «can not be blank» the attribute name could be used like this : «Please fill in your %{attribute}».

For the complete list of available interpolation variables, check this link.

Translations for Action Mailer E-Mail Subjects

If a subject is not passed to the mail method, Action Mailer will try to find it in the translations. The performed lookup will use the pattern ..subject to construct the key.

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

Internationalization for Sinatra with i18n gem

sinatra

Sinatra can be easily set up to use i18n gem for internationalization:

require 'i18n' require 'i18n/backend/fallbacks'

configure   I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)   I18n.load_path, Dir[File.join(settings.root, 'locales', '*.yml')]   I18n.backend.load_translations end

Passing the locale

As previously described for Ruby on Rails, there are several methods of passing the locale information:

  • Specific urls:
before '/:locale/*' do   I18n.locale       =       params[:locale]   request.path_info = '/' + params[:splat ][0] end
  • dedicated subdomain:
before do   if (locale = request.host.split('.')[0]) != 'www'     I18n.locale = locale   end end
  • browser preference (requires rack-contrib)
use Rack::Locale

When the i18n gem is required, the resource file paths set and the locale passed, the pages can be translated and localized with the I18n.t and I18n.l methods, as described earlier in the article. To avoid allways typing the module name in the method calls, a simple helpers can be defined:

helpers do   def t(*args)     I18n.t(*args)   end

def l(*args)     I18n.l(*args)   end end

For rendering localized templates, find_template method needs to be extended. It needs to select the first template matching the user locale (or at least one acceptable fallback). To help in the selection process templates stored in the views directory are suffixed by the name of the locale.

helpers do   def find_template(views, name, engine, &block)     I18n.fallbacks[I18n.locale].each { |locale|       super(views, "#{name}.#{locale}", engine, &block) }     super(views, name, engine, &block)   end end

Internationalization for Padrino with i18n gem

padrino

i18n gem is used as a default internationalization solution for the Padrino framework, and by default Padrino will search for all .yml or .rb files located in app/locale. Localization is fully supported in:

  • padrino-core (date formats, time formats etc…)
  • padrino-admin (admin language, orm fields, orm errors, etc…)
  • padrino-helpers (currency, percentage, precision, duration etc…)

So far Padrino itself has been localized to the following languages: Czech, Danish, German, English, Spanish, French, Italian, Dutch, Norwegian, Russian, Polish, Brazilian Portuguese, Turkish, Ukrainian, Traditional Chinese, Simplified Chinese, Japanese.

Translation and localization is done in a similar way as described previously.

Setting the default locale can be done in config/boot.rb:

Padrino.before_load do
I18n.locale = :en
end

i18n is simple to use and yet very powerful, it has everything that most developers need. Thanks to that, it has become the most popular internationalization solution in the Ruby world.

I18n in most cases really works practically «out of the box». It is easy to set up and use, and after that, the only thing you’ll need is a reliable localization service.

In this article we described the basic use of the i18n gem. The next article in the Internationalization for Ruby series will demonstrate some advanced applications.

Sources

  1. Wikipedia article on Ruby
  2. List of websites using RoR on builtwith.com

Further reading

  • This article uses Ruby on Rails Guides as the main source
  • Internationalization for Ruby wiki on ruby-i18n.org
  • YAML 1.2 specification
  • YAML 1.1 Reference card
  • Wikipedia article on yaml
  • Internationalization for Ruby wiki on ruby-i18n.org
  • rails-i18n github repository
  • Ruby on Rails home page
  • Ruby on Rails github page
  • i18n for Sinatra on Sinatra Recipes
  • Sinatra home page
  • Sinatra github page
  • Padrino guide for localization
  • Padrino home page
  • Padrino github page

Понравилась статья? Поделить с друзьями:
  • Rabbit error destiny 2
  • Rails rescue error
  • Rails flash error
  • R99 на брелке starline a93 как исправить
  • R6s error at hooking api