我们已经完成了基本的购物车功能,但客户提出新需求,要求系统能被不同语言的国家使用,她的公司打算对新兴市场国家进行大型推广。如果我们不提供对其他语言用户操作系统的便利支持,客户打算将掏出的钱收回,所以我们必须接收这个需求。
首要问题是我们中并没有专业的翻译家。关于这点客户让我们安心,我们只需要关注技术的部分,翻译的事情分交由外包处理。于是我们的核心问题变为了翻译功能,而且并不需要考虑管理员界面,因为管理员都使用英语,我们只需要关注商店相关界面即可。
虽然工作已经进行了删减,但还是有大量的任务需要处理。首先,我们要设计用户选择语言的功能,并且我们还需要提供相应的翻译内容,然后根据用户选择的语言将翻译内容应用于 view 中。不过我们依然可以胜任,凭借高中学习的一些西班牙语我们开始了工作。
迭代 J1:进行本地化
一开始,我们需要创建一个新的配置文件,通过它列举可用的语言及设置系统默认语言。
# config/initializers/i18n.rb
#encoding: utf-8
I18n.default_locale = :en
LANGUAGES = [
['English', 'en'],
['Español'.html_safe, 'es']
]
Joe 提问:如果我们只对一种语言的用户提供服务,还需要继续阅读本章吗?
简要回答是不需要。实际上,Rails 通常都是小型团队在使用,基本上不会使用到翻译功能。不过话说回来,如果预计将使用到翻译功能那能够提前了解也是好的。所以,如果你无法确定自己是否将会使用翻译功能,我们的建议是最好先了解翻译功能的内容是什么再做出有效的决定。
上述代码主要是为了两个目的。
首先是通过 I18n 模块设置默认本地化选项。I18n 是个十分有趣的名字,它完全是为了避免每次都完全书写 internationalization 而出现。由于「Internationalization」以「i」开头,以「n」结尾,并且中间有 18 个字符,所以得名「I18n」。
接着在文件中定义了一个列表,列表中的值表示着显示名和本地化选项名称。但是通过 U.S. 键盘我们无法输入所有字符,比如「español」就有这样的字符。虽然不同的操作系统有不同的处理方式,不过最简单的莫过于直接从网站上拷贝字符本身。如果你打算这样处理,请先确认当前编辑器字符集为 UTF-8。不过,我们选择使用 HTML 表达式处理相关字符。如果我们什么都不在此基础上什么都不做,字符会按 HTML 的修饰显示,但通过调用 html_safe Rails 会知道这段字符串含有 HTML 表达式是安全的。
要使配置生效需要重启服务。
需要进行翻译的页面要有 en 和 es 版本(现在虽然还没有,稍后会陆续加上),而不同语言版本将在 URL 中有所表现。我们打算配置本地化显示并使用当前本地语言作为默认选项,所以将语言默认设置为英语。要实现这个有趣的功能需要先修改 config/routes.rb 文件。
# config/routes.rb
Rails.application.routes.draw do
get 'admin' => 'admin#index'
controller :sessions do
get 'login' => :new
post 'login' => :create
delete 'logout' => :destroy
end
resources :users
resources :products do
get :who_bought, on: :member
end
scope '(:locale)' do
resources :orders
resources :line_items
resources :carts
root 'store#index', as: 'store', via: :all
end
end
在代码中我们将相关 resources 和 root 声明都放置在 :locale 的 scope 声明中。而且 :locale 被放置于小括号内,意在说明它是可选择的。并且我们没有将管理员功能和 session 相关功能放置在 scope 中,因为它们并不是此次翻译计划的目标。
通过上述配置,http://localhost:3000 地址将会使用默认本地语言,也就是英语,而且http://localhost:3000/en 和 http://localhost:3000/es 都将路由至相同的 controller action 中,不过将会使用不同的语言版本界面。
由于我们对 config.routes 进行了大量的修改,又进行了嵌套,又进行了路径参数的修改,一旦路径出现问题也难以发现。不过不必担心,在开发环境下启动服务后,Rails 会提供相关的可视化帮助。只需要访问 http://localhost:3000/rails/info/routes 就可以查看所有路由地址,就像下图展示的一样。路由列表界面展示的详情会在 312 页做进一步解读。
A list of all of the active routes.png
通过路由配置,我们已经可以获取本地化参数并使其作用于整个应用。实现这个目标需要创建一个 before_action 回调函数,并设置 default_url_options 值。类似的逻辑都需要在所有 controller 的公共地带处理,所以我们选择 ApplicationController。
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
#...
before_action :set_i18n_locale_from_params
protected
#...
def set_i18n_locale_from_params
unless params[:locale]
return
end
if I18n.available_locales.map(&:to_s).include?(params[:locale])
I18n.locale = params[:locale]
else
flash[:notice] = "#{params[:locale]} translation not available"
logger.error flash[:notice]
end
end
def default_url_options
{ locale: I18n.locale }
end
#...
end
default_url_options 见名知意,它会提供一个 URL 参数的 hash 对象,如果 URL 中没有 hash 对象选项值时将使用 hash 对象作为默认值。在上述示例中我们提供了 :locale 的默认值,当页面的 view 链接中不包含本地化选项时此函数将发挥作用。
我们可以看到下图中展示出来的效果:
English version of the front page.png
现在网站的首页和以 /en 开头的页面的英文版已经可以正常运行。不过在界面上会看到翻译不可用类似的字样(就如同下图展示的一样),这只是表示当前语言的页面版本没有找到,虽然是个半成品,不过已经是个进步了。
Translation not available.png
迭代 J2:翻译商店首页
现在是时候提供一些翻译文本了。先从布局文件开始,毕竟一眼就能看见。通过 I18n.translate 替换需要翻译的文本。不仅方法被简化为了 I18n.t,并且辅助方法名称就是 t。
翻译方法的参数是以点号开头的唯一名称。可以使用任何名称,不过这个名称作为 t 方法的入参,而且入参时以点开头的名字会被作为模板名称的扩展使用。所以我们开始操作吧。
<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
<head>
<title>Depot</title>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
<%= csrf_meta_tags %>
</head>
<body class='<%= controller.controller_name %>'>
<div id="banner">
<%= image_tag("logo.png") %>
<!-- Here's new line -->
<%= @page_title || t('.title') %>
</div>
<div id="columns">
<div id="side">
<% if @cart %>
<%= hidden_div_if(@cart.line_items.empty?, id: 'cart') do %>
<%= render @cart %>
<% end %>
<% end %>
<ul>
<!-- Here's new line -->
<li><a href="http://www...."><%= t('.home') %></a></li>
<!-- Here's new line -->
<li><a href="http://www..../faq"><%= t('.question') %></a></li>
<!-- Here's new line -->
<li><a href="http://www..../news"><%= t('.new') %></a></li>
<!-- Here's new line -->
<li><a href="http://www..../contact"><%= t('.contact') %></a></li>
</ul>
<% if session[:user_id] %>
<ul>
<li><%= link_to 'Orders', orders_path %></li>
<li><%= link_to 'Products', products_path %></li>
<li><%= link_to 'Users', users_path %></li>
</ul>
<% end %>
<div id="date_time"><%= Time.now.strftime("%Y-%m-%d %H:%M:%S") %></div>
</div>
<div id="main">
<%= yield %>
</div>
</div>
</body>
</html>
根据模板名称 layouts/application.html.erb,与之匹配的英文版本是 en.layouts.application。下面就是相应的本地化文件:
# config/locales/en.yml
en:
layouts:
application:
title: 'Pragmatic Bookshelf'
home: 'Home'
question: 'Question'
new: 'New'
contact: 'Contact'
另外这个是西班牙语的:
# config/locales/es.yml
es:
layouts:
application:
title: "Publicaciones de Pragmatic"
home: "Inicio"
questions: "Preguntas"
news: "Noticias"
contact: "Contacto"
文件格式都是 YAML,与数据库配置文件类似。YAML 由缩进及名字与值简单构成,缩进表示当前名称下相应的数据结构。为了让 Rails 能够识别新创建的 YAML 文件,需要重启服务。
下图中就是我们翻译的界面显示:
Baby steps: translated titles and sidebar.png
接着要修改的主要是「Add to Cart」按钮。它处于商铺主页模板中。
<!-- app/views/store/index.html.erb -->
<% if notice %>
<p id="notice"><%= notice %></p>
<% end %>
<h1>
<!-- Here's new line -->
<%= t('.title_html') %>
<span>accessed <%= pluralize(@access_times, 'time') %></span>
</h1>
<% cache ['store', Product.latest] do %>
<% @products.each do |product| %>
<% cache ['entry', product] do %>
<div class="entry">
<%= image_tag(product.image_url) %>
<h3><%= product.title %></h3>
<%= sanitize(product.description) %>
<div class="price_line">
<span class="price"><%= number_to_currency(product.price, unit: "¥") %></span>
<!-- Here's new line -->
<%= button_to t('.add_html'), line_items_path(product_id: product), remote: true %>
</div>
</div>
<% end %>
<% end %>
<% end %>
同样需要修改相应的本地化配置文件,首先是英语版本:
# config/locales/en.yml
en:
store:
index:
title_html: "Your Pragmatice Catalog"
add_html: "Add to Cart"
接着是西班牙语:
# config/locales/es.yml
es:
store:
index:
title_html: "Su Catálogo de Pragmatic"
add_html: "Añadir al Carrito"
注意因为 title_html 和 add_html 是以 _html 结尾,所以我们可以直接使用 HTML 表达式编写键盘上没有的字符。如果不这样命名翻译项名称,我们只能在页面上看见标记。其实还有很多方便的功能,Rails 就是为了让你的代码生涯更加轻松。Rails 也会将包含 html 的名称视作 HTML 关键字的组件(在其他单词中是字符串 .html)。
再次刷新页面,我们可以看到如下图的结果:
Translated heading and button.png
越来越自信,我们继续处理购物车 partial 文件。
<!-- app/views/carts/_cart.html.erb -->
<!-- Here's new line -->
<h2><%= t('.title') %></h2>
<table>
<%= render(cart.line_items) %>
<tr class="total_line">
<td colspan="2">Total</td>
<td class="total_cell"><%= number_to_currency(cart.total_price) %></td>
</tr>
</table>
<% unless @order %>
<!-- Here's new line -->
<%= button_to t('.checkout'), new_order_path, method: :get %>
<% end %>
<!-- Here's new line -->
<%= button_to t('.empty'), cart, method: :delete, remote: true, data: { confirm: 'Are you sure?' } %>
接着,依然是翻译文本的编写:
# config/locales/en.yml
en:
carts:
cart:
title: "Your Cart"
checkout: "Checkout"
empty: "Empty Cart"
# config/locales/es.yml
es:
carts:
cart:
title: "Carrito de la Compra"
empty: "Vaciar Carrito"
checkout: "Comprar"
刷新页面,可以看到标题和按钮都已经进行了相应翻译。
Carrito bonita.png
不过现在我们发现了一个问题。语言并不只是文字翻译,货币也是需要本地化的内容。数字也需要根据本地化设置显示。
所以,通过我们与客户的协商,得到的结果是暂时不需要关心汇率转换的问题,因为这应该是信用卡或支付公司需要处理的问题,不过在系统使用西班牙语时需要在数字之后展示「USD」或 「$US」字符串。
数字的展示方式也有变化。小数部分以逗号分隔,千位分隔符使用点符号。
货币的问题比一开始要复杂,需要做许多判断。不过,Rails 晓得查看一番翻译文件的信息,剩下要做的提供相关信息。下面是英文版配置:
# config/locales/en.yml
en:
number:
currency:
format:
unit: "$"
precision: 2
separator: "."
delimiter: ","
format: "%u%n"
然后是西班牙语配置:
# config/locales/es.yml
es:
number:
currency:
format:
unit: "$US"
precision: 2
separator: ","
delimiter: "."
format: "%n %u"
在配置中对 number.currency.format 的单位、精度、分隔符及定界符进行了指定。这里最好的一点是配置自解释。format 配置项比较复杂,%n 是数字占位符; 是空格符,它将数字与单位间隔开;%u 是单位的占位符。
Mas dinero. por favor.png
迭代 J3:翻译结账页面
现在已经对主页翻译得差不多了,接下来是订单页。
<!-- app/views/orders/new.html.erb -->
<div class="depot_form">
<fieldset>
<!-- Here's new line -->
<legend><%= t('.legend') %></legend>
<%= render 'form' %>
</fieldset>
</div>
然后是页面使用的表单:
<!-- app/views/orders/_form.html.erb -->
<%= form_for(@order) do |f| %>
<% if @order.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@order.errors.count, "error") %> prohibited this order from being saved:</h2>
<ul>
<% @order.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<!-- Here's new line -->
<%= f.label :name, t('.name') %><br>
<%= f.text_field :name, size: 40 %>
</div>
<div class="field">
<!-- Here's new line -->
<%= f.label :address, t('.address_html') %><br>
<%= f.text_area :address, rows: 3, cols: 40 %>
</div>
<div class="field">
<!-- Here's new line -->
<%= f.label :email, t('.email') %><br>
<%= f.text_field :email, size: 40 %>
</div>
<div class="field">
<!-- Here's new line -->
<%= f.label :pay_type, t('.pay_type') %><br>
<%= f.collection_select :pay_type_id, PayType.all, :id, :name,
prompt: t('.pay_prompt_html') %>
</div>
<div class="actions">
<!-- Here's new line -->
<%= f.submit t('.submit') %>
</div>
<% end %>
接着修改相应的本地化配置:
# config/locales/en.yml
en:
orders:
new:
legend: "Please Enter Your Details"
form:
name: "Name"
address_html: "Address"
email: "E-mail"
pay_type: "Pay with"
pay_prompt_html: "Select a payment method"
submit: "Place Order"
# config/locales/es.yml
es:
orders:
new:
legend: "Por favor, introduzca sus datos"
form:
name: "Nombre"
address_html: "Dirección"
email: "E-mail"
pay_type: "Forma de pago"
pay_prompt_html: "Seleccione un método de pago"
submit: "Realizar Pedido"
看下图中表单的变化。
Ready to take your money-in Spanish.png
一切看起来都还不错,直到你点击「Realizar Pedido」按钮后将看见如下图的结果。Active Record 的错误信息也应该进行翻译,所以需要提供相应的翻译文本。
Translation missing.png
# config/locales/es.yml
es:
activerecord:
errors:
messages:
inclusion: "no está incluido en la lista"
blank: "no puede quedar en blanco"
errors:
template:
body: "Hay problemas con los siguientes campos:"
header:
one: "1 error ha impedido que este %{model} se guarde"
other: "%{count} errores han impedido que este %{model} se guarde"
注意错误统计信息的显示有两种格式,errors.template.header.one 是出现一个错误时的信息,errors.template.header.other 是其他情况时显示的信息。通过提供的翻译选项使名词形式规范并且也使相应的动词能与名词匹配。
当再次使用 HTML 表达式时我们希望这些错误信息也能按编写的形式显示(或者按 Rails 的说法是 raw)。所有还要翻译错误信息。接着修改表单代码。
<!-- app/views/orders/_form.html.erb -->
<%= form_for(@order) do |f| %>
<% if @order.errors.any? %>
<div id="error_explanation">
<h2><%=raw t('errors.template.header', count: @order.errors.count, model: t("activerecord.models.order")) %></h2>
<p><%= t('errors.template.body') %></p>
<ul>
<% @order.errors.full_messages.each do |message| %>
<li><%= raw message %></li>
<% end %>
</ul>
</div>
<% end %>
<!-- ..... -->
<% end %>
在翻译错误模板头的调用表达式中分别传递了 count 和 model 名。
进行了相应的修改后我们看看下图的效果:
English nouns in Spanish sentences.png
效果是有所改善,不过 model 的名字和属性没有被翻译。使用英语时还行,因为我们都是通过英文进行工作,不过还是需要对每个属性和名字提供翻译。
接着修改 YAML 文件。
# config/locales/es.yml
es:
activerecord:
models:
order: "pedido"
attributes:
order:
address: "Dirección"
name: "Nombre"
email: "E-mail"
pay_type: "Forma de pago"
这些翻译不需要再提供英文版本配置了,因为这些信息本身就由 Rails 构建。
很高兴相应的 model 名称和属性翻译已经体现在下图中,填充订单信息后将其提交,会收到「Thank you for your order」信息。
Model names are now translated too.png
flash 信息也需要进行翻译。
# app/controllers/orders_controller.rb
def create
@order = Order.new(order_params)
@order.add_line_items_from_cart(@cart)
respond_to do |format|
if @order.save
Cart.destroy(session[:cart_id])
session[:cart_id] = nil
OrderNotifier.received(@order).deliver_now
# Here's new line
format.html { redirect_to store_url, notice: I18n.t('.thanks') }
format.json { render :show, status: :created, location: @order }
else
format.html { render :new }
format.json { render json: @order.errors, status: :unprocessable_entity }
end
end
end
最后,提供翻译文本。
# config/locales/en.yml
en:
thanks: "Thank you for your order."
# config/locales/es.yml
es:
thanks: "Gracias por su pedido"
看看下图中的提示信息:
Thanking the customer in Spanish.png
迭代 J4:添加地域切换器
虽然已经完成了所有任务,不过还需要将此功能的可用性提高。我们打算在布局中右上角未开发区域中添加些功能,最后决定在 image_tag 前面添加一个表单。
<!-- app/views/layouts/application.html.erb -->
<div id="banner">
<%= form_tag store_path, class: 'locale' do %>
<%= select_tag 'set_locale', options_for_select(LANGUAGES, I18n.locale.to_s), onchange: 'this.form.submit()' %>
<%= submit_tag 'submit' %>
<%= javascript_tag "$('.locale input').hide()" %>
<% end %>
<%= image_tag("logo.png") %>
<%= @page_title || t('.title') %>
</div>
form_tag 指定了商铺首页为提交表单后的重新显示界面。class 属性是为了方便关联相应的 CSS 样式。
select_tag 用来定义表单中的输入项,也就是本地化选项。其选项是基于在配置文件中设置的 LANGUAGES 数组,而当前语言是其默认选项(也是通过 I18n 模块处理)。建立 onchange 事件的处理是为了在选项值发生变化的时候提交表单,此功能通过 JavaScript 即可实现,而且它也十分灵活方便。
而 submit_tag 是为了防止 JavaScript 因为特殊情况无法使用时准备。如果处理事件的 JavaScript 可用,此时就不需要提交按钮,所以我们添加了些 JavaScript 功能将表单中的 input 标签隐藏,虽然此处只有一个 input 标签。
接着,将 store controller 进行修改,当 :set_locale 表单被操作时就加上语言选项重定向到商铺首页。
# app/controllers/store_controller.rb
def index
if params[:set_locale]
redirect_to store_url(locale: params[:set_locale])
else
@products = Product.order(:title)
end
end
然后,再添加一些 CSS。
// app/assets/stylesheets/application.css.scss
.locale {
float: right;
margin: 1.6em 0.4em;
}
选择器的效果如下图,我们现在可以通过鼠标点击在语言之间相互切换。
Locale selector in top right.png
现在,系统已经可以进行两种语言的切换了,是时候将系统进行真实的部署了。不过今天已经被任务填满,所以就在工作之后好好休息,明天早上再进行部署吧。
总结
迭代结束后,我们完成了下列任务:
-
对应用设置了默认语言,并提供给用户选项切换语言的方式
-
对文本、货币数字、错误和 model 名称进行了翻译
-
通过
t()辅助方法对布局文件和 view 中的文本进行了翻译
自习天地
下列知识需要你自己学习:
-
在 products 表中添加 locale 列,在首页中只显示与本地化相符的商品。修改商品 view,以便可以查看、修改新添加的列。在每个地区都添加一些商品,然后测试一下。
-
查询当前美元与欧元之间的转换率,当选择西班牙地区时将货币按比率显示为相应的欧元。
-
翻译支付类型的下拉选项。选项值依然不变,只修改显示文本。
本文翻译自《Agile Web Development with Rails 4》,目的为学习所用,如有转载请注明出处。












网友评论