locale.rs 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. //! Process-global locale store + locale-aware task spawner.
  2. //!
  3. //! `fluent_i18n::set_locale` writes to a `thread_local!` `RefCell`, so
  4. //! `tokio::spawn` worker threads do NOT inherit the locale set on the main
  5. //! thread — every `t!` call inside a spawned task falls back to English.
  6. //! This module closes that gap: [`set_global_locale`] captures the locale
  7. //! once at startup (process-global `OnceLock`), and [`spawn_with_locale`]
  8. //! re-applies it at the start of each spawned task so `t!` renders in the
  9. //! user's locale, not the fallback.
  10. use std::future::Future;
  11. use std::sync::OnceLock;
  12. use tokio::task::JoinHandle;
  13. /// Process-global locale string (e.g. `"ru-RU"`), set once on the main
  14. /// thread at startup. Read by [`spawn_with_locale`] before each task.
  15. static GLOBAL_LOCALE: OnceLock<Option<String>> = OnceLock::new();
  16. /// Capture the locale once at startup. Called from `main` after
  17. /// `set_locale`. Subsequent calls are no-ops (first wins).
  18. pub fn set_global_locale(locale: Option<String>) {
  19. let _ = GLOBAL_LOCALE.set(locale);
  20. }
  21. /// Like `tokio::spawn`, but re-applies the process-global locale to the
  22. /// worker thread's thread-local before polling the future, so `t!` calls
  23. /// inside the task render in the user's locale.
  24. pub fn spawn_with_locale<F, T>(future: F) -> JoinHandle<T>
  25. where
  26. F: Future<Output = T> + Send + 'static,
  27. T: Send + 'static,
  28. {
  29. let locale = GLOBAL_LOCALE.get().and_then(|opt| opt.clone());
  30. tokio::spawn(async move {
  31. if let Some(ref loc) = locale {
  32. let _ = fluent_i18n::set_locale(Some(loc.as_str()));
  33. }
  34. future.await
  35. })
  36. }
  37. /// Re-apply the process-global locale to the CURRENT thread. Call at the
  38. /// top of a `tokio::task::spawn_blocking` closure (those threads are
  39. /// fresh and do not inherit the thread-local locale either).
  40. pub fn apply_global_locale() {
  41. if let Some(loc) = GLOBAL_LOCALE.get().and_then(|opt| opt.clone()) {
  42. let _ = fluent_i18n::set_locale(Some(loc.as_str()));
  43. }
  44. }
  45. #[cfg(test)]
  46. mod tests {
  47. use super::*;
  48. #[tokio::test]
  49. async fn spawn_with_locale_inherits_locale() {
  50. set_global_locale(Some("ru-RU".to_string()));
  51. let result = spawn_with_locale(async {
  52. fluent_i18n::t!(
  53. "composed-dir-built",
  54. {
  55. "host" => "/tmp/x".to_string(),
  56. "node" => "n".to_string(),
  57. "sources" => 3u32,
  58. "files" => 1u32,
  59. "local" => 2u32,
  60. }
  61. )
  62. })
  63. .await
  64. .unwrap();
  65. assert!(
  66. result.contains("источник"),
  67. "expected Russian output (источник), got: {result}"
  68. );
  69. }
  70. }