多国语系及时区

多国语系及时区

It Works on My Machine! - 数以万计的程式设计师

多国语系

安装Rails中文翻译词汇档

Rails 默认的语系是英文。要换成繁体中文,可以安装 rails-i18n 这个 gem 有社群帮忙翻译的繁体中文:

  • 在Gemfile加上gem "rails-i18n",然后执行 bundle
  • 修改 config/application.rb 的默认语系
  config.i18n.default_locale = "zh-TW"

这样就会使用http://github.com/svenfuchs/rails-i18n的繁体中文翻译。

自订翻译档案

要让你的网站可以支援多国语系,必须定义出翻译的词汇对应档案。这些词汇档放在 config/locales 下,使用 YAML 格式,例如新增一个 config/locales/zh-TW.yml 的档案,内容如下:

"zh-TW":
    hello_world: 哈囉
    admin:
        event: 活动管理

注意 YAML 格式的缩排必须使用两个空格, Tab 是不允许的。直接复制贴上可能会有问题,请小心检查缩排。

这样就可以用I18n.t这个方法来做翻译词汇的替换。如果在 View 中可以直接使用t这个 Helper 方法。翻译关键字可以用字串或 Symbol,也可以加上 Scope,例如:

t("admin.event")
t(:event, :scope => :admin )

I18n.t(:hello_world)  # 如果不在View中,则需要加上 I18n 类别

如果要在词汇内嵌变量的话,可以使用%{variable _name}_的语法,修改_config/locales/zh-TW.yml :

"zh-TW"
    hello: "亲爱的%{name}你好!"

这样在 template 中改成传入参数即可:

t(:hello, :name => @user.name) # 亲爱的XXX你好

就算你的网站不需要支援多国语系,这个功能对于团队协作开发网站仍然非常有帮助,因为写程式的时候不一定会先确定文案规格,用 i18n 来处理的话,最后只需要让 PM 统一修改翻译词汇档即可。

搭配Model使用

在套用上述的翻译词汇档之后,你可能会注意到 Model 验证错误讯息会变成如 Name 不能是空白字符 ,如果需要近一步中文化字段名称,你可以新增 config/locales/events.yml 内容如下:

zh-TW:
  activerecord:
    attributes:
      event:
        name: "活动名称"
        description: "描述"

其实,翻译档档名叫 events.yml 、 zh-TW.yml 、 en.yml 什么都无所谓,重要的是 YAML 结构中第一层要对应 locale 的名称,也就是zh-TW, Rails 会加载 config/locales 下所有的 YAML 词汇档案。

如何让使用者可以切换多语系

在 application_controller.rb 中加入:

before_action :set_locale

def set_locale
  # 可以将 ["en", "zh-TW"] 设定为 VALID_LANG 放到 config/environment.rb 中
  if params[:locale] && I18n.available_locales.include?( params[:locale].to_sym )
    session[:locale] = params[:locale]
  end

  I18n.locale = session[:locale] || I18n.default_locale
end

在 View 中可以这样做:

<%= link_to "中文版", :controller => controller_name, :action => action_name, :locale => "zh-TW" %>
<%= link_to "English", :controller => controller_name, :action => action_name, :locale => "en" %>

侦测浏览器的语系自动选择

请参考 <http://guides.rubyonrails.org/i18n.html#setting-the-locale-from-the-client-supplied-information>

语系样板

除了上述一个单字一个单字的翻译词汇替换之外,如果样板内大多是属于较为静态的内容, Rails 也提供了不同语系可以有不同样板,你只要将样板命名加上语系附档名即可,例如:

app/views/pages/faq.zh-TW.html.erb
app/views/pages/faq.en.html.erb

如此在英文版的时候就会使用 faq.en.html.erb 这个样板,中文版时使用 faq.zh-TW.html.erb 这个样板。

时区 TimeZone

在 Rails 中,数据库里面的时间(datetime)字段一定都是储存 UTC 时间。而 Rails 提供的机制是让你从数据库拿资料时,自动帮你转换时区。例如,要设定台北 +8 时区:

首先设定 config/application.rb 中默认时区为 config.time_zone = “Taipei”,如此 ActiveRecord 便会帮你自动转换时区,也就是拿出来时 +8,存回去时 -8

如何根据使用者切换时区?

首先,你必须找个地方储存不同使用者的时区,例如 User model 有一个字段叫做 time_zone:string。然后在编辑设定的地方,可以让使用者自己选择时区:

 <%= time_zone_select :user, :time_zone %>

接着在 application_controller.rb 中加入:

before_action :set_timezone

def set_timezone
   if current_user && current_user.time_zone
      Time.zone = current_user.time_zone
    end
end

时区处理方法

Ruby 原生的 Time 类别对于时区的处理一律是参考唯一的系统环境变量ENV['TZ'],这在使用者多时区的应用程式中就显的见拙。因此在 Rails 中的时间类别使用的是 ActiveSupport::TimeWithZone ,我们已经知道可以使用Time.zone可以改变时区,其他的用法例如:

Time.zone = "Taipei"
Time.zone.local(2011, 8, 3, 9, 0) # 建立一个Taipei当地时间
=> Wed, 03 Aug 2011 09:00:00 CST +08:00
t = Time.zone.now # 目前时间
=> Wed, 03 Aug 2011 22:17:54 CST +08:00
t.in_time_zone("Tokyo") # 将这个时间换时区
=> Wed, 03 Aug 2011 23:18:34 JST +09:00
Time.utc(2005,2,1,15,15,10).in_time_zone # 将UTC时间换Taipei当地时间
=> Tue, 01 Feb 2005 23:15:10 CST +08:00

时间的显示

除了使用 Ruby 内建的Datetime#strftime格式化时间之外, Rails 也可以直接呼叫to_s转换输出格式:

datetime.to_s(:db)                      # => "2007-12-04 00:00:00"
datetime.to_s(:number)                  # => "20071204000000"
datetime.to_s(:short)         # => "04 Dec 00:00"
datetime.to_s(:long)          # => "December 04, 2007 00:00"
datetime.to_s(:long_ordinal)  # => "December 4th, 2007 00:00"
datetime.to_s(:rfc822)        # => "Tue, 04 Dec 2007 00:00:00 +0000"
datetime.to_s(:iso8601)       # => "2007-12-04T00:00:00+00:00"

也可以自行注册专案常用的格式在 config/initializers/time_formats.rb 里:

Time::DATE_FORMATS[:month_and_year] = '%B %Y'
Time::DATE_FORMATS[:short_ordinal] = lambda { |time| time.strftime("%B #{time.day.ordinalize}") }

或是透过 I18n 的机制,在翻译词汇档中编辑格式,然后使用:

I18n.l( Time.now )
I18n.l( Time.now, :format => :short )

其他实务技巧

本章开头提到字段 (datetime) 会存成 UTC 时间,所以比对时间大小和区间的时候,无论资料当初是什么时区,都可以正确的操作。例如:

Event.where( "created_at > ? and created_at <= ?", Time.now.beginning_of_day, Time.now.end_of_day )
## 这样产生的 SQL 是  SELECT * FROM "events" WHERE (created_at > '2016-03-30 16:00:00' and created_at <= '2016-03-31 15:59:59')

这样就会抓取到今天内的所有活动,注意到因为台北时区的关系,Rails 转换成 SQL 的时候是是从 16:00 到 15:59:59。

但是如果业务需求是想要抓某一个日期或是某一个时间,而不管时区的话,用 datetime 字段反而麻烦。例如想要抓 2016-03-31 的资料,不管是台北时区或旧金山时区,反正就是要 2016-03-31 (因为同一个时间,可能在不同时区会是不同天)。如果想要抓早上 09:00~09:59 的资料,不管是哪一个时区。这时候笔者建议你开单纯的 date (只有日期)或 time (只有时间没有日期) 数据库字段即可,这样反而会比较简单。

参考资料