1#![deny(missing_docs)]
2use std::borrow::Cow;
4use std::rc::Rc;
5
6use crate::style::{CustomStylable, CustomStyle, Style, StylePropValue};
7use crate::views::Decorators;
8use crate::{AnyView, IntoView, View, ViewId, prop, prop_extractor, style_class};
9use floem_reactive::UpdaterEffect;
10use fluent_bundle::{FluentBundle, FluentResource};
11use ouroboros::self_referencing;
12use parley::Alignment;
13
14pub use fluent_bundle::FluentArgs;
15pub use fluent_bundle::types::FluentValue;
16pub use unic_langid::LanguageIdentifier;
17
18use super::Label;
19
20#[derive(Clone)]
22pub struct LocaleMap(pub imbl::HashMap<LanguageIdentifier, Rc<FluentBundle<FluentResource>>>);
23
24impl std::ops::Deref for LocaleMap {
25 type Target = imbl::HashMap<LanguageIdentifier, Rc<FluentBundle<FluentResource>>>;
26
27 fn deref(&self) -> &Self::Target {
28 &self.0
29 }
30}
31
32impl std::ops::DerefMut for LocaleMap {
33 fn deref_mut(&mut self) -> &mut Self::Target {
34 &mut self.0
35 }
36}
37
38impl std::fmt::Debug for LocaleMap {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 f.debug_map()
41 .entries(self.0.keys().map(|lang_id| (lang_id, "<FluentBundle>")))
42 .finish()
43 }
44}
45
46impl PartialEq for LocaleMap {
47 fn eq(&self, other: &Self) -> bool {
48 if self.0.len() != other.0.len() {
49 return false;
50 }
51
52 self.0.keys().all(|key| {
53 other.0.contains_key(key)
54 && Rc::ptr_eq(self.0.get(key).unwrap(), other.0.get(key).unwrap())
55 })
56 }
57}
58
59impl StylePropValue for LocaleMap {
60 fn debug_view(&self) -> Option<AnyView> {
61 use crate::prelude::*;
62
63 let languages: Vec<String> = self.0.keys().map(|lang_id| lang_id.to_string()).collect();
64
65 let count = languages.len();
66
67 let view = Stack::new((
68 format!("Languages ({count})").style(|s| {
69 s.font_size(12.0)
70 .font_weight(floem_renderer::text::FontWeight::SEMI_BOLD)
71 }),
72 Stack::vertical_from_iter(languages.into_iter().map(|lang| {
73 lang.style(|s| {
74 s.font_size(11.0)
75 .color(Color::WHITE.with_alpha(0.7))
76 .width_full()
77 .items_center()
78 .justify_center()
79 .text_align(Alignment::Center)
80 })
81 }))
82 .style(|s| s.gap(2.0).width_full()),
83 ))
84 .style(|s| {
85 s.flex_row()
86 .gap(8.0)
87 .items_center()
88 .padding(8.0)
89 .border(1.)
90 .border_color(palette::css::WHITE.with_alpha(0.3))
91 .border_radius(6.0)
92 .min_width(120.0)
93 });
94
95 Some(view.into_any())
96 }
97}
98
99impl StylePropValue for LanguageIdentifier {
100 fn debug_view(&self) -> Option<Box<dyn View>> {
101 Some(crate::views::Label::new(format!("{self:?}")).into_any())
102 }
103
104 fn interpolate(&self, _other: &Self, _value: f64) -> Option<Self> {
105 None
106 }
107}
108
109impl LocaleMap {
110 pub fn from_resources<'a, I>(resources: I) -> Result<Self, Box<dyn std::error::Error>>
121 where
122 I: IntoIterator<Item = (&'a str, &'a str)>,
123 {
124 let mut map = imbl::HashMap::new();
125 for (lang_id, resource_str) in resources {
126 let lang_id = lang_id.parse::<LanguageIdentifier>()?;
127 let resource = FluentResource::try_new(resource_str.to_string())
128 .map_err(|(_, errs)| format!("Failed to parse Fluent resource: {:?}", errs))?;
129
130 let mut bundle = FluentBundle::new(vec![lang_id.clone()]);
131 bundle
132 .add_resource(resource)
133 .map_err(|errs| format!("Failed to add resource to bundle: {:?}", errs))?;
134
135 map.insert(lang_id, Rc::new(bundle));
136 }
137
138 Ok(Self(map))
139 }
140
141 fn find_bundle(
144 &self,
145 locale: &LanguageIdentifier,
146 ) -> Option<&Rc<FluentBundle<FluentResource>>> {
147 self.0.get(locale).or_else(|| {
148 self.0.iter().find_map(|(key, resource)| {
149 if key.language == locale.language {
150 Some(resource)
151 } else {
152 None
153 }
154 })
155 })
156 }
157}
158
159prop!(pub L10nLocale: Option<LanguageIdentifier> { inherited } = sys_locale::get_locale().and_then(|l| l.parse().ok()));
160prop!(pub L10nFallback: Option<String> {} = None);
161prop!(pub L10nBundle: LocaleMap { inherited } = LocaleMap(imbl::HashMap::new()));
162
163prop_extractor! {
164 LanguageExtractor {
165 locale: L10nLocale,
166 bundle: L10nBundle,
167 }
168}
169
170prop_extractor! {
171 FallBackExtractor {
172 fallback: L10nFallback,
173 }
174}
175
176style_class!(
177 pub L10nClass
179);
180
181pub enum L10nState {
183 Arg(usize, FluentValue<'static>),
188 Fallback(String),
191}
192
193#[self_referencing]
194struct L10nArgs {
195 arg_keys: Vec<String>,
196 arg_values: Vec<FluentValue<'static>>,
197 #[borrows(arg_keys)]
198 #[covariant]
199 args: FluentArgs<'this>,
200}
201
202impl L10nArgs {
203 fn empty() -> Self {
204 L10nArgsBuilder {
205 arg_keys: Vec::new(),
206 arg_values: Vec::new(),
207 args_builder: |_| FluentArgs::new(),
208 }
209 .build()
210 }
211
212 fn from_parts(arg_keys: Vec<String>, arg_values: Vec<FluentValue<'static>>) -> Self {
213 let values_for_args = arg_values.clone();
214 L10nArgsBuilder {
215 arg_keys,
216 arg_values,
217 args_builder: move |arg_keys| {
218 let mut args = FluentArgs::with_capacity(values_for_args.len());
219 for (key, value) in arg_keys.iter().zip(values_for_args.iter().cloned()) {
220 args.set(Cow::Borrowed(key.as_str()), value);
221 }
222 args
223 },
224 }
225 .build()
226 }
227
228 fn len(&self) -> usize {
229 self.borrow_arg_keys().len()
230 }
231
232 fn insert(self, arg_key: String, arg_val: FluentValue<'static>) -> Self {
233 let mut heads = self.into_heads();
234 heads.arg_keys.push(arg_key);
235 heads.arg_values.push(arg_val);
236 Self::from_parts(heads.arg_keys, heads.arg_values)
237 }
238
239 fn set(&mut self, offset: usize, arg_val: FluentValue<'static>) {
240 self.with_arg_values_mut(|values| values[offset] = arg_val.clone());
241 self.with_mut(|fields| {
242 let key = fields.arg_keys[offset].as_str();
243 fields.args.set(Cow::Borrowed(key), arg_val);
244 });
245 }
246}
247
248pub struct L10n {
250 id: ViewId,
251 label_id: ViewId,
252 key: String,
253 args: L10nArgs,
254 locale: LanguageExtractor,
255 fallback: FallBackExtractor,
256 fallback_override: Option<String>,
257 has_format_value: bool,
258}
259
260impl L10n {
261 pub fn new(key: impl Into<String>) -> Self {
263 let key: String = key.into();
264 let label = Label::new(key.clone());
265 let label_id = label.id();
266 let id = ViewId::new();
267 id.add_child(label.into_any());
268 Self {
269 id,
270 label_id,
271 key,
272 args: L10nArgs::empty(),
273 locale: Default::default(),
274 fallback: Default::default(),
275 fallback_override: None,
276 has_format_value: false,
277 }
278 .class(L10nClass)
279 }
280
281 pub fn arg<FV: Into<FluentValue<'static>>>(
294 mut self,
295 arg_key: impl Into<String>,
296 arg_val: impl Fn() -> FV + 'static,
297 ) -> Self {
298 let id = self.id;
299 let arg_key = arg_key.into();
300 let offset = self.args.len();
301
302 let initial_val = UpdaterEffect::new(
303 move || arg_val().into(),
304 move |arg_val: FluentValue<'static>| {
305 id.update_state(L10nState::Arg(offset, arg_val));
306 },
307 );
308 self.args = self.args.insert(arg_key, initial_val);
309 self
310 }
311
312 pub fn fallback<S: Into<String>>(mut self, fallback: impl Fn() -> S + 'static) -> Self {
329 let id = self.id;
330 let initial_fallback = UpdaterEffect::new(
331 move || fallback().into(),
332 move |fallback| {
333 id.update_state(L10nState::Fallback(fallback));
334 },
335 );
336 self.fallback_override = Some(initial_fallback);
337 self
338 }
339
340 fn try_format_message(&self) -> Option<String> {
341 let bundle = self.locale.bundle();
342 let locale = self.locale.locale()?;
343 let resource = bundle.find_bundle(&locale)?;
344 let message = resource.get_message(&self.key)?;
345 let pattern = message.value()?;
346 let errors = &mut vec![];
347 self.args.with_args(|args| {
348 let value = resource.format_pattern(pattern, Some(args), errors);
349 if errors.is_empty() {
350 Some(value.to_string())
351 } else {
352 None
353 }
354 })
355 }
356
357 fn apply_fallback(&self) -> bool {
358 if let Some(fallback) = &self.fallback_override {
359 self.label_id.update_state(fallback.to_string());
360 true
361 } else if let Some(fallback) = self.fallback.fallback() {
362 self.label_id.update_state(fallback.to_string());
363 true
364 } else {
365 false
366 }
367 }
368}
369
370pub fn l10n(label_key: impl Into<String>) -> L10n {
457 L10n::new(label_key)
458}
459
460impl View for L10n {
461 fn id(&self) -> ViewId {
462 self.id
463 }
464
465 fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
466 if self.locale.read(cx) {
467 self.has_format_value = false;
468 }
469 if !self.has_format_value
470 && let Some(formatted) = self.try_format_message()
471 {
472 self.label_id.update_state(formatted);
473 self.has_format_value = true;
474 }
475 self.fallback.read(cx);
476 if !self.has_format_value && !self.apply_fallback() {
477 self.label_id.update_state(self.key.clone());
478 }
479 }
480
481 fn update(&mut self, _cx: &mut crate::context::UpdateCx, state: Box<dyn std::any::Any>) {
482 if let Ok(inner) = state.downcast::<L10nState>() {
483 match *inner {
484 L10nState::Arg(stack_offset, fluent_value) => {
485 self.has_format_value = false;
486 self.args.set(stack_offset, fluent_value);
487
488 if let Some(formatted) = self.try_format_message() {
489 self.label_id.update_state(formatted);
490 self.has_format_value = true;
491 }
492
493 if !self.has_format_value && !self.apply_fallback() {
494 self.label_id.update_state(self.key.clone());
495 }
496 }
497 L10nState::Fallback(fallback_override) => {
498 self.fallback_override = Some(fallback_override);
499 if !self.has_format_value {
500 self.label_id
501 .update_state(self.fallback_override.clone().unwrap());
502 }
503 }
504 }
505 }
506 }
507}
508
509#[derive(Debug, Clone)]
511pub struct L10nCustomStyle(Style);
512
513impl From<L10nCustomStyle> for Style {
514 fn from(value: L10nCustomStyle) -> Self {
515 value.0
516 }
517}
518
519impl From<Style> for L10nCustomStyle {
520 fn from(value: Style) -> Self {
521 Self(value)
522 }
523}
524
525impl CustomStyle for L10nCustomStyle {
526 type StyleClass = L10nClass;
527}
528
529impl CustomStylable<L10nCustomStyle> for L10n {
530 type DV = Self;
531}
532
533impl L10nCustomStyle {
534 pub fn new() -> Self {
536 Self(Style::new())
537 }
538
539 pub fn locale(mut self, locale_key: impl Into<LanguageIdentifier>) -> Self {
541 let locale = locale_key.into();
542 self = Self(self.0.set(L10nLocale, Some(locale)));
543 self
544 }
545
546 pub fn fallback(mut self, fallback: impl Into<String>) -> Self {
549 let string = fallback.into();
550 self = Self(self.0.set(L10nFallback, Some(string)));
551 self
552 }
553
554 pub fn apply_opt<T>(self, opt: Option<T>, f: impl FnOnce(Self, T) -> Self) -> Self {
556 if let Some(t) = opt { f(self, t) } else { self }
557 }
558
559 pub fn bundle(mut self, bundle: impl Into<LocaleMap>) -> Self {
561 self = Self(self.0.set(L10nBundle, bundle));
562 self
563 }
564}
565
566impl Default for L10nCustomStyle {
567 fn default() -> Self {
568 Self::new()
569 }
570}