跟踪一下I18n.t的流程

[13] pry(main)> I18n;binding.trace_tree(htmp: 'i18n_t_key', return: false){ I18n.t('helpers.links.show') }
=> "查看"

完整调用栈如下

20181117_211247_900_i18n_t_key.html

一般流程

大概流程如下


一般来说,默认就是使用Simple这个backend来翻译,它集成了Base的一些通用功能

查找翻译值主要是从lookup方法开始,它首先将翻译文件读入,构造成translations这个nested Hash,然后用normalize_keys把待翻译的字符串分解成查找路径,到Hash中挖掘

def lookup(locale, key, scope = [], options = EMPTY_HASH)
  init_translations unless initialized?
  keys = I18n.normalize_keys(locale, key, scope, options[:separator])

  keys.inject(translations) do |result, _key|
    _key = _key.to_sym
    return nil unless result.is_a?(Hash) && result.has_key?(_key)
    result = result[_key]
    result = resolve(locale, _key, result, options.merge(:scope => nil)) if result.is_a?(Symbol)
    result
  end
end

normalize_keys就是将将传入的待翻译值的路径,分解成数组形式。此数组会用local起头,以分辨出要用哪个语言集合来翻译,然后,如果有scope选项,则加入scope,最后才是待翻译值的查找路径

scope的效果,就是会使I18n.t('helpers.links.show') 和 I18n.t('links.show', scope: :helpers) 等同

def normalize_keys(locale, key, scope, separator = nil)
  separator ||= I18n.default_separator

  keys = []
  keys.concat normalize_key(locale, separator)
  keys.concat normalize_key(scope, separator)
  keys.concat normalize_key(key, separator)
  keys
end

def normalize_key(key, separator)
  @@normalized_key_cache[separator][key] ||=
    case key
    when Array
      key.map { |k| normalize_key(k, separator) }.flatten
    else
      keys = key.to_s.split(separator)
      keys.delete('')
      keys.map! { |k| k.to_sym }
      keys
    end
end

而interpolate主要是为了应付此种情况

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

在请求中指定翻译语言

可以根据每个请求传来的要求语言来设置翻译语言,并且不影响其他并发的请求,原因在于,设置是一个线程级变量

module I18n
  module Base
    # Gets I18n configuration object.
    def config
      Thread.current[:i18n_config] ||= I18n::Config.new
    end

    # Sets I18n configuration object.
    def config=(value)
      Thread.current[:i18n_config] = value
    end

    # Write methods which delegates to the configuration object
    %w(locale backend default_locale available_locales default_separator
      exception_handler load_path enforce_available_locales).each do |method|
      module_eval <<-DELEGATORS, __FILE__, __LINE__ + 1
        def #{method}
          config.#{method}
        end

        def #{method}=(value)
          config.#{method} = (value)
        end
      DELEGATORS
    end

获取整个翻译嵌套Hash

只需I18n.t('.')。因根据lookup的原理,'.'的路径为[locale],这就直接取得指定语言的整个翻译集了

同时,如果想即时加载翻译集,也可使用此法,因为lookup是总会先检查翻译集是否已加载,否则加载

翻译文件是如何加载的

为检查load_path是怎样包含了这么多翻译文件的地址的,对其修改一下,以打印出哪里有调用过

def load_path
  #@@load_path ||= []
  @@load_path ||= (
    arr = []
    def arr.<<(*obj)
      pp [__method__, caller[0], obj]
      super
    end
    arr
  )
end

# Sets the load path instance. Custom implementations are expected to
# behave like a Ruby Array.
def load_path=(load_path)
  pp [__method__, caller[1], load_path]
  @@load_path = load_path
  @@available_locales_set = nil
  backend.reload!
end

启动一下rails c,就打印出来了

[:<<,
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activesupport-5.2.1/lib/active_support/i18n.rb:15:in `<top (required)="">'",
["/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activesupport-5.2.1/lib/active_support/locale/en.yml"]]
[:<<,
"/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activemodel-5.2.1/lib/active_model.rb:76:in `block in </top>
'", ["/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activemodel-5.2.1/lib/active_model/locale/en.yml"]] [:<<, "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.2.1/lib/action_view.rb:96:in `block in
'", ["/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.2.1/lib/action_view/locale/en.yml"]] [:<<, "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-5.2.1/lib/active_record.rb:183:in `block in
'", ["/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-5.2.1/lib/active_record/locale/en.yml"]] [:<<, "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/mongoid-7.0.2/lib/mongoid.rb:33:in `
'", ["/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/mongoid-7.0.2/lib/config/locales/en.yml"]] [:<<, "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/mongoid_orderable-5.2.0/lib/mongoid_orderable.rb:4:in `
'", ["/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/mongoid_orderable-5.2.0/lib/config/locales/en.yml"]] [:<<, "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/duck_record-0.0.26/lib/duck_record.rb:64:in `block in
'", ["/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/duck_record-0.0.26/lib/duck_record/locale/en.yml"]] /home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/pry-rails-0.3.6/lib/pry-rails/prompt.rb:36: warning: constant Pry::Prompt::MAP is deprecated [:load_path=, "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activesupport-5.2.1/lib/active_support/i18n_railtie.rb:49:in `block in initialize_i18n'", ["/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activesupport-5.2.1/lib/active_support/locale/en.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activemodel-5.2.1/lib/active_model/locale/en.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.2.1/lib/action_view/locale/en.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-5.2.1/lib/active_record/locale/en.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/mongoid-7.0.2/lib/config/locales/en.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/mongoid_orderable-5.2.0/lib/config/locales/en.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/duck_record-0.0.26/lib/duck_record/locale/en.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/de.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/en.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/es.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/fr.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/it.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/ja.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/pl.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/pt-BR.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/ru.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/tr.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/zh-CN.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/zh-TW.yml",   "/home/z/test_rails/reocar_reimbursement/config/locales/bootstrap.en.yml",   "/home/z/test_rails/reocar_reimbursement/config/locales/bootstrap.zh-CN.yml",   "/home/z/test_rails/reocar_reimbursement/config/locales/compare_methods.zh-CN.yml",   "/home/z/test_rails/reocar_reimbursement/config/locales/dictionary.zh-CN.yml",   "/home/z/test_rails/reocar_reimbursement/config/locales/duck_record.zh-CN.yml",   "/home/z/test_rails/reocar_reimbursement/config/locales/en.yml",   "/home/z/test_rails/reocar_reimbursement/config/locales/models.zh-CN.yml",   "/home/z/test_rails/reocar_reimbursement/config/locales/mongo_errors.zh-CN.yml",   "/home/z/test_rails/reocar_reimbursement/config/locales/zh-CN.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/web-console-3.7.0/lib/web_console/locales/en.yml"]] [:load_path=, "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activesupport-5.2.1/lib/active_support/i18n_railtie.rb:63:in `block in initialize_i18n'", ["/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activesupport-5.2.1/lib/active_support/locale/en.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activemodel-5.2.1/lib/active_model/locale/en.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.2.1/lib/action_view/locale/en.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-5.2.1/lib/active_record/locale/en.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/mongoid-7.0.2/lib/config/locales/en.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/mongoid_orderable-5.2.0/lib/config/locales/en.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/duck_record-0.0.26/lib/duck_record/locale/en.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/de.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/en.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/es.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/fr.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/it.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/ja.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/pl.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/pt-BR.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/ru.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/tr.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/zh-CN.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/apipie-rails-0.5.13/config/locales/zh-TW.yml",   "/home/z/test_rails/reocar_reimbursement/config/locales/bootstrap.en.yml",   "/home/z/test_rails/reocar_reimbursement/config/locales/bootstrap.zh-CN.yml",   "/home/z/test_rails/reocar_reimbursement/config/locales/compare_methods.zh-CN.yml",   "/home/z/test_rails/reocar_reimbursement/config/locales/dictionary.zh-CN.yml",   "/home/z/test_rails/reocar_reimbursement/config/locales/duck_record.zh-CN.yml",   "/home/z/test_rails/reocar_reimbursement/config/locales/en.yml",   "/home/z/test_rails/reocar_reimbursement/config/locales/models.zh-CN.yml",   "/home/z/test_rails/reocar_reimbursement/config/locales/mongo_errors.zh-CN.yml",   "/home/z/test_rails/reocar_reimbursement/config/locales/zh-CN.yml",   "/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/web-console-3.7.0/lib/web_console/locales/en.yml"]]

从以上可知,某些gem是会用I18n.load_path数组的:<<方法来塞入自带的翻译文件的

而rails项目中的多语言yml则是用以下的I18n.load_path += 来加载

此处的代码比较绕。I18n.load_path += value的value来自于app.config.i18n.load_path,而app.config.i18n.load_path则来自于config.i18n.railties_load_path。这里app.config.i18n是一个ActiveSupport::OrderedOptions。在集合的迭代中修改集合应该是不太推荐的,尽管这里修改的是集合的子集合

# activesupport-5.2.1/lib/active_support/i18n_railtie.rb
def self.initialize_i18n(app)
  return if @i18n_inited

  fallbacks = app.config.i18n.delete(:fallbacks)

  # Avoid issues with setting the default_locale by disabling available locales
  # check while configuring.
  enforce_available_locales = app.config.i18n.delete(:enforce_available_locales)
  enforce_available_locales = I18n.enforce_available_locales if enforce_available_locales.nil?
  I18n.enforce_available_locales = false

  reloadable_paths = []
  app.config.i18n.each do |setting, value|
    case setting
    when :railties_load_path
      reloadable_paths = value
      app.config.i18n.load_path.unshift(*value.flat_map(&:existent))
    when :load_path
      I18n.load_path += value
    else
      I18n.send("#{setting}=", value)
    end
  end

为查出config.i18n.railties_load_path的内容是怎样填充进去,把它freeze

# activesupport-5.2.1/lib/active_support/i18n_railtie.rb
module I18n
  class Railtie < Rails::Railtie
    config.i18n = ActiveSupport::OrderedOptions.new
    config.i18n.railties_load_path = [].freeze

使其报错并显示出调用栈

/home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/railties-5.2.1/lib/rails/engine.rb:589:in `block in ': can't modify frozen Array (FrozenError)
    from /home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/railties-5.2.1/lib/rails/initializable.rb:32:in `instance_exec'
    from /home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/railties-5.2.1/lib/rails/initializable.rb:32:in `run'
    from /home/z/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/railties-5.2.1/lib/rails/initializable.rb:61:in `block in run_initializers'
...

可见它是rails初始化时塞入的

# railties-5.2.1/lib/rails/engine.rb
initializer :add_locales do
  config.i18n.railties_load_path << paths["config/locales"]
end

paths方法是委托到railties-5.2.1/lib/rails/engine/configuration.rb的。Rails::Paths::Root的用途看上去比较好懂,就不分析了

def paths
  @paths ||= begin
    paths = Rails::Paths::Root.new(@root)

    paths.add "app",                 eager_load: true, glob: "{*,*/concerns}"
    paths.add "app/assets",          glob: "*"
    paths.add "app/controllers",     eager_load: true
    paths.add "app/channels",        eager_load: true, glob: "**/*_channel.rb"
    paths.add "app/helpers",         eager_load: true
    paths.add "app/models",          eager_load: true
    paths.add "app/mailers",         eager_load: true
    paths.add "app/views"

    paths.add "lib",                 load_path: true
    paths.add "lib/assets",          glob: "*"
    paths.add "lib/tasks",           glob: "**/*.rake"

    paths.add "config"
    paths.add "config/environments", glob: "#{Rails.env}.rb"
    paths.add "config/initializers", glob: "**/*.rb"
    paths.add "config/locales",      glob: "*.{rb,yml}"
    paths.add "config/routes.rb"

    paths.add "db"
    paths.add "db/migrate"
    paths.add "db/seeds.rb"

    paths.add "vendor",              load_path: true
    paths.add "vendor/assets",       glob: "*"

    paths
  end
end