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
141pub(crate) fn apply_shared_text_locale(
144 style: std::sync::Arc<crate::text::TextStyle>,
145) -> std::sync::Arc<crate::text::TextStyle> {
146 if style.span_style.locale_list.is_some() {
147 return style;
148 }
149 crate::text_modifier_node::shared_text_style(apply_text_locale(crate::text::TextStyle::clone(
150 &style,
151 )))
152}
153
154struct CachedTranslation {
155 translator: Option<Translator>,
156 message: &'static Message,
157 arguments: Vec<Argument<'static>>,
158 text: LocalizedText,
159}
160
161#[track_caller]
164pub fn localized<const N: usize>(
165 message: &'static Message,
166 mut arguments: [Argument<'_>; N],
167) -> LocalizedText {
168 localized_impl(message, &mut arguments)
169}
170
171#[track_caller]
172fn localized_impl(message: &'static Message, arguments: &mut [Argument<'_>]) -> LocalizedText {
173 let translator = local_translator().current();
174 let slot = remember(|| RefCell::new(None::<CachedTranslation>));
175 slot.with(|slot| {
176 let mut cached = slot.borrow_mut();
177 if let Some(value) = &*cached
178 && value.translator == translator
179 && std::ptr::eq(value.message, message)
180 && value.arguments.as_slice() == &*arguments
181 {
182 return value.text.clone();
183 }
184 let formatted = match &translator {
185 Some(translator) => translator.format(message, arguments),
186 None => message.format_source(arguments),
187 };
188 let text = LocalizedText::from(formatted.unwrap_or_else(|error| {
189 log::error!("{error}");
190 message.fallback_text(arguments)
191 }));
192 if let Some(value) = &mut *cached {
193 value.translator = translator;
194 value.message = message;
195 value.text = text.clone();
196 for (index, argument) in arguments.iter_mut().enumerate() {
197 match value.arguments.get_mut(index) {
198 Some(previous) => previous.update_from(argument),
199 None => value.arguments.push(argument.take_owned()),
200 }
201 }
202 value.arguments.truncate(arguments.len());
203 } else {
204 *cached = Some(CachedTranslation {
205 translator,
206 message,
207 arguments: arguments.iter_mut().map(Argument::take_owned).collect(),
208 text: text.clone(),
209 });
210 }
211 text
212 })
213}