这是indexloc提供的服务,不要输入任何密码
Skip to content

[BUG] I18n.locale resets in Fiber context due to Thread.current fiber-local storage #723

@lee266

Description

@lee266

What I tried to do

I observed that I18n.locale is unexpectedly reset when entering a new Fiber context, because I18n stores its config using Thread.current[:i18n_config].
In Ruby, Thread.current[:key] is fiber-local, not strictly thread-local, so each Fiber gets its own isolated config.

This behavior may affect Fiber-based environments, including:

  • CSV library v3.2.6+ (uses Fibers internally for enumeration)
  • Falcon web server (uses Fibers for request handling)
  • Code that relies on Enumerator (which internally creates Fibers)

What I expected to happen

I18n.locale should be consistent across Fibers in the same Thread.

[before] I18n.locale=en
[fiber] I18n.locale=en
[after] I18n.locale=en

What actually happened

When a new Fiber is created, I18n.locale falls back to I18n.default_locale because the config is missing in the new Fiber context.

[before] I18n.locale=en
[fiber] I18n.locale=ja
[after] I18n.locale=en

Versions of i18n, rails, and anything else you think is necessary

Ruby version: 3.4.6
I18n version: 1.14.7


Steps to reproduce

require "i18n"

I18n.available_locales = [:en, :ja]
I18n.default_locale = :ja
I18n.locale = :en

puts "[before] I18n.locale=#{I18n.locale}"  # => en

Fiber.new do
  puts "[fiber] I18n.locale=#{I18n.locale}"  # => ja (unexpected!)
end.resume

puts "[after] I18n.locale=#{I18n.locale}"   # => en

Root cause analysis

In lib/i18n.rb, the config method uses fiber-local storage:

def config
  Thread.current[:i18n_config] ||= I18n::Config.new
end

However, Thread.current[:key] is fiber-local in Ruby.
Each Fiber has its own Thread.current hash, so :i18n_config can be missing/reset when code runs inside a Fiber, causing I18n.locale to fall back to default_locale.

Real-world example

This breaks behavior in the CSV library (v3.2.6+):

require "csv"
require "i18n"

I18n.locale = :en

CSV.parse("name\nTaro\n", headers: true, header_converters: [
  ->(f, info) { puts I18n.locale }  # => :ja (wrong!)
])

This happens because CSV internally uses Enumerator#next, which creates Fibers.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions