cranpose_ui/
localization.rs1use std::cell::RefCell;
23
24use cranpose_core::{
25 CompositionLocal, CompositionLocalProvider, compositionLocalOf, remember, rememberKeyed,
26};
27pub use cranpose_localization::{
28 Argument, Catalog, DeferredMessage, FluentValue, Language, LanguagePreference,
29 LanguagePreferenceStore, Locale, Localization, LocalizationError, Message, PreviewMode,
30 Resource, SourceCatalog, Translator,
31};
32
33pub type LocalizedText = crate::text::SharedText;
35
36#[cfg(feature = "localization-formatting")]
37pub use cranpose_localization::{FormatError, LocaleFormatters};
38
39pub use crate::ui_strings::catalog as framework_catalog;
42
43#[cfg(feature = "localization-formatting")]
46#[track_caller]
47pub fn local_formatters() -> Result<std::rc::Rc<LocaleFormatters>, FormatError> {
48 if let Some(translator) = local_translator().current() {
49 return translator.formatters();
50 }
51 rememberKeyed((), |()| {
52 LocaleFormatters::new(&Locale::parse("en").expect("source locale")).map(std::rc::Rc::new)
53 })
54}
55
56#[track_caller]
59pub fn localized_message(message: &DeferredMessage) -> LocalizedText {
60 let translator = local_translator().current();
61 rememberKeyed((message.clone(), translator), |(message, translator)| {
62 LocalizedText::from(message.format(translator.as_ref()).unwrap_or_else(|error| {
63 log::error!("{error}");
64 message.fallback_text()
65 }))
66 })
67}
68
69pub fn local_translator() -> CompositionLocal<Option<Translator>> {
72 crate::environment_locals::ENVIRONMENT_LOCALS.with(|locals| {
73 locals
74 .translator
75 .get_or_init(|| compositionLocalOf(|| None))
76 .clone()
77 })
78}
79
80#[expect(non_snake_case)]
84#[track_caller]
85pub fn ProvideLocalization(catalog: &Catalog, locale: Locale, content: impl FnOnce()) {
86 let translator = rememberKeyed((catalog.clone(), locale), |(catalog, locale)| {
87 catalog
88 .clone()
89 .with_fallback(&crate::ui_strings::catalog())
90 .translator(locale.clone())
91 });
92 provide(translator, content);
93}
94
95#[expect(non_snake_case)]
97#[track_caller]
98pub fn ProvideTranslator(translator: Translator, content: impl FnOnce()) {
99 let translator = rememberKeyed(translator, |translator| {
100 translator.with_fallback(&crate::ui_strings::catalog())
101 });
102 provide(translator, content);
103}
104
105#[track_caller]
106fn provide(translator: Translator, content: impl FnOnce()) {
107 let direction = if translator.locale().is_rtl() {
108 crate::LayoutDirection::Rtl
109 } else {
110 crate::LayoutDirection::Ltr
111 };
112 let locales = rememberKeyed(translator.locale().clone(), |locale| {
113 crate::text::LocaleList::new(vec![locale.to_string()])
114 });
115 CompositionLocalProvider(
116 [
117 local_translator().provides(Some(translator)),
118 crate::local_layout_direction().provides(direction),
119 text_locales().provides(Some(locales)),
120 ],
121 content,
122 );
123}
124
125fn text_locales() -> CompositionLocal<Option<crate::text::LocaleList>> {
126 crate::environment_locals::ENVIRONMENT_LOCALS.with(|locals| {
127 locals
128 .text_locales
129 .get_or_init(|| compositionLocalOf(|| None))
130 .clone()
131 })
132}
133
134pub(crate) fn apply_text_locale(mut style: crate::text::TextStyle) -> crate::text::TextStyle {
135 if style.span_style.locale_list.is_none() {
136 style.span_style.locale_list = text_locales().current();
137 }
138 style
139}
140
141struct CachedTranslation {
142 translator: Option<Translator>,
143 message: &'static Message,
144 arguments: Vec<Argument<'static>>,
145 text: LocalizedText,
146}
147
148#[track_caller]
151pub fn localized<const N: usize>(
152 message: &'static Message,
153 mut arguments: [Argument<'_>; N],
154) -> LocalizedText {
155 localized_impl(message, &mut arguments)
156}
157
158#[track_caller]
159fn localized_impl(message: &'static Message, arguments: &mut [Argument<'_>]) -> LocalizedText {
160 let translator = local_translator().current();
161 let slot = remember(|| RefCell::new(None::<CachedTranslation>));
162 slot.with(|slot| {
163 let mut cached = slot.borrow_mut();
164 if let Some(value) = &*cached
165 && value.translator == translator
166 && std::ptr::eq(value.message, message)
167 && value.arguments.as_slice() == &*arguments
168 {
169 return value.text.clone();
170 }
171 let formatted = match &translator {
172 Some(translator) => translator.format(message, arguments),
173 None => message.format_source(arguments),
174 };
175 let text = LocalizedText::from(formatted.unwrap_or_else(|error| {
176 log::error!("{error}");
177 message.fallback_text(arguments)
178 }));
179 if let Some(value) = &mut *cached {
180 value.translator = translator;
181 value.message = message;
182 value.text = text.clone();
183 for (index, argument) in arguments.iter_mut().enumerate() {
184 match value.arguments.get_mut(index) {
185 Some(previous) => previous.update_from(argument),
186 None => value.arguments.push(argument.take_owned()),
187 }
188 }
189 value.arguments.truncate(arguments.len());
190 } else {
191 *cached = Some(CachedTranslation {
192 translator,
193 message,
194 arguments: arguments.iter_mut().map(Argument::take_owned).collect(),
195 text: text.clone(),
196 });
197 }
198 text
199 })
200}