rstest_bdd/
localization.rs1use std::cell::RefCell;
4use std::sync::{LazyLock, RwLock};
5
6use fluent::FluentArgs;
7use i18n_embed::I18nEmbedError;
8use i18n_embed::fluent::{FluentLanguageLoader, fluent_language_loader};
9use rust_embed::RustEmbed;
10use thiserror::Error;
11use unic_langid::LanguageIdentifier;
12
13#[derive(RustEmbed)]
28#[folder = "i18n"]
29pub struct Localizations;
30
31static LANGUAGE_LOADER: LazyLock<RwLock<FluentLanguageLoader>> = LazyLock::new(|| {
32 let loader = fluent_language_loader!();
33 i18n_embed::select(&loader, &Localizations, &[unic_langid::langid!("en-US")])
34 .unwrap_or_else(|error| panic!("failed to load default English translations: {error}"));
35 RwLock::new(loader)
36});
37
38thread_local! {
39 static OVERRIDE_LOADER: RefCell<Option<FluentLanguageLoader>> = const { RefCell::new(None) };
40}
41
42#[derive(Debug, Error)]
44pub enum LocalizationError {
45 #[error("localization state is poisoned")]
47 Poisoned,
48 #[error("failed to load localization resources: {0}")]
50 Loader(#[from] I18nEmbedError),
51}
52
53#[must_use]
56pub struct ScopedLocalization {
57 previous: Option<FluentLanguageLoader>,
58}
59
60impl ScopedLocalization {
61 pub fn new(requested: &[LanguageIdentifier]) -> Result<Self, LocalizationError> {
69 let loader = fluent_language_loader!();
70 i18n_embed::select(&loader, &Localizations, requested)?;
71 let previous = OVERRIDE_LOADER.with(|cell| cell.replace(Some(loader)));
72 Ok(Self { previous })
73 }
74}
75
76impl Drop for ScopedLocalization {
77 fn drop(&mut self) {
78 let previous = self.previous.take();
79 OVERRIDE_LOADER.with(|cell| {
80 *cell.borrow_mut() = previous;
81 });
82 }
83}
84
85pub fn install_localization_loader(loader: FluentLanguageLoader) -> Result<(), LocalizationError> {
91 let mut guard = LANGUAGE_LOADER
92 .write()
93 .map_err(|_| LocalizationError::Poisoned)?;
94 *guard = loader;
95 Ok(())
96}
97
98pub fn select_localizations(
105 requested: &[LanguageIdentifier],
106) -> Result<Vec<LanguageIdentifier>, LocalizationError> {
107 OVERRIDE_LOADER.with(|cell| -> Result<_, LocalizationError> {
108 if let Some(loader) = cell.borrow_mut().as_mut() {
109 let selected = i18n_embed::select(loader, &Localizations, requested)?;
110 return Ok(selected);
111 }
112 let guard = LANGUAGE_LOADER
113 .read()
114 .map_err(|_| LocalizationError::Poisoned)?;
115 let selected = i18n_embed::select(&*guard, &Localizations, requested)?;
116 Ok(selected)
117 })
118}
119
120pub fn current_languages() -> Result<Vec<LanguageIdentifier>, LocalizationError> {
126 OVERRIDE_LOADER.with(|cell| -> Result<_, LocalizationError> {
127 if let Some(loader) = cell.borrow().as_ref() {
128 return Ok(loader.current_languages());
129 }
130 let guard = LANGUAGE_LOADER
131 .read()
132 .map_err(|_| LocalizationError::Poisoned)?;
133 Ok(guard.current_languages())
134 })
135}
136
137#[must_use]
138pub fn message(id: &str) -> String {
149 with_loader(|loader| loader.get(id))
150}
151
152#[must_use]
153pub fn message_with_args<F>(id: &str, configure: F) -> String
164where
165 F: FnOnce(&mut FluentArgs<'static>),
166{
167 with_loader(|loader| message_with_loader(loader, id, configure))
168}
169
170pub(crate) fn message_with_loader<F>(
171 loader: &FluentLanguageLoader,
172 id: &str,
173 configure: F,
174) -> String
175where
176 F: FnOnce(&mut FluentArgs<'static>),
177{
178 let mut args: FluentArgs<'static> = FluentArgs::new();
179 configure(&mut args);
180 loader.get_args_fluent(id, Some(&args))
181}
182
183pub(crate) fn with_loader<R>(callback: impl FnOnce(&FluentLanguageLoader) -> R) -> R {
184 OVERRIDE_LOADER.with(|cell| {
185 let borrow = cell.borrow();
186 if let Some(loader) = borrow.as_ref() {
187 return callback(loader);
188 }
189 drop(borrow);
190 let guard = LANGUAGE_LOADER
191 .read()
192 .unwrap_or_else(std::sync::PoisonError::into_inner);
193 callback(&guard)
194 })
195}
196
197#[must_use]
199pub fn strip_directional_isolates(text: &str) -> String {
200 text.chars()
201 .filter(|c| !matches!(*c, '\u{2066}' | '\u{2067}' | '\u{2068}' | '\u{2069}'))
202 .collect()
203}
204
205#[macro_export]
207macro_rules! panic_localized {
208 ($id:expr $(, $key:ident = $value:expr )* $(,)?) => {{
209 let message = $crate::localization::message_with_args($id, |args| {
210 $( args.set(stringify!($key), $value.to_string()); )*
211 });
212 panic!("{message}");
213 }};
214}