10.3. Mehrsprachige
Rails-Applikation
I18n.locale zum Definieren
der gewünschten Sprache benutzen
Natürlich muss eine Rails-Applikation wissen, in welcher Sprache
eine Webseite dargestellt werden soll.
I18n.locale
speichert die aktuelle Sprache ab und kann von der Applikation
ausgelesen werden. Ich zeige Ihnen das mit einem
Mini-Webshop-Beispiel:
MacBook:~ xyz$ rails new webshop
[...]
MacBook:~ xyz$ cd webshop
MacBook:webshop xyz$
Dieser bekommt eine Homepage:
MacBook:webshop xyz$ rails generate controller Page index
[...]
MacBook:webshop xyz$ rm public/index.html
MacBook:webshop xyz$
Diese müssen wir noch in der
config/routes.rb
als Root-Seite eintragen:
Webshop::Application.routes.draw do
root :to => 'page#index'
get "page/index"
end
Die
app/views/page/index.html.erb
füllen wir
mit folgendem Beispiel:
<h1>Example Webshop</h1>
<p>Welcome to this webshop.</p>
<p><b>I18n.locale:</b>
<%= I18n.locale %>
</p>
Wenn wir den Rails-Server mit
rails server
starten und im Browser
http://0.0.0.0:3000/
aufrufen,
dann sehen wir folgende Webseite:
Sie sehen, dass "en" für Englisch als Default eingestellt ist.
Stoppen Sie den Rails-Server mit
CTRL-C und ändern Sie
in der Datei
config/application.rb
die Einstellung
für die Default-Sprache auf:
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
config.i18n.default_locale = :de
Wenn Sie danach rails server starten und mit dem Webbrowser
http://0.0.0.0:3000/
wieder
aufrufen, dann sehen Sie folgende Webseite:
Die Webseite hat sich nicht verändert, aber als Ausgabe von
<%= I18n.locale %>
bekommen Sie jetzt
'de
' für Deutsch und nicht mehr 'en
' für
Englisch angezeigt.
Bitte stoppen Sie den Rails-Server mit
CTRL-C und
ändern Sie in der Datei
config/application.rb
die
Einstellung für die Default-Sprache auf
en
für
Englisch:
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
config.i18n.default_locale = :en
Wir wissen jetzt, wie der Default für
I18n.locale
in der gesamten Applikation gesetzt wird,
aber das ist ja nur die halbe Miete. Ein User möchte sich seine Sprache
selber aussuchen. Dafür gibt es verschiedene Ansätze. Zum
Veranschaulichen dieser Ansätze brauchen wir eine zweite Seite, die
einen deutschen Text anzeigt. Bitte legen Sie die Datei
app/views/page/index.de.html.erb
mit diesem Inhalt
an:
<h1>Beispiel Webshop</h1>
<p>Willkommen in diesem Webshop.</p>
<p><b>I18n.locale:</b>
<%= I18n.locale %>
</p>
I18n.locale per URL
Path-Prefix setzen
Der charmante Weg, die Sprache zu setzen, ist sie als Prefix mit
in die URL aufzunehmen. So können Suchmaschinen besser verschiedene
Sprachversionen verwalten.
http://0.0.0.0:3000/de
soll
die deutsche Version der Homepage und
http://0.0.0.0:3000/en
die
englische Version der Homepage anzeigen. Der erste Schritt dafür ist
eine Anpassung der
config/routes.rb:
Webshop::Application.routes.draw do
scope "(:locale)", :locale => /en|de/ do
root :to => 'page#index'
get "page/index"
end
end
Als Nächstes müssen wir in der
app/controllers/application_controller.rb
einen
before_filter
setzen, der den von der Route
gesetzten Parameter locale als
I18n.locale
setzt:
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :set_locale
private
def set_locale
I18n.locale = params[:locale] || I18n.default_locale
end
end
Jetzt haben wir aber ein Problem: Zwar kommen wir initial mit
dem Prefix auf eine Seite mit der richtigen Sprache, aber was ist,
wenn wir von dieser Seite auf eine andere Seite in unserem
Rails-Projekt verlinken wollen? Dann müssten wir den Prefix manuell in
den Link einbauen. Das will keiner. Deshalb gibt es eine Lösung in
Form von
Rails.application.routes.default_url_options
.
Wir müssen nur unsere
set_locale
-Methode in
app/controllers/application_controller.rb
entsprechend erweitern:
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :set_locale
private
def set_locale
I18n.locale = params[:locale] || I18n.default_locale
Rails.application.routes.default_url_options[:locale]= I18n.locale
end
end
Damit werden alle mit link_to
und
url_for
(auf das
link_to
aufsetzt) erzeugten Links automatisch
um den Parameter locale
erweitert. Sie müssen
nichts weiter tun. Alle mit dem Scaffold Generator generierten Links
werden automatisch entsprechend verändert.
Um dem User die Möglichkeit zu geben, einfach zwischen den
Sprachversionen hin- und herzuschalten, bieten sich am Kopf der
Webseite zwei Links an. Dabei sollte die aktuelle Sprache nicht als
aktiver Link angezeigt werden. Dies kann man in der
app/views/layouts/application.html.erb
-Datei
für alle Views wie folgt realisieren:
<!DOCTYPE html>
<html>
<head>
<title>Webshop</title>
<%= stylesheet_link_tag "application", :media => "all" %>
<%= javascript_include_tag "application" %>
<%= csrf_meta_tags %>
</head>
<body>
<p>
<%= link_to_unless I18n.locale == :en, "English", locale: :en %>
|
<%= link_to_unless I18n.locale == :de, "Deutsch", locale: :de %>
</p>
<%= yield %>
</body>
</html>
Die Navigation wird dann am Kopf der Seite angezeigt.
I18n.locale per Accept-Language HTTP Header des Browsers
setzen
Wenn ein User Ihre Webseite zum ersten Mal aufruft, möchten Sie
ihm im Idealfall direkt die Webseite in der richtigen Sprache
anzeigen. Dafür können Sie das Accept-Language Feld im HTTP Header
auslesen. In jedem Webbrowser kann der Benutzer der Software seine
bevorzugte Sprache einstellen (siehe
http://www.w3.org/International/questions/qa-lang-priorities
).
Dieser Wert wird dem Webserver und damit Rails automatisch vom Browser
mitgeteilt.
Bitte verändern Sie
app/controllers/application_controller.rb
wie
folgt ab:
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :set_locale
private
def extract_locale_from_accept_language_header
request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first
end
def set_locale
I18n.locale = extract_locale_from_accept_language_header || I18n.default_locale
end
end
Und vergessen Sie bitte nicht, die
config/routes.rb
von den Einstellungen in
„I18n.locale per URL
Path-Prefix setzen“ zu bereinigen:
Webshop::Application.routes.draw do
get "page/index"
root :to => 'page#index'
end
Jetzt bekommen Sie immer die im Webbrowser definierte Sprache
ausgeliefert. Dabei ist zu beachten, dass
request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first
nicht alle Fälle abfängt. So sollten Sie überprüfen, ob Sie in Ihrer
Rails-Applikation die angegebene Sprache überhaupt unterstützen. Es
gibt einige fertige Gems, die Ihnen diese Arbeit einfach abnehmen. Die
Suchmaschine Ihrer Wahl wird Ihnen bei der Suche helfen.
I18n.locale in einer Session
abspeichern
Oft soll der Wert von
I18n.locale
in einer
Session (siehe
Abschnitt 8.2, „Sessions“) gespeichert werden.
Legen wir zum Setzen des Wertes als Beispiel in unserem
Webshop einen Controller
SetLanguage
mit den
beiden Actions
english
und
german
an:
MacBook:webshop xyz$ rails generate controller SetLanguage english german
[...]
MacBook:webshop xyz$
In der
app/controllers/set_language_controller.rb
-Datei
füllen wir die beiden Actions wie folgt:
class SetLanguageController < ApplicationController
def english
I18n.locale = :en
set_session_and_redirect
end
def german
I18n.locale = :de
set_session_and_redirect
end
private
def set_session_and_redirect
session[:locale] = I18n.locale
redirect_to :back
rescue ActionController::RedirectBackError
redirect_to :root
end
end
Zum Schluss müssen wir noch in der Datei
app/controllers/application_controller.rb
die
set_locale
-Methode anpassen:
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :set_locale
private
def set_locale
I18n.locale = session[:locale] || I18n.default_locale
session[:locale] = I18n.locale
end
end
Um dem User die Möglichkeit zu geben, einfach zwischen den
Sprachversionen hin- und herzuschalten, bieten sich am Kopf der
Webseite zwei Links an. Dabei sollte die aktuelle Sprache nicht als
aktiver Link angezeigt werden. Dies kann man in der
app/views/layouts/application.html.erb
-Datei
für alle Views wie folgt realisieren:
<!DOCTYPE html>
<html>
<head>
<title>Webshop</title>
<%= stylesheet_link_tag "application", :media => "all" %>
<%= javascript_include_tag "application" %>
<%= csrf_meta_tags %>
</head>
<body>
<p>
<%= link_to_unless I18n.locale == :en, "English", set_language_english_path %>
|
<%= link_to_unless I18n.locale == :de, "Deutsch", set_language_german_path %>
</p>
<%= yield %>
</body>
</html>
Die Navigation wird dann am Kopf der Seite angezeigt.
I18n.locale per
Domain-Endung setzen
Wenn Sie mehrere Domains mit den für die entsprechenden Sprachen
typischen Domain-Endungen haben, dann können Sie natürlich darüber
auch die Sprache setzen. So würde ein Besucher von
http://www.example.com
die
englische Version und ein Besucher von
http://www.example.de
die
deutsche Version angezeigt bekommen.
Dazu müssen wir in der
app/controllers/application_controller.rb
einen
before_filter
setzen, der die aufgerufene
Domain analysiert und
I18n.locale
setzt:
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :set_locale
private
def set_locale
case request.host.split('.').last
when 'de'
I18n.locale = :de
when 'com'
I18n.locale = :en
else
I18n.locale = I18n.default_locale
end
end
end
Tipp
Zum Testen dieser Funktionalität können Sie auf Ihrem Linux-
oder Mac OS X-Entwicklungssystem in der Datei
/etc/hosts
die folgenden Einträge
hinzufügen:
0.0.0.0 www.example.com
0.0.0.0 www.example.de
Welcher Weg ist der
beste?
Meiner Meinung nach führt eine Kombination der vorgestellten
Wege zum besten Ergebnis. Als Erst-Besucher einer Webseite freue ich
mich, wenn der Accept-Language HTTP Header meines Browsers korrekt
gelesen und umgesetzt wird. Es ist aber auch schön, die Sprache später
in der User-Konfiguration ändern zu können (gerade bei schlecht
übersetzten Seite ist oft Englisch besser). Und zum Schluss muss man
sagen, dass eine gute Abbildbarkeit der Seite inklusive der Sprachen
für eine Suchmaschine sehr viel wert ist. Rails gibt Ihnen die
Möglichkeit, alle Varianten leicht zu benutzen und sogar zu
verknüpfen.
Als Beispiel benutzen wir einen Mini-Webshop, in dem wir einen
Product Scaffold übersetzen. Das Ziel ist, die Applikation in Deutsch
und Englisch zur Verfügung zu stellen. Die Rails-Applikation:
MacBook:~ xyz$ rails new webshop
[...]
MacBook:~ xyz$ cd webshop
MacBook:webshop xyz$ rails generate scaffold Product name description 'price:decimal{7,2}'
[...]
MacBook:webshop xyz$ rake db:migrate
[...]
MacBook:webshop xyz$
Das Product-Model definieren wir in der
app/models/product.rb:
class Product < ActiveRecord::Base
attr_accessible :description, :name, :price
validates :name,
:presence => true,
:uniqueness => true,
:length => { :within => 2..255 }
validates :price,
:presence => true,
:numericality => { :greater_than => 0 }
end
Bei der Sprachauswahl für den User nehmen wir die
URL-Prefix-Variante, die in
„I18n.locale per URL
Path-Prefix setzen“
beschrieben wird. Dazu verwenden wir folgende
app/controllers/application_controller.rb:
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :set_locale
private
def set_locale
I18n.locale = params[:locale] || I18n.default_locale
Rails.application.routes.default_url_options[:locale]= I18n.locale
end
end
Diese
config/routes.rb:
Webshop::Application.routes.draw do
scope "(:locale)", :locale => /en|de/ do
root :to => 'products#index'
resources :products
end
end
Fügen Sie in der
app/views/layouts/application.html.erb
noch die
Links für die Navigation ein:
<!DOCTYPE html>
<html>
<head>
<title>Webshop</title>
<%= stylesheet_link_tag "application", :media => "all" %>
<%= javascript_include_tag "application" %>
<%= csrf_meta_tags %>
</head>
<body>
<p>
<%= link_to_unless I18n.locale == :en, "English", locale: :en %>
|
<%= link_to_unless I18n.locale == :de, "Deutsch", locale: :de %>
</p>
<%= yield %>
</body>
</html>
Und vor dem Starten von Rails mit
rails server
löschen wir noch die
public/index.html
-Datei, damit
wir als Root-Seite die Liste aller Produkte angezeigt bekommen.
MacBook:webshop xyz$ rm public/index.html
MacBook:webshop xyz$ rails server
=> Booting WEBrick
=> Rails 3.2.3 application starting in development on http://0.0.0.0:3000
=> Call with -d to detach
=> Ctrl-C to shutdown server
[2012-06-01 10:43:06] INFO WEBrick 1.3.1
[2012-06-01 10:43:06] INFO ruby 1.9.3 (2012-04-20) [x86_64-darwin11.3.0]
[2012-06-01 10:43:06] INFO WEBrick::HTTPServer#start: pid=51412 port=3000
Wenn wir auf Deutsch klicken, verändert sich bis auf die
Sprachnavigation ganz oben nichts auf der Seite.
Bei beiden Sprachen haben wir die Datei
app/views/products/index.html.erb
gerendet. Sie
enthält folgenden Code:
<h1>Listing products</h1>
<table>
<tr>
<th>Name</th>
<th>Description</th>
<th>Price</th>
<th></th>
<th></th>
<th></th>
</tr>
<% @products.each do |product| %>
<tr>
<td><%= product.name %></td>
<td><%= product.description %></td>
<td><%= product.price %></td>
<td><%= link_to 'Show', product %></td>
<td><%= link_to 'Edit', edit_product_path(product) %></td>
<td><%= link_to 'Destroy', product, confirm: 'Are you sure?', method: :delete %></td>
</tr>
<% end %>
</table>
<br />
<%= link_to 'New Product', new_product_path %>
Jetzt müssen wir nur noch einen Weg finden, die einzelnen Elemente
dieser Seite sinnvoll und möglichst generisch zu übersetzen.
Textbausteine im YAML-Format
Jetzt müssen wir für
I18n.t
die
einzelnen Textbausteine definieren. Die entsprechenden Verzeichnisse
müssen dazu noch angelegt werden:
MacBook:webshop xyz$ mkdir -p config/locales/models/product
MacBook:webshop xyz$ mkdir -p config/locales/views/product
MacBook:webshop xyz$
Damit die dort angelegten YAML-Dateien auch automatisch
eingelesen werden, müssen Sie die folgenden Zeilen in der Datei
config/application.rb
einfügen:
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', 'models', '*', '*.yml').to_s]
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', 'views', '*', '*.yml').to_s]
config.i18n.default_locale = :en
Bitte legen Sie die Datei
config/locales/models/product/de.yml
mit
folgendem Inhalt an.
# ruby encoding: utf-8
de:
activerecord:
models:
product: 'Produkt'
attributes:
product:
name: 'Name'
description: 'Beschreibung'
price: 'Preis'
In der Datei
config/locales/views/product/de.yml
fügen wir
ein paar Werte für die Scaffold Views ein:
# ruby encoding: utf-8
de:
views:
show: Anzeigen
edit: Editieren
destroy: Löschen
are_your_sure: Sind Sie sicher?
back: Zurück
edit: Editieren
product:
index:
title: Liste aller Produkte
new_product: Neues Produkt
edit:
title: Produkt editieren
new:
title: Neues Produkt
flash_messages:
product_was_successfully_created: 'Das Produkt wurde erfolgreich angelegt.'
product_was_successfully_updated: 'Das Produkt wurde erfolgreich aktualisiert.'
Zum Schluss kopieren wir noch eine fertige
Standard-Übersetzung von Sven Fuchs aus seinem Github-Repository
https://github.com/svenfuchs/rails-i18n
:
MacBook:webshop xyz$ cd config/locales
MacBook:locales xyz$ curl -O https://raw.github.com/svenfuchs/rails-i18n/master/rails/locale/de.yml
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 4940 100 4940 0 0 9574 0 --:--:-- --:--:-- --:--:-- 11932
MacBook:locales xyz$ cd ../..
MacBook:webshop xyz$
Anmerkung
Wenn Sie wissen, wie Bundler funktioniert, können Sie auch
die Zeile gem 'rails-i18n'
in die Datei
Gemfile
einfügen und danach
bundle ausführen. Damit bekommen Sie alle
Sprachdateien aus dem Repository eingespielt.
In der config/locales/de.yml
-Datei sind
die benötigten Formate und generischen Formulierungen für Deutsch
enthalten, die wir für eine normale Rails-Applikation benötigen (z.
B. die Wochentage, Währungssymbol usw.). Schauen Sie einmal mit
Ihrem Lieblingseditor rein, um einen Überblick zu bekommen.
Da für Englisch die meisten Sachen schon im System vorhanden
sind, müssen wir nur in der Datei
config/locales/views/product/en.yml
ein paar
Werte für die Scaffold Views einfügen:
en:
views:
show: Show
edit: Edit
destroy: Delete
are_you_sure: Are you sure?
back: Back
edit: Edit
product:
index:
title: List of all products
new_product: New product
edit:
title: Edit Product
new:
title: New product
flash_messages:
product_was_successfully_created: 'Product was successfully created.'
product_was_successfully_updated: 'Product was successfully updated.'
Views mit I18n.t ausstatten
Bitte verändern Sie die aufgeführten View-Dateien wie
angegeben.
In der Datei
app/views/products/_form.html.erb
müssen wir im
oberen Teil die Anzeige der Validierungsfehler auf
I18n.t
umstellen. Die Namen der
Formularfehler werden automatisch aus
activerecord.attributes.product
eingelesen:
<%= form_for(@product) do |f| %>
<% if @product.errors.any? %>
<div id="error_explanation">
<h2><%= t 'activerecord.errors.template.header', :model => Product.model_name.human, :count => @product.errors.count %></h2>
<ul>
<% @product.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
</div>
<div class="field">
<%= f.label :description %><br />
<%= f.text_field :description %>
</div>
<div class="field">
<%= f.label :price %><br />
<%= f.text_field :price %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
In der Datei
app/views/products/edit.html.erb
müssen wir die
Überschrift und die Links am Fuß der Seite mit
I18n.t
einbauen:
<h1><%= t 'views.product.edit.title' %></h1>
<%= render 'form' %>
<%= link_to I18n.t('views.show'), @product %> |
<%= link_to I18n.t('views.back'), products_path %>
In der Datei
app/views/products/index.html.erb
müssen wir so
gut wie jede Zeile verändern. Im Tabellenkopf benutze ich dafür
human_attribute_name()
, das könnte aber
auch direkt mit
I18n.t
gemacht werden. Der
Preis des Produktes wird mit dem Helper number_to_currency
angegeben. In einer realen Applikation müssen wir an dieser Stelle
noch eine definierte Währung angeben.
<h1><%= t 'views.product.index.listing_products' %></h1>
<table>
<tr>
<th><%= Product.human_attribute_name(:name) %></th>
<th><%= Product.human_attribute_name(:description) %></th>
<th><%= Product.human_attribute_name(:price) %></th>
<th></th>
<th></th>
<th></th>
</tr>
<% @products.each do |product| %>
<tr>
<td><%= product.name %></td>
<td><%= product.description %></td>
<td><%= number_to_currency(product.price) %></td>
<td><%= link_to I18n.t('views.show'), product %></td>
<td><%= link_to I18n.t('views.edit'), edit_product_path(product) %></td>
<td><%= link_to I18n.t('views.destroy'), product, confirm: I18n.t('views.are_you_sure'), method: :delete %></td>
</tr>
<% end %>
</table>
<br />
<%= link_to I18n.t('views.product.index.new_product'), new_product_path %>
In der
app/views/products/new.html.erb
müssen die Überschrift und der Link angepasst
werden:
<h1><%= t 'views.product.new.title' %></h1>
<%= render 'form' %>
<%= link_to I18n.t('views.back'), products_path %>
In der
app/views/products/show.html.erb
benutzen wir wieder
human_attribute_name()
für die Attribute. Zusätzlich müssen noch die Links mit
I18n.t
übersetzt werden. Wie beim index
View benutzen wir auch
number_to_currency()
, um den Preis
formatiert anzuzeigen:
<p id="notice"><%= notice %></p>
<p>
<b><%= Product.human_attribute_name(:name) %>:</b>
<%= @product.name %>
</p>
<p>
<b><%= Product.human_attribute_name(:description) %>:</b>
<%= @product.description %>
</p>
<p>
<b><%= Product.human_attribute_name(:price) %>:</b>
<%= number_to_currency(@product.price) %>
</p>
<%= link_to I18n.t('views.edit'), edit_product_path(@product) %> |
<%= link_to I18n.t('views.back'), products_path %>
Flash-Messages im Controller übersetzen
Zum Schluss müssen wir in der
app/controllers/products_controller.rb
die zwei
Flash-Nachrichten beim Erstellen (create) und Updaten (update) von
Datensätzen mit
I18n.t
übersetzen:
class ProductsController < ApplicationController
# GET /products
# GET /products.json
def index
@products = Product.all
respond_to do |format|
format.html # index.html.erb
format.json { render json: @products }
end
end
# GET /products/1
# GET /products/1.json
def show
@product = Product.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.json { render json: @product }
end
end
# GET /products/new
# GET /products/new.json
def new
@product = Product.new
respond_to do |format|
format.html # new.html.erb
format.json { render json: @product }
end
end
# GET /products/1/edit
def edit
@product = Product.find(params[:id])
end
# POST /products
# POST /products.json
def create
@product = Product.new(params[:product])
respond_to do |format|
if @product.save
format.html { redirect_to @product, notice: I18n.t('views.product.flash_messages.product_was_successfully_created') }
format.json { render json: @product, status: :created, location: @product }
else
format.html { render action: "new" }
format.json { render json: @product.errors, status: :unprocessable_entity }
end
end
end
# PUT /products/1
# PUT /products/1.json
def update
@product = Product.find(params[:id])
respond_to do |format|
if @product.update_attributes(params[:product])
format.html { redirect_to @product, notice: I18n.t('views.product.flash_messages.product_was_successfully_updated') }
format.json { head :no_content }
else
format.html { render action: "edit" }
format.json { render json: @product.errors, status: :unprocessable_entity }
end
end
end
# DELETE /products/1
# DELETE /products/1.json
def destroy
@product = Product.find(params[:id])
@product.destroy
respond_to do |format|
format.html { redirect_to products_url }
format.json { head :no_content }
end
end
end
Jetzt können Sie das Scaffold products sowohl in Deutsch als
auch in Englisch bedienen. Die Umschaltung der Sprache erfolgt über
den Link am Kopf der Seite.