bug_id: BUG-2026-07-27T120657 status: open severity: medium scope: i18n
Actual: When the user's locale is non-English (e.g. LANG=ru_RU.UTF-8),
messages emitted from inside tokio::spawn worker tasks render in English
(the fallback), not the user's locale.
Expected: All user-facing messages — including those from spawned
parallel tasks (serve, before_serve, after_stop, power, health, etc.) —
render in the locale set via LANG.
Reproduce:
LANG=ru_RU.UTF-8 cargo run -- -d serve --recipe test-composed
# → "[dry-run] would build composed model dir ... 3 sources, 1 file"
# (English; should be Russian: "3 источника, 1 файл")
Main-thread t! calls DO render in the user's locale (verified: a
main-thread t!("composed-dir-built", ...) with set_locale("ru-RU")
prints "3 источника, 1 файл"). Only spawned tasks are affected.
Code path: main.rs calls fluent_i18n::set_locale(locale) during
startup. t! macro calls read the current locale. Spawned tasks call
t! from worker threads.
Why it fails: fluent_i18n::set_locale writes to a thread_local!
RefCell<Option<LanguageIdentifier>> (confirmed by reading the crate
source: CURRENT_LOCALE: RefCell<Option<LanguageIdentifier>>).
tokio::spawn schedules the task onto a worker thread that has its own,
independent thread-local slot — which was never set. The t! macro reads
None there, falls back to the static fallback locale (en-US).
Contributing factor: The launcher uses tokio::spawn for parallel
per-node SSH/Docker work across 16 call sites. Every t! call inside
those spawned blocks is affected.
Risk level: Medium. No data loss, no security boundary. User-facing i18n is broken for non-English locales in the async paths; English (the fallback and most common case) is unaffected. 16 spawn sites need migration.
diagnose-root 4-phase RCA (performed earlier this session):
LANG=ru_RU.UTF-8 ./sm121 -d serve --recipe test-composed
→ English output from spawned composed-dir builder.set_locale("ru-RU") succeeds on main;
get_locale() returns ru-RU on main. A t! call on the main thread
returns Russian. A t! call inside tokio::spawn returns English.fluent_i18n::set_locale is thread-local; tokio
worker threads don't inherit the main-thread locale.fluent-i18n source — CURRENT_LOCALE is declared
thread_local! { RefCell<Option<LanguageIdentifier>> }. Confirmed.Fix approach: Wrap tokio::spawn with a locale-aware spawner that
captures the locale from a process-global store (set once on main) and
calls set_locale at the start of each spawned task. Migrate the 16
tokio::spawn call sites to the wrapper.
RED: Write a test that sets the global locale to ru-RU, spawns a
task via the (not-yet-existing) spawn_with_locale helper, calls
t!("composed-dir-built", ...), and asserts the result contains a
Russian word (e.g. источник). Test fails to compile (helper missing).
GREEN: Create src/locale.rs with a OnceLock<Option<String>>
global + set_global_locale() + spawn_with_locale() wrapper that
calls fluent_i18n::set_locale before awaiting the future.
verify: cargo test -- spawn_with_locale_inherits_locale
RED: Add a characterization test proving a bare tokio::spawn (no
wrapper) still returns English despite the global locale being set —
documents the underlying crate behavior we're working around.
GREEN: No code change (characterization test; pins the bug).
verify: cargo test -- bare_spawn_falls_back_to_english
REFACTOR: Migrate the 16 tokio::spawn(async move { ... }) call
sites to locale::spawn_with_locale(async move { ... }). Mechanical
replacement; each site's JoinHandle return type is unchanged.
verify: make check (fmt + clippy + 128+ tests + ≤500 lines)
src/tmux/ops.rs:203 src/tmux/serve.rs:108
src/before_serve.rs:51 src/cluster_state.rs:49
src/doctor.rs:74 src/power.rs:66, :301
src/after_stop.rs:42 src/health.rs:64
src/exec.rs:77 src/docker/logs.rs:77
src/docker/mod.rs:367 src/docker/deploy.rs:47
src/docker/health.rs:31, :77, :224
cargo test -- spawn_with_locale_inherits_locale passescargo test -- bare_spawn_falls_back_to_english passes (characterization)make check green (fmt + clippy + all tests + ≤500 lines)LANG=ru_RU.UTF-8 cargo run -- -d serve --recipe test-composed prints
Russian for the composed-dir dry-run messagetokio::spawn sites migrated to spawn_with_locale#[allow(...)] attributesFixed in: 4ece367 (GREEN) + 16f4239 (RED test)
Branch: fix-i18n-spawn-locale
Fix: Added src/locale.rs with a process-global OnceLock<Option<String>>
locale store + spawn_with_locale — a drop-in tokio::spawn wrapper that
calls fluent_i18n::set_locale inside the spawned task before polling
the future. Migrated all 17 tokio::spawn call sites to the wrapper.
main.rs calls set_global_locale once at startup.
Verified:
spawn_with_locale_inherits_locale passesmake check green (129 tests, fmt, clippy, ≤500 lines)LANG=ru_RU.UTF-8 dry-run serve prints Russian from spawned tasksLANG=de_DE.UTF-8 prints German ("3 Quellen, 1 Datei")