Статьи

Go Global с Rails и I18n

Bola Del Mundo

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

Правда или нет, люди сегодня говорят на разных языках. Однажды вам может понадобиться включить поддержку разных языков на вашем сайте. Как бы Вы это сделали? Что, если есть пользовательский контент, который также необходимо перевести? Читайте дальше и узнайте!

В этой статье будут рассмотрены следующие темы:

  • API интернационализации Rails (I18n)
  • Переключение языков на сайте
  • Автоматическая настройка языка для пользователя в зависимости от его местоположения (Geocoder gem)
  • Хранение различных версий пользовательского контента в базе данных (Globalize gem)

Исходный код доступен на GitHub, а рабочую демонстрацию можно найти по адресу http://sitepoint-i18n.herokuapp.com .

Земляные работы

Для этой демонстрации я буду использовать Rails 4.1.5, но такое же решение можно реализовать с помощью Rails 3.

Для сегодняшних целей давайте создадим простой образовательный веб-сайт под названием « Педагог» . Прежде всего, мы должны заложить некоторые наземные работы. Не бойтесь — эта итерация будет короткой.

Создайте новое приложение Rails без набора тестов по умолчанию:

$ rails new educator -T 

На нашем сайте будут опубликованы посты репетиторов. Мы не собираемся внедрять систему аутентификации и авторизации — только модель, контроллер, представление и несколько маршрутов. Вот список полей, которые будут присутствовать в таблице articles (вместе с id , created_at , updated_at ):

  • title ( string )
  • body ( text )

Создайте и примените необходимую миграцию:

 $ rails g model Article title:string body:string $ rake db:migrate 

Теперь добавьте следующие маршруты в ваш файл rout.rb :

конфиг / routes.rb

 [...] resources :articles, only: [:index, :new, :create, :edit, :update] root to: 'articles#index' [...] 

Затем создайте файл article_controller.rb и вставьте следующий код:

articles_controller.rb

 class ArticlesController < ApplicationController def index @articles = Article.order('created_at DESC') end def new @article = Article.new end def create @article = Article.new(article_params) if @article.save flash[:success] = "The article was published successfully!" redirect_to articles_path else render 'new' end end def edit @article = Article.find(params[:id]) end def update @article = Article.find(params[:id]) if @article.update_attributes(article_params) flash[:success] = "The article was updated successfully!" redirect_to articles_path else render 'edit' end end private def article_params params.require(:article).permit(:body, :title) end end 

Здесь нет ничего особенного — некоторые основные методы для отображения, создания и редактирования статей.

Если вы используете Rails 3 и не используете strong_params , замените метод article_params на params[:article] и добавьте следующую строку в модель:

модели / article.rb

 [...] attr_accessible :body, :title [...] 

Перейдите к виду. Чтобы помочь со стилем, давайте используем Twitter Bootstrap:

Gemfile

 [...] gem 'bootstrap-sass' [...] bundle install 

таблицы стилей / application.css.scss

 @import 'bootstrap'; @import 'bootstrap/theme'; 

JavaScripts / application.js

 [...] //= require bootstrap 

Теперь мы можем воспользоваться преимуществами Bootstrap:

макеты / application.html.erb

 [...] <body> <div class="navbar navbar-default navbar-static-top"> <div class="container"> <div class="navbar-header"> <%= link_to 'Educator', root_path, class: 'navbar-brand' %> </div> <ul class="nav navbar-nav navbar-left"> <li><%= link_to 'Add new article', new_article_path %></li> </ul> </div> </div> <div class="container"> <% flash.each do |key, value| %> <div class="alert alert-<%= key %>"> <%= value %> </div> <% end %> <div class="page-header"> <h1><%= yield :page_header %></h1> </div> <%= yield %> </div> </body> [...] 

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

Создайте фактический вид:

Статьи / index.html.erb

 <% content_for(:page_header) {"List of articles"} %> <%= render @articles %> 

content_for здесь используется для предоставления заголовка страницы для блока yield о котором я говорил выше. Мы также должны создать партиал _article.html.erb, который будет отображаться для каждой статьи в массиве @articles :

Статьи / _article.html.erb

 <h2><%= article.title %></h2> <small class="text-muted"><%= article.created_at.strftime('%-d %B %Y %H:%M:%S') %></small> <p><%= article.body %></p> <p><%= link_to 'Edit', edit_article_path(article), class: 'btn btn-default' %></p> <hr/> 

strftime отображает дату создания в следующем формате: «1 января 2014 00:00:00». Подробнее о поддерживаемых флагах для strftime читайте здесь .

Представления для new и edit действий еще проще:

Статьи / new.html.erb

 <% content_for(:page_header) {"New article"} %> <%= render 'form' %> 

Статьи / edit.html.erb

 <% content_for(:page_header) {"Edit article"} %> <%= render 'form' %> 

Наконец, часть _form.html.erb :

Статьи / _form.html.erb

 <%= form_for @article do |f| %> <div class="form-group"> <%= f.label :title %> <%= f.text_field :title, class: 'form-control', required: true %> </div> <div class="form-group"> <%= f.label :body %> <%= f.text_area :body, rows: 3, class: 'form-control', required: true %> </div> <%= f.submit 'Submit', class: 'btn btn-primary btn-lg' %> <% end %> 

Запустите сервер и создайте пару статей, чтобы проверить, что все работает. Отлично, теперь мы можем перейти к следующей итерации!

Это длинное слово «интернационализация»

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

Сейчас мы представим пользователям два языка на выбор: английский и русский. Почему русский? Ну, это один из самых популярных языков в мире, на котором говорят 254 миллиона человек, и… ну, это мой родной язык. Конечно, вы можете выбрать любой язык (выбор не распространенного языка может потребовать от вас дополнительной работы, о которой я расскажу чуть позже).

Вставьте новый драгоценный камень в свой Gemfile:

Gemfile

 [...] gem 'rails-i18n', '~> 4.0.0' # for Rails 4 # OR gem 'rails-i18n', '~> 3.0.0' # for Rails 3 [...] 

Не забудь бежать

 $ bundle install 

rails-i18n — это хранилище для сбора данных о региональных настройках Ruby on Rails I18n. Хорошо, что, черт возьми, I18n? На самом деле это не более чем сокращенная версия слова «интернационализация» — между первым «i» и последним «n» ровно 18 букв. Глупо, не правда ли?

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

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

конфиг / application.rb

 [...] I18n.available_locales = [:en, :ru] [...] 

Кстати, вы также можете настроить часовой пояс по умолчанию («Центральное время (США и Канада)» используется изначально) и локаль по умолчанию ( :en используется изначально, что, как вы уже догадались, означает «английский»):

 config.time_zone = 'Moscow' # set default time zone to "Moscow" (UTC +4) config.i18n.default_locale = :ru # set default locale to Russian 

Для каждой локали необходим файл для хранения переводов заголовков, кнопок, меток и других элементов нашего сайта. Эти файлы перевода находятся в каталоге config / locales с расширением .yml ( YAML — еще один язык разметки).

По умолчанию есть только один файл, en.yml , с некоторым демонстрационным контентом и краткими пояснениями. Замените этот файл следующим содержимым:

конфиг / локали / en.yml

 en: activerecord: attributes: article: title: "Title" body: "Body" forms: messages: success: "Operation succeeded!" buttons: edit: "Edit" submit: "Submit" menu: new_article: "Add an article" articles: index: title: "List of articles" edit: title: "Add article" new: title: "New article" 

Некоторые вещи, чтобы отметить здесь. Прежде всего, мы должны предоставить локаль этого файла, что делается с корневым ключом, en . Все остальные данные перевода вложены в ключ локали. Чтобы хранить переводы для атрибутов Active Record, используйте структуру activerecord - attributes - *model_name* .

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

Сейчас мы пропустим обсуждение блока articles . А сейчас давайте использовать несколько причудливых букв кириллицы для хранения русских переводов:

конфиг / локали / ru.yml

 ru: activerecord: attributes: article: title: "Название" body: "Текст" forms: messages: success: "Операция была успешно выполнена!" buttons: edit: "Редактировать" submit: "Отправить" menu: new_article: "Добавить статью" articles: index: title: "Список статей" edit: title: "Редактировать статью" new: title: "Добавить статью" 

Есть шутка о парне, который показал некоторые «трюки», используя такие латинские буквы: «R -> Я», «N -> И». На самом деле он просто использовал буквы кириллицы. В любом случае, мы можем использовать эти данные перевода в наших представлениях.

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

В представлениях метод translate может вызываться с псевдонимом t :

 t('menu.new_article') 

Или:

 t(:new_article, scope: :menu) # This uses a scope to explain where to find the required key 

Хорошо, давайте попробуем это в файле макета:

макеты / application.html.erb

 <body> <div class="navbar navbar-default navbar-static-top"> <div class="container"> <div class="navbar-header"> <%= link_to 'Educator', root_path, class: 'navbar-brand' %> </div> <ul class="nav navbar-nav navbar-left"> <li><%= link_to t('menu.new_article'), new_article_path %></li> </ul> </div> </div> <div class="container"> <% flash.each do |key, value| %> <div class="alert alert-<%= key %>"> <%= value %> </div> <% end %> <div class="page-header"> <h1><%= yield :page_header %></h1> </div> <%= yield %> </div> </body> 

Я заменил в , Теперь, в зависимости от текущей локали, эта ссылка будет содержать другой текст. Это очень удобный способ работы с разными локалями. Если когда-нибудь в будущем вам потребуется добавить поддержку немецкого языка на свой сайт, просто создайте другой файл локали и добавьте немецкий в массив доступных языков.

Давайте index.html.erb представление index.html.erb сейчас:

 <% content_for(:page_header) {t('.title')} %> <%= render @articles %> 

Здесь происходит что-то другое. Я просто использую t('.title') , но заголовок страницы индекса хранится в области article.index. Почему это работает? Чтобы сделать нашу жизнь немного проще, Rails поддерживает поиск «Lazy» . Если у нас есть представление index.html.erb , которое хранится в каталоге articles и в файле перевода следующая структура:

 articles: index: title: "List of articles" 

Внутри index.html.erb мы можем найти ключ title , просто введя t('.title') . То же решение может быть реализовано для других представлений:

Статьи / new.html.erb

 <% content_for(:page_header) {t('.title')} %> <%= render 'form' %> 

Статьи / edit.html.erb

 <% content_for(:page_header) {t('.title')} %> <%= render 'form' %> 

Перейдем к части _article.html.erb :

Статьи / _article.html.erb

 <h2><%= article.title %></h2> <small class="text-muted"><%= l(article.created_at, format: '%-d %B %Y %H:%M:%S') %></small> <p><%= article.body %></p> <p><%= link_to t('forms.buttons.edit'), edit_article_path(article), class: 'btn btn-default' %></p> <hr/> 

Обратите внимание, что метод strftime заменяется на l , псевдоним для localize . Этот метод берет предоставленную дату и / или время и форматирует ее соответствующим образом, используя соответствующие данные перевода (представленные rails-i18n ).

Не забудьте про форму:

Статьи / _form.html.erb

 <%= form_for @article do |f| %> <div class="form-group"> <%= f.label :title %> <%= f.text_field :title, class: 'form-control', required: true %> </div> <div class="form-group"> <%= f.label :body %> <%= f.text_area :body, rows: 3, class: 'form-control', required: true %> </div> <%= f.submit t('forms.buttons.submit'), class: 'btn btn-primary btn-lg' %> <% end %> 

Пока мы указали переводы для атрибутов Article , они будут помещены в соответствующие метки.

Наконец, переведите текст для флеш-сообщений:

articles_controller.rb

 [...] def create @article = Article.new(article_params) if @article.save flash[:success] = t('forms.messages.success') redirect_to articles_path else render 'new' end end [...] def update @article = Article.find(params[:id]) if @article.update_attributes(article_params) flash[:success] = t('forms.messages.success') redirect_to articles_path else render 'edit' end end [...] 

Давайте обсудим еще одну вещь в этой итерации. Предположим, вы хотите показать, сколько образовательных статей в базе данных. Чтобы сосчитать все статьи, вы можете использовать что-то вроде @articles.length , но как насчет множественного числа? В английском языке правила плюрализации просты: «0 статей», «1 статьи», «2 статьи» и т. Д. Однако на русском языке все становится немного сложнее, потому что правила плюрализации не так просты. Похоже, что модуль I18n достаточно умен, чтобы работать с правилами плюрализации.

локали / en.yml

 [...] articles: index: count: one: "%{count} article" other: "%{count} articles" [...] 

локали / ru.yml

 articles: index: count: zero: "%{count} статей" one: "%{count} статья" few: "%{count} статьи" many: "%{count} статей" other: "%{count} статьи" 

Здесь я просто добавляю, какой вариант использовать в каком случае. Также обратите внимание на %{count} - это интерполяция.

Теперь мы можем добавить эту строку в представление:

Статьи / index.html.erb

 <small><%= t('.count', :count => @articles.length) %></small> 

Я передаю хэш в качестве второго аргумента метода t . Он использует этот атрибут count для выбора необходимого перевода и интерполирует его значение в строку. Круто, не правда ли?

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

Давайте дадим нашим пользователям возможность изменить язык сайта.

Смена языка

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

 <div class="navbar navbar-default navbar-static-top"> <div class="container"> [...] <ul class="nav navbar-nav navbar-right"> <li class="dropdown"> <a class="dropdown-toggle" data-toggle="dropdown" href="#"> <%= t('menu.languages.lang') %> <span class="caret"></span> </a> <ul class="dropdown-menu" role="menu"> <li> <%= link_to t('menu.languages.en'), change_locale_path(:en) %> </li> <li> <%= link_to t('menu.languages.ru'), change_locale_path(:ru) %> </li> </ul> </li> </ul> [...] 

Добавить новые данные перевода:

локали / en.yml

 [...] menu: new_article: "Add an article" languages: lang: "Language" ru: "Russian" en: "English" [...] 

локали / ru.yml

 [...] menu: new_article: "Добавить статью" languages: lang: "Язык" ru: "Русский" en: "Английский" [...] 

Нам нужен новый маршрут для изменения локали. Для простоты я создам новый контроллер с именем SettingsController и добавлю туда метод change_locale (чтобы следовать принципам REST, вы можете создать отдельный контроллер для управления локалями и использовать метод update ):

конфиг / routes.rb

 [...] get '/change_locale/:locale', to: 'settings#change_locale', as: :change_locale [...] 

Фактический метод:

settings_controller.rb

 class SettingsController < ApplicationController def change_locale l = params[:locale].to_s.strip.to_sym l = I18n.default_locale unless I18n.available_locales.include?(l) cookies.permanent[:educator_locale] = l redirect_to request.referer || root_url end end 

Проверьте, какой язык был передан и является ли он действительным. Мы не хотим устанавливать язык на тарабарщину. Затем установите постоянный файл cookie (который не такой постоянный, как вы думаете. Срок его действия истечет через 20 лет), чтобы сохранить выбранную локаль и перенаправить пользователя обратно.

Отлично. Последнее, что нам нужно сделать, это проверить содержимое куки и соответствующим образом изменить локаль. Мы хотим, чтобы это происходило на каждой странице, поэтому используйте ApplicationController :

application_controller.rb

 [...] before_action :set_locale def set_locale if cookies[:educator_locale] && I18n.available_locales.include?(cookies[:educator_locale].to_sym) l = cookies[:educator_locale].to_sym else l = I18n.default_locale cookies.permanent[:educator_locale] = l end I18n.locale = l end [...] 

Проверьте, установлен ли файл cookie, и указан ли язык в списке доступных языков. Если да - извлеките его содержимое и установите соответствующий язык. В противном случае установите локаль по умолчанию.

Перезагрузите сервер и попробуйте изменить локаль. Вы заметите, что все заголовки, меню, кнопки и метки
измените их содержание соответственно. Но можем ли мы сделать лучше? Можем ли мы проверить страну пользователя по IP и указать наиболее подходящий язык? Ответ: да, мы можем!

Настройка локали в соответствии со страной пользователя

Чтобы выбрать страну пользователя по IP-адресу, мы будем использовать гем Geocoder от Alex Reisner.

Gemfile

 [...] gem 'geocoder' [...] 

Не забудь бежать

 $ bundle install 

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

 request.location.country_code 

Он возвращает строку типа «RU», «CN», «DE» и т. Д. Пока мы предоставляем поддержку русского языка, мы установим его для всех пользователей из стран СНГ и установим английский для всех остальных:

application_controller.rb

 def set_locale if cookies[:educator_locale] && I18n.available_locales.include?(cookies[:educator_locale].to_sym) l = cookies[:educator_locale].to_sym else begin country_code = request.location.country_code if country_code country_code = country_code.downcase.to_sym # use russian for CIS countries, english for others [:ru, :kz, :ua, :by, :tj, :uz, :md, :az, :am, :kg, :tm].include?(country_code) ? l = :ru : l = :en else l = I18n.default_locale # use default locale if cannot retrieve this info end rescue l = I18n.default_locale ensure cookies.permanent[:educator_locale] = l end end I18n.locale = l end 

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

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

Хранение переводов для статей

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

Самый простой способ сделать это - использовать гем Globalize, созданный Свеном Фуксом и другими людьми. Добавьте его в свой Gemfile :

Gemfile

 [...] gem 'globalize', '~> 4.0.2' # For Rails 4 # Or gem 'globalize', '~> 3.1.0' # For Rails 3 

и беги

 $ bundle install 

Этот драгоценный камень использует отдельную таблицу переводов. Для нашей демонстрации он будет называться article_translations и будет содержать следующие поля:

  • id
  • article_id
  • locale
  • created_at - это дата создания самого перевода, а не оригинальной статьи
  • updated_at
  • title
  • body

Мы храним переведенные версии статей для каждого языка. Перед созданием таблицы переводов измените нашу модель, чтобы отметить поля, которые должны быть переведены (например, нам не нужно переводить поля, такие как id ):

модели / article.rb

 [...] translates :title, :body [...] 

Теперь создайте соответствующую миграцию:

 $ rails g migration create_translation_for_articles 

Измените миграцию следующим образом:

xxx_create_translation_for_articles.rb

 class CreateTranslationForArticles < ActiveRecord::Migration def up Article.create_translation_table!({ title: :string, body: :text}, {migrate_data: true}) end def down Article.drop_translation_table! migrate_data: true end end 

Во-первых, обратите внимание, что мы должны использовать методы up и down . Глобализация не может работать с методом change . Во-вторых, необходима опция migrate_data: true , потому что наша таблица articles уже содержит некоторые данные, и мы хотим перенести их (они будут перенесены в локаль по умолчанию).

Запустите миграцию

 $ rake db:migrate 

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

Globalize также поддерживает управление версиями с помощью PaperTrail, когда вы используете гем globalize-versioning .

Если вы не знаете, что такое PaperTrail или как интегрировать управление версиями в приложении Rails, вас может заинтересовать моя статья « Управление версиями с помощью PaperTrail»

Просмотрите альтернативные решения на странице Globalize , но большинство из них устарели.

Вывод

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

Вы когда-нибудь реализовывали подобную функциональность в своих проектах? Какие инструменты вы использовали? Вам когда-нибудь нужно было хранить переводы для пользовательского контента? Поделитесь своим опытом в комментариях!