1use std::{borrow::Cow, collections::HashMap, fmt, fs, path::Path, sync::Arc, time::Instant};
238
239use fluent_bundle::{FluentArgs, FluentResource, FluentValue, concurrent::FluentBundle};
240use log::error;
241use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
242use sys_locale::get_locale;
243use unic_langid::LanguageIdentifier;
244
245#[cfg(feature = "icu")]
246use fixed_decimal::{Decimal, FloatPrecision};
247#[cfg(feature = "icu")]
248use fluent_bundle::types::FluentType;
249#[cfg(feature = "icu")]
250use icu::{
251 calendar::Iso,
252 datetime::{
253 DateTimeFormatter,
254 fieldsets::{
255 self,
256 enums::{DateAndTimeFieldSet, DateFieldSet, TimeFieldSet},
257 },
258 options::Length,
259 },
260 decimal::{
261 DecimalFormatter,
262 options::{DecimalFormatterOptions, GroupingStrategy},
263 },
264};
265#[cfg(feature = "icu")]
266use time::{Date, OffsetDateTime, Time};
267
268#[cfg(feature = "icu")]
269use std::str::FromStr;
270
271use crate::{
272 prelude::*,
273 reactive::{VarKey, VarReadGuard},
274 util::ResourceInfo,
275};
276
277struct TranslationMapInner {
278 last_loaded: Instant,
279 current_locale: LanguageIdentifier,
280 translations: HashMap<LanguageIdentifier, TranslationFile>,
281}
282
283#[derive(Clone)]
289pub struct TranslationMap {
290 inner: Arc<Var<TranslationMapInner>>,
292}
293
294impl Default for TranslationMap {
295 fn default() -> Self {
297 Self::new(get_locale().map_or_else(|| unic_langid::langid!("en-US"), |locale| locale.parse().unwrap_or(unic_langid::langid!("en-US"))))
298 }
299}
300
301impl TranslationMap {
302 pub fn new(current_locale: LanguageIdentifier) -> Self {
304 Self {
305 inner: Arc::new(Var::new(TranslationMapInner {
306 last_loaded: Instant::now(),
307 current_locale,
308 translations: HashMap::new(),
309 })),
310 }
311 }
312
313 pub fn add_translation(self, file: TranslationFile) -> Self {
315 let mut guard = self.inner.write();
316 guard.translations.insert(file.bundle.locales[0].clone(), file);
318 guard.last_loaded = Instant::now();
319 drop(guard);
320 self
321 }
322
323 pub fn get_bundle(&self, locale: &LanguageIdentifier) -> Option<VarReadGuard<'_, FluentBundle<FluentResource>>> {
325 VarReadGuard::try_map(self.inner.read(), |inner| inner.translations.get(locale).map(|f| &f.bundle)).ok()
326 }
327
328 pub fn get_current_locale(&self) -> VarReadGuard<'_, LanguageIdentifier> {
330 VarReadGuard::map(self.inner.read(), |inner| &inner.current_locale)
331 }
332
333 pub fn set_current_locale(&mut self, locale: LanguageIdentifier) {
335 self.inner.write().current_locale = locale;
336 }
337
338 pub fn reload(&mut self) -> Result<bool, (bool, Vec<std::io::Error>)> {
347 let mut guard = self.inner.write();
348
349 let mut reloaded = false;
350 let mut errors = Vec::new();
351
352 for file in guard.translations.values_mut() {
353 match file.reload() {
354 Ok(did_load) => reloaded |= did_load,
355 Err(error) => errors.push(error),
356 }
357 }
358
359 if reloaded {
360 guard.last_loaded = Instant::now();
361 } else {
362 guard.cancel_change();
364 }
365
366 if !errors.is_empty() { Err((reloaded, errors)) } else { Ok(reloaded) }
367 }
368}
369
370pub struct TranslationFile {
374 info: Option<ResourceInfo>,
375 bundle: FluentBundle<FluentResource>,
376}
377
378impl TranslationFile {
379 pub fn from_str(locales: Vec<LanguageIdentifier>, text: &str) -> Result<Self, std::io::Error> {
383 let bundle = Self::make_bundle(locales, text.to_string())?;
384 Ok(Self { info: None, bundle })
385 }
386
387 pub fn from_file(locales: Vec<LanguageIdentifier>, path: impl AsRef<Path>) -> Result<TranslationFile, std::io::Error> {
391 let path_buf = path.as_ref().canonicalize()?;
392 let text = fs::read_to_string(&path_buf)?;
393 let bundle = Self::make_bundle(locales, text)?;
394 let info = Some(ResourceInfo {
395 last_modified: fs::metadata(&path_buf)?.modified()?,
396 path: path_buf.clone(),
397 });
398
399 Ok(Self { info, bundle })
400 }
401
402 pub(crate) fn reload(&mut self) -> Result<bool, std::io::Error> {
403 if let Some(ref mut resource_info) = self.info {
404 let current_modified_time = fs::metadata(&resource_info.path)?.modified()?;
405
406 if current_modified_time > resource_info.last_modified {
407 let text = fs::read_to_string(&resource_info.path)?;
409 self.bundle = Self::make_bundle(self.bundle.locales.clone(), text)?;
410
411 resource_info.last_modified = current_modified_time;
412
413 return Ok(true);
414 }
415 }
416
417 Ok(false)
418 }
419
420 fn make_bundle(locales: Vec<LanguageIdentifier>, text: String) -> Result<FluentBundle<FluentResource>, std::io::Error> {
422 if locales.is_empty() {
423 return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Locales list cannot be empty"));
424 }
425
426 let resource = match FluentResource::try_new(text) {
427 Ok(res) => res,
428 Err((res, error_list)) => {
429 for error in error_list {
430 error!("{error}");
431 }
432 res
433 }
434 };
435 let mut bundle = FluentBundle::new_concurrent(locales.clone());
436
437 Self::add_custom_formatters(&mut bundle, &locales);
438
439 if let Err(error_list) = bundle.add_resource(resource) {
440 for error in error_list {
441 error!("{error}");
442 }
443 }
444 Ok(bundle)
445 }
446
447 #[cfg(not(feature = "icu"))]
448 fn add_custom_formatters(bundle: &mut FluentBundle<FluentResource>, _: &[LanguageIdentifier]) {
449 let _ = bundle.add_function("NUMBER", |args, named_args| {
450 let value = match args.first() {
451 Some(FluentValue::Number(n)) => n.value,
452 Some(v) => return v.clone(),
453 None => return FluentValue::Error,
454 };
455
456 if !value.is_finite() {
459 return FluentValue::from(value.to_string());
460 }
461
462 let get_opt = |name: &str| -> Option<i64> {
463 match named_args.get(name) {
464 Some(FluentValue::Number(n)) if n.value.is_finite() => Some(n.value.trunc() as i64),
465 _ => None,
466 }
467 };
468
469 const MAX_FRAC: i64 = 20;
470 const MAX_SIG: i64 = 21;
471 const MAX_MIN_INT: i64 = 308;
472
473 let use_grouping = named_args
474 .get("useGrouping")
475 .map(|v| match v {
476 FluentValue::String(s) => s.as_ref() != "false",
477 _ => true,
478 })
479 .unwrap_or(true);
480
481 let min_integer_digits = get_opt("minimumIntegerDigits").map(|v| v.clamp(1, MAX_MIN_INT) as usize).unwrap_or(1);
482 let max_fraction_digits = get_opt("maximumFractionDigits").map(|v| v.clamp(0, MAX_FRAC) as usize);
483
484 let mut min_fraction_digits = get_opt("minimumFractionDigits").map(|v| v.clamp(0, MAX_FRAC) as usize).unwrap_or(0);
485 if let Some(maxf) = max_fraction_digits {
486 min_fraction_digits = min_fraction_digits.min(maxf);
487 }
488
489 let min_sig_raw = get_opt("minimumSignificantDigits").map(|v| v.clamp(1, MAX_SIG) as usize);
490 let max_sig_raw = get_opt("maximumSignificantDigits").map(|v| v.clamp(1, MAX_SIG) as usize);
491
492 let sig_mode = min_sig_raw.is_some() || max_sig_raw.is_some();
493
494 let text = if sig_mode {
495 let mut min_sig = min_sig_raw.unwrap_or(1);
496 let max_sig = max_sig_raw.unwrap_or(21);
497 if max_sig < min_sig {
498 min_sig = max_sig;
499 }
500
501 let rounded = if let Some(user_max) = max_sig_raw {
503 if value != 0.0 && value.is_finite() {
504 let log10 = value.abs().log10();
505 let magnitude = if log10.is_finite() { log10.floor() as isize } else { 0 };
506
507 let power = -(magnitude - (user_max as isize - 1));
509 let factor = 10f64.powi(power as i32);
510
511 if !factor.is_finite() || factor == 0.0 {
513 value
514 } else {
515 let r = (value * factor).round() / factor;
516 if r.is_finite() { r } else { value }
517 }
518 } else {
519 value
520 }
521 } else {
522 value
523 };
524
525 let mut s = if let Some(user_max) = max_sig_raw {
527 let abs = rounded.abs();
528 if abs != 0.0 && abs.is_finite() {
529 let magnitude = abs.log10().floor() as isize;
530 let frac = ((user_max as isize - 1) - magnitude).max(0) as usize;
531 format!("{:.*}", frac, rounded)
532 } else {
533 rounded.to_string()
534 }
535 } else {
536 rounded.to_string()
538 };
539
540 if s.contains('e') || s.contains('E') {
542 return FluentValue::from(s);
543 }
544
545 if s.as_bytes().contains(&b'.') {
547 while s.ends_with('0') {
548 s.pop();
549 }
550 if s.ends_with('.') {
551 s.pop();
552 }
553 }
554
555 let mut seen_nonzero = false;
558 let mut current_sig = 0usize;
559
560 for &b in s.as_bytes() {
561 if b.is_ascii_digit() {
562 if !seen_nonzero {
563 if b != b'0' {
564 seen_nonzero = true;
565 current_sig += 1;
566 }
567 } else {
568 current_sig += 1;
569 }
570 }
571 }
572
573 if current_sig == 0 {
574 if min_sig <= 1 {
576 s.clear();
577 s.push('0');
578 } else {
579 s.clear();
580 s.push('0');
581 s.push('.');
582 for _ in 0..(min_sig - 1) {
583 s.push('0');
584 }
585 }
586 } else if current_sig < min_sig {
587 let needed = min_sig - current_sig;
588 if !s.as_bytes().contains(&b'.') {
589 s.push('.');
590 }
591 for _ in 0..needed {
592 s.push('0');
593 }
594 }
595
596 s
597 } else {
598 if let Some(max_frac) = max_fraction_digits {
600 let mut s = format!("{0:.1$}", value, max_frac);
601
602 if s.as_bytes().contains(&b'.') {
604 while s.ends_with('0') {
605 s.pop();
606 }
607 if s.ends_with('.') {
608 s.pop();
609 }
610 }
611
612 if min_fraction_digits > 0 {
614 let dot = s.find('.');
615 let have = match dot {
616 Some(d) => s.len().saturating_sub(d + 1),
617 None => 0,
618 };
619
620 if have < min_fraction_digits {
621 if dot.is_none() {
622 s.push('.');
623 }
624 for _ in 0..(min_fraction_digits - have) {
625 s.push('0');
626 }
627 }
628 }
629
630 s
631 } else {
632 let mut s = value.to_string();
634
635 if s.contains('e') || s.contains('E') {
637 return FluentValue::from(s);
638 }
639
640 if min_fraction_digits > 0 {
642 let dot = s.find('.');
643 let have = match dot {
644 Some(d) => s.len().saturating_sub(d + 1),
645 None => 0,
646 };
647
648 if have < min_fraction_digits {
649 if dot.is_none() {
650 s.push('.');
651 }
652 for _ in 0..(min_fraction_digits - have) {
653 s.push('0');
654 }
655 }
656 }
657
658 s
659 }
660 };
661
662 if text.contains('e') || text.contains('E') {
664 return FluentValue::from(text);
665 }
666
667 let (int_slice, frac_slice) = if let Some(dot) = text.find('.') {
668 (&text[..dot], &text[dot..])
669 } else {
670 (text.as_str(), "")
671 };
672
673 let neg = int_slice.starts_with('-');
675 let digits = if neg { &int_slice[1..] } else { int_slice };
676 let needed = min_integer_digits.saturating_sub(digits.len());
677
678 let mut int_part = String::with_capacity(int_slice.len() + needed);
679 if neg {
680 int_part.push('-');
681 }
682 for _ in 0..needed {
683 int_part.push('0');
684 }
685 int_part.push_str(digits);
686
687 if use_grouping {
689 let neg = int_part.starts_with('-');
690 let start = if neg { 1 } else { 0 };
691 let digits = &int_part[start..];
692
693 if digits.len() > 3 {
694 let mut grouped = String::with_capacity(int_part.len() + (digits.len() / 3));
695 if neg {
696 grouped.push('-');
697 }
698
699 let offset = digits.len() % 3;
700
701 if offset > 0 {
702 grouped.push_str(&digits[..offset]);
703 grouped.push(',');
704 }
705
706 for (i, b) in digits[offset..].bytes().enumerate() {
707 if i > 0 && i % 3 == 0 {
708 grouped.push(',');
709 }
710 grouped.push(b as char);
711 }
712
713 int_part = grouped;
714 }
715 }
716
717 if !frac_slice.is_empty() {
718 int_part.push_str(frac_slice);
719 }
720
721 FluentValue::from(int_part)
722 });
723 }
724
725 #[cfg(feature = "icu")]
726 fn add_custom_formatters(bundle: &mut FluentBundle<FluentResource>, locales: &[LanguageIdentifier]) {
727 use icu::locale::locale;
728
729 let icu_locale = locales.first().and_then(|l| l.to_string().parse().ok()).unwrap_or(locale!("en-US"));
730
731 let _ = bundle.add_function("NUMBER", {
732 let icu_locale = icu_locale.clone();
733 move |args, named_args| {
734 let num_value = match args.first() {
735 Some(FluentValue::Number(n)) => n,
736 Some(other) => return other.clone(),
737 None => return FluentValue::Error,
738 };
739
740 if !num_value.value.is_finite() {
742 return FluentValue::from(num_value.value.to_string());
743 }
744
745 let mut options = DecimalFormatterOptions::default();
746
747 if let Some(FluentValue::String(s)) = named_args.get("useGrouping") {
749 if s.as_ref() == "false" {
750 options.grouping_strategy = Some(GroupingStrategy::Never);
751 } else if s.as_ref() == "always" {
752 options.grouping_strategy = Some(GroupingStrategy::Always);
753 }
754 }
755
756 let mut decimal = match Decimal::try_from_f64(num_value.value, FloatPrecision::RoundTrip) {
757 Ok(d) => d,
758 Err(_) => return FluentValue::from(num_value.value.to_string()),
759 };
760
761 let get_opt = |name: &str| -> Option<i64> {
762 match named_args.get(name) {
763 Some(FluentValue::Number(n)) if n.value.is_finite() => Some(n.value.trunc() as i64),
764 _ => None,
765 }
766 };
767
768 const MAX_FRAC: i64 = 20;
770 const MAX_SIG: i64 = 21;
771 const MAX_MIN_INT: i64 = 308;
772
773 let min_sig_raw = get_opt("minimumSignificantDigits").map(|v| v.clamp(1, MAX_SIG) as i16);
774 let max_sig_raw = get_opt("maximumSignificantDigits").map(|v| v.clamp(1, MAX_SIG) as i16);
775
776 let (min_sig, max_sig) = match (min_sig_raw, max_sig_raw) {
778 (Some(a), Some(b)) => (Some(a.min(b)), Some(b)),
779 other => other,
780 };
781
782 let mut sig_applied = false;
783 let val_abs = num_value.value.abs();
784
785 if let Some(max) = max_sig {
789 let magnitude = if val_abs != 0.0 { val_abs.log10().floor() as i16 } else { 0 };
790 let position = magnitude - (max - 1);
791 decimal.round(position);
792 decimal.trim_end(); sig_applied = true;
794 }
795
796 if let Some(min) = min_sig {
797 let magnitude = if val_abs != 0.0 { val_abs.log10().floor() as i16 } else { 0 };
798 let position = magnitude - (min - 1);
799 decimal.pad_end(position); sig_applied = true;
801 }
802
803 if !sig_applied {
805 let min_frac_raw = get_opt("minimumFractionDigits").map(|v| v.clamp(0, MAX_FRAC) as i16);
806 let max_frac_raw = get_opt("maximumFractionDigits").map(|v| v.clamp(0, MAX_FRAC) as i16);
807
808 let (min_frac, max_frac) = match (min_frac_raw, max_frac_raw) {
810 (Some(a), Some(b)) => (Some(a.min(b)), Some(b)),
811 other => other,
812 };
813
814 if let Some(min_frac) = min_frac {
816 decimal.pad_end(-min_frac);
817 }
818
819 if let Some(max_frac) = max_frac {
823 let limit = -max_frac;
824 if *decimal.magnitude_range().start() < limit {
826 decimal.round(limit);
827 }
828 }
829 }
830
831 if let Some(min_int) = get_opt("minimumIntegerDigits").map(|v| v.clamp(1, MAX_MIN_INT) as i16) {
833 decimal.pad_start(min_int);
834 }
835
836 let formatter: DecimalFormatter = match DecimalFormatter::try_new(icu_locale.clone().into(), options) {
838 Ok(fmt) => fmt,
839 Err(_) => return FluentValue::from(num_value.value.to_string()),
840 };
841
842 FluentValue::from(formatter.format(&decimal).to_string())
843 }
844 });
845
846 let _ = bundle.add_function("DATETIME", {
847 let icu_locale = icu_locale.clone();
848 move |args, named_args| {
849 let fallback_value = match args.first() {
850 Some(v) => v.clone(),
851 None => return FluentValue::Error,
852 };
853
854 let parse_date_length = |val: &FluentValue| -> Option<Length> {
855 if let FluentValue::String(s) = val {
856 match s.as_ref() {
857 "long" => Some(Length::Long),
858 "medium" => Some(Length::Medium),
859 "short" => Some(Length::Short),
860 _ => None,
861 }
862 } else {
863 None
864 }
865 };
866
867 let parse_time_length = |val: &FluentValue| -> Option<Length> {
868 if let FluentValue::String(s) = val {
869 match s.as_ref() {
870 "long" => Some(Length::Long),
871 "medium" => Some(Length::Medium),
872 "short" => Some(Length::Short),
873 _ => None,
874 }
875 } else {
876 None
877 }
878 };
879
880 let date_style = named_args.get("dateStyle").and_then(parse_date_length);
881 let time_style = named_args.get("timeStyle").and_then(parse_time_length);
882
883 let effective_date_style = date_style.or_else(|| if time_style.is_none() { Some(Length::Medium) } else { None });
884
885 enum DateOrTime {
886 Date(icu::calendar::Date<Iso>),
887 Time(icu::datetime::input::Time),
888 DateTime(icu::datetime::input::DateTime<Iso>),
889 }
890
891 let to_icu_date =
892 |d: Date| -> Option<icu::calendar::Date<Iso>> { icu::calendar::Date::try_new_iso(d.year(), u8::from(d.month()), d.day()).ok() };
893
894 let to_icu_time = |t: Time| -> Option<icu::datetime::input::Time> {
895 icu::datetime::input::Time::try_new(t.hour(), t.minute(), t.second(), t.nanosecond()).ok()
896 };
897
898 let to_icu_datetime = |dt: OffsetDateTime| -> Option<icu::datetime::input::DateTime<Iso>> {
899 let d = dt.date();
900 let t = dt.time();
901 let date = to_icu_date(d)?;
902 let time = to_icu_time(t)?;
903 Some(icu::datetime::input::DateTime { date, time })
904 };
905
906 let input: DateOrTime = match args.first() {
907 Some(FluentValue::Custom(custom)) => {
908 if let Some(arg) = custom.as_any().downcast_ref::<LocalizedArg>() {
909 match arg {
910 LocalizedArg::ConstDate(d) => {
911 let date = match to_icu_date(*d) {
912 Some(v) => v,
913 None => return fallback_value,
914 };
915 DateOrTime::Date(date)
916 }
917 LocalizedArg::VarDate(v) => {
918 let date = match v.get().and_then(to_icu_date) {
919 Some(v) => v,
920 None => return fallback_value,
921 };
922 DateOrTime::Date(date)
923 }
924 LocalizedArg::ConstTime(t) => {
925 let time = match to_icu_time(*t) {
926 Some(v) => v,
927 None => return fallback_value,
928 };
929 DateOrTime::Time(time)
930 }
931 LocalizedArg::VarTime(v) => {
932 let time = match v.get().and_then(to_icu_time) {
933 Some(v) => v,
934 None => return fallback_value,
935 };
936 DateOrTime::Time(time)
937 }
938 LocalizedArg::ConstDateTime(dt) => {
939 let dt = match to_icu_datetime(*dt) {
940 Some(v) => v,
941 None => return fallback_value,
942 };
943 DateOrTime::DateTime(dt)
944 }
945 LocalizedArg::VarDateTime(v) => {
946 let dt = match v.get().and_then(to_icu_datetime) {
947 Some(v) => v,
948 None => return fallback_value,
949 };
950 DateOrTime::DateTime(dt)
951 }
952
953 _ => return fallback_value,
955 }
956 } else {
957 return fallback_value;
959 }
960 }
961 Some(FluentValue::String(s)) => match icu::datetime::input::DateTime::<Iso>::from_str(s.as_ref()) {
962 Ok(dt) => DateOrTime::DateTime(dt),
963 Err(_) => return FluentValue::from(s.clone()),
964 },
965 Some(v) => return v.clone(),
966 None => return FluentValue::Error,
967 };
968
969 let formatted = match (effective_date_style, time_style) {
971 (Some(date_len), Some(time_len)) => {
972 let dt = match &input {
973 DateOrTime::DateTime(dt) => dt,
974 _ => return fallback_value,
975 };
976
977 let ymd = fieldsets::YMD::for_length(date_len);
978 let ymdt = match time_len {
979 Length::Short => ymd.with_time_hm(),
980 Length::Medium | Length::Long => ymd.with_time_hms(),
981 _ => ymd.with_time_hms(),
982 };
983
984 match DateTimeFormatter::<DateAndTimeFieldSet>::try_new(icu_locale.clone().into(), DateAndTimeFieldSet::YMDT(ymdt)) {
985 Ok(fmt) => fmt.format(dt).to_string(),
986 Err(_) => return fallback_value,
987 }
988 }
989 (Some(date_len), None) => {
990 let ymd = fieldsets::YMD::for_length(date_len);
991
992 let fmt = match DateTimeFormatter::<DateFieldSet>::try_new(icu_locale.clone().into(), DateFieldSet::YMD(ymd)) {
993 Ok(fmt) => fmt,
994 Err(_) => return fallback_value,
995 };
996
997 match &input {
998 DateOrTime::Date(date) => fmt.format(date).to_string(),
999 DateOrTime::DateTime(dt) => fmt.format(dt).to_string(),
1000 DateOrTime::Time(_) => return fallback_value,
1001 }
1002 }
1003 (None, Some(time_len)) => {
1004 let tf = match time_len {
1005 Length::Short => fieldsets::T::hm().with_length(time_len),
1006 Length::Medium | Length::Long => fieldsets::T::hms().with_length(time_len),
1007 _ => fieldsets::T::hms().with_length(time_len),
1008 };
1009
1010 let fmt = match DateTimeFormatter::<TimeFieldSet>::try_new(icu_locale.clone().into(), TimeFieldSet::T(tf)) {
1011 Ok(fmt) => fmt,
1012 Err(_) => return fallback_value,
1013 };
1014
1015 match &input {
1016 DateOrTime::Time(time) => fmt.format(time).to_string(),
1017 DateOrTime::DateTime(dt) => fmt.format(dt).to_string(),
1018 DateOrTime::Date(_) => return fallback_value,
1019 }
1020 }
1021 (None, None) => return fallback_value,
1022 };
1023
1024 FluentValue::from(formatted)
1025 }
1026 });
1027 }
1028}
1029
1030#[derive(Clone, Debug, PartialEq)]
1032#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1033pub enum LocalizedArg {
1034 ConstString(Cow<'static, str>),
1035 ConstNumber(f64),
1036 VarString(WeakVar<String>),
1037 VarNumber(WeakVar<f64>),
1038 #[cfg(feature = "icu")]
1039 ConstTime(Time),
1040 #[cfg(feature = "icu")]
1041 VarTime(WeakVar<Time>),
1042 #[cfg(feature = "icu")]
1043 ConstDate(Date),
1044 #[cfg(feature = "icu")]
1045 VarDate(WeakVar<Date>),
1046 #[cfg(feature = "icu")]
1047 ConstDateTime(OffsetDateTime),
1048 #[cfg(feature = "icu")]
1049 VarDateTime(WeakVar<OffsetDateTime>),
1050}
1051
1052#[cfg(feature = "icu")]
1053impl FluentType for LocalizedArg {
1054 fn duplicate(&self) -> Box<dyn FluentType + Send> {
1055 Box::new(self.clone())
1056 }
1057
1058 fn as_string(&self, _intls: &intl_memoizer::IntlLangMemoizer) -> Cow<'static, str> {
1059 match self {
1062 LocalizedArg::ConstString(s) => s.clone(),
1063 LocalizedArg::ConstNumber(n) => Cow::Owned(n.to_string()),
1064 LocalizedArg::VarString(v) => v.get().map(|val| Cow::Owned(val.clone())).unwrap_or_default(),
1065 LocalizedArg::VarNumber(v) => v.get().map(|val| Cow::Owned(val.to_string())).unwrap_or_default(),
1066 LocalizedArg::ConstTime(t) => Cow::Owned(format!("{}", t)),
1067 LocalizedArg::VarTime(v) => v.get().map(|t| Cow::Owned(format!("{}", t))).unwrap_or_default(),
1068 LocalizedArg::ConstDate(d) => Cow::Owned(format!("{}", d)),
1069 LocalizedArg::VarDate(v) => v.get().map(|d| Cow::Owned(format!("{}", d))).unwrap_or_default(),
1070 LocalizedArg::ConstDateTime(dt) => Cow::Owned(format!("{}", dt)),
1071 LocalizedArg::VarDateTime(v) => v.get().map(|dt| Cow::Owned(format!("{}", dt))).unwrap_or_default(),
1072 }
1073 }
1074
1075 fn as_string_threadsafe(&self, _intls: &intl_memoizer::concurrent::IntlLangMemoizer) -> Cow<'static, str> {
1076 match self {
1078 LocalizedArg::ConstString(s) => s.clone(),
1079 LocalizedArg::ConstNumber(n) => Cow::Owned(n.to_string()),
1080 LocalizedArg::VarString(v) => v.get().map(|val| Cow::Owned(val.clone())).unwrap_or_default(),
1081 LocalizedArg::VarNumber(v) => v.get().map(|val| Cow::Owned(val.to_string())).unwrap_or_default(),
1082 LocalizedArg::ConstTime(t) => Cow::Owned(format!("{}", t)),
1083 LocalizedArg::VarTime(v) => v.get().map(|t| Cow::Owned(format!("{}", t))).unwrap_or_default(),
1084 LocalizedArg::ConstDate(d) => Cow::Owned(format!("{}", d)),
1085 LocalizedArg::VarDate(v) => v.get().map(|d| Cow::Owned(format!("{}", d))).unwrap_or_default(),
1086 LocalizedArg::ConstDateTime(dt) => Cow::Owned(format!("{}", dt)),
1087 LocalizedArg::VarDateTime(v) => v.get().map(|dt| Cow::Owned(format!("{}", dt))).unwrap_or_default(),
1088 }
1089 }
1090}
1091
1092impl LocalizedArg {
1093 fn to_fluent<'a>(&'a self) -> Option<FluentValue<'a>> {
1094 match self {
1095 LocalizedArg::ConstString(s) => Some(FluentValue::String(Cow::Borrowed(s.as_ref()))),
1096 LocalizedArg::ConstNumber(n) => Some((*n).into()),
1097 LocalizedArg::VarString(v) => Some(v.get()?.into()),
1098 LocalizedArg::VarNumber(v) => Some(v.get()?.into()),
1099 #[cfg(feature = "icu")]
1100 LocalizedArg::ConstTime(_)
1101 | LocalizedArg::VarTime(_)
1102 | LocalizedArg::ConstDate(_)
1103 | LocalizedArg::VarDate(_)
1104 | LocalizedArg::ConstDateTime(_)
1105 | LocalizedArg::VarDateTime(_) => {
1106 match self {
1107 LocalizedArg::VarTime(v) if !v.is_alive() => return None,
1108 LocalizedArg::VarDate(v) if !v.is_alive() => return None,
1109 LocalizedArg::VarDateTime(v) if !v.is_alive() => return None,
1110 _ => {}
1111 }
1112 Some(FluentValue::Custom(Box::new(self.clone())))
1113 }
1114 }
1115 }
1116
1117 fn get_key(&self) -> Option<VarKey> {
1118 match self {
1119 LocalizedArg::VarString(v) => Some(v.get_key()),
1120 LocalizedArg::VarNumber(v) => Some(v.get_key()),
1121 #[cfg(feature = "icu")]
1122 LocalizedArg::VarTime(v) => Some(v.get_key()),
1123 #[cfg(feature = "icu")]
1124 LocalizedArg::VarDate(v) => Some(v.get_key()),
1125 #[cfg(feature = "icu")]
1126 LocalizedArg::VarDateTime(v) => Some(v.get_key()),
1127 _ => None,
1128 }
1129 }
1130
1131 fn get_version(&self) -> Option<u64> {
1132 match self {
1133 LocalizedArg::VarString(v) => v.get_version(),
1134 LocalizedArg::VarNumber(v) => v.get_version(),
1135 #[cfg(feature = "icu")]
1136 LocalizedArg::VarTime(v) => v.get_version(),
1137 #[cfg(feature = "icu")]
1138 LocalizedArg::VarDate(v) => v.get_version(),
1139 #[cfg(feature = "icu")]
1140 LocalizedArg::VarDateTime(v) => v.get_version(),
1141 _ => Some(0),
1142 }
1143 }
1144}
1145
1146impl From<&'static str> for LocalizedArg {
1147 fn from(s: &'static str) -> Self {
1148 LocalizedArg::ConstString(Cow::Borrowed(s))
1149 }
1150}
1151
1152impl From<String> for LocalizedArg {
1153 fn from(s: String) -> Self {
1154 LocalizedArg::ConstString(Cow::Owned(s))
1155 }
1156}
1157
1158impl From<Cow<'static, str>> for LocalizedArg {
1159 fn from(c: Cow<'static, str>) -> Self {
1160 LocalizedArg::ConstString(c)
1161 }
1162}
1163
1164impl From<f64> for LocalizedArg {
1165 fn from(n: f64) -> Self {
1166 LocalizedArg::ConstNumber(n)
1167 }
1168}
1169
1170impl From<WeakVar<String>> for LocalizedArg {
1171 fn from(v: WeakVar<String>) -> Self {
1172 LocalizedArg::VarString(v)
1173 }
1174}
1175
1176impl From<WeakVar<f64>> for LocalizedArg {
1177 fn from(v: WeakVar<f64>) -> Self {
1178 LocalizedArg::VarNumber(v)
1179 }
1180}
1181
1182#[cfg(feature = "icu")]
1183impl From<Time> for LocalizedArg {
1184 fn from(t: Time) -> Self {
1185 LocalizedArg::ConstTime(t)
1186 }
1187}
1188
1189#[cfg(feature = "icu")]
1190impl From<Date> for LocalizedArg {
1191 fn from(d: Date) -> Self {
1192 LocalizedArg::ConstDate(d)
1193 }
1194}
1195
1196#[cfg(feature = "icu")]
1197impl From<OffsetDateTime> for LocalizedArg {
1198 fn from(dt: OffsetDateTime) -> Self {
1199 LocalizedArg::ConstDateTime(dt)
1200 }
1201}
1202
1203#[cfg(feature = "icu")]
1204impl From<WeakVar<Time>> for LocalizedArg {
1205 fn from(v: WeakVar<Time>) -> Self {
1206 LocalizedArg::VarTime(v)
1207 }
1208}
1209
1210#[cfg(feature = "icu")]
1211impl From<WeakVar<Date>> for LocalizedArg {
1212 fn from(v: WeakVar<Date>) -> Self {
1213 LocalizedArg::VarDate(v)
1214 }
1215}
1216
1217#[cfg(feature = "icu")]
1218impl From<WeakVar<OffsetDateTime>> for LocalizedArg {
1219 fn from(v: WeakVar<OffsetDateTime>) -> Self {
1220 LocalizedArg::VarDateTime(v)
1221 }
1222}
1223
1224pub struct LocalizedStringBuilder {
1226 key: &'static str,
1227 placeholder: Option<&'static str>,
1228 args: Vec<(Cow<'static, str>, LocalizedArg)>,
1229}
1230
1231impl LocalizedStringBuilder {
1232 pub fn new(key: &'static str) -> Self {
1234 Self {
1235 key,
1236 placeholder: None,
1237 args: Vec::new(),
1238 }
1239 }
1240
1241 pub fn placeholder(mut self, text: &'static str) -> Self {
1243 self.placeholder = Some(text);
1244 self
1245 }
1246
1247 pub fn arg(mut self, key: &'static str, value: impl Into<LocalizedArg>) -> Self {
1250 self.args.push((Cow::Borrowed(key), value.into()));
1251 self
1252 }
1253
1254 pub fn build(self) -> LocalizedString {
1256 #[allow(clippy::mutable_key_type)]
1260 let mut dependencies = DependencyMap::default();
1261 for (_, arg) in &self.args {
1262 if let (Some(key), Some(version)) = (arg.get_key(), arg.get_version()) {
1263 dependencies.record(key, version);
1264 }
1265 }
1266
1267 LocalizedString {
1268 inner: Arc::new(RwLock::new(LocalizedStringInner {
1269 last_loaded: None,
1270 key: Cow::Borrowed(self.key),
1271 placeholder: self.placeholder.map(Cow::Borrowed),
1272 args: self.args,
1273 last_locale: None,
1274 resolved_string: None,
1275 })),
1276 }
1277 }
1278}
1279
1280struct LocalizedStringInner {
1281 last_loaded: Option<Instant>,
1282 key: Cow<'static, str>,
1283 placeholder: Option<Cow<'static, str>>,
1284 args: Vec<(Cow<'static, str>, LocalizedArg)>,
1285 last_locale: Option<LanguageIdentifier>,
1286 resolved_string: Option<String>,
1287}
1288
1289impl fmt::Debug for LocalizedStringInner {
1290 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1291 f.debug_struct("LocalizedString")
1292 .field("key", &self.key)
1293 .field("placeholder", &self.placeholder)
1294 .field("args", &self.args.len())
1295 .field("last_locale", &self.last_locale)
1296 .finish()
1297 }
1298}
1299
1300#[derive(Clone)]
1306pub struct LocalizedString {
1307 inner: Arc<RwLock<LocalizedStringInner>>,
1308}
1309
1310impl fmt::Debug for LocalizedString {
1311 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1312 self.inner.read().fmt(f)
1313 }
1314}
1315
1316impl LocalizedString {
1317 pub fn resolve(&self, translation_map: &TranslationMap) -> MappedRwLockReadGuard<'_, str> {
1319 let map_guard = translation_map.inner.read();
1320 let current_locale = &map_guard.current_locale;
1321
1322 let mut write_guard = self.inner.write();
1323 write_guard.resolved_string = None;
1324 write_guard.last_loaded = Some(map_guard.last_loaded);
1325 write_guard.last_locale = Some(current_locale.clone());
1326
1327 if let Some(file) = map_guard.translations.get(current_locale)
1329 && let Some(msg) = file.bundle.get_message(write_guard.key.as_ref())
1330 && let Some(value) = msg.value()
1331 {
1332 let mut args = FluentArgs::new();
1333 for (key, arg) in &write_guard.args {
1334 if let Some(fluent) = arg.to_fluent() {
1335 args.set(key.as_ref(), fluent);
1336 }
1337 }
1338
1339 let mut errors = Vec::new();
1340 let resolved = file.bundle.format_pattern(value, Some(&args), &mut errors).into_owned();
1341
1342 if errors.is_empty() {
1343 write_guard.resolved_string = Some(resolved);
1344 } else {
1345 for error in errors {
1346 error!("{error}");
1347 }
1348 write_guard.resolved_string = None;
1350 }
1351 }
1352
1353 let read_guard = RwLockWriteGuard::downgrade(write_guard);
1354 RwLockReadGuard::map(read_guard, |inner| {
1355 if let Some(text) = &inner.resolved_string {
1356 text.as_str()
1357 } else {
1358 inner.placeholder.as_deref().unwrap_or(inner.key.as_ref())
1359 }
1360 })
1361 }
1362}
1363
1364#[cfg(feature = "serde")]
1365mod serde_impl {
1366 use super::*;
1367
1368 use serde::{Deserialize, Deserializer, Serialize, Serializer};
1369
1370 #[derive(Serialize, Deserialize)]
1371 struct LocalizedStringSerde {
1372 key: Cow<'static, str>,
1373 placeholder: Option<Cow<'static, str>>,
1374 args: Vec<(Cow<'static, str>, LocalizedArg)>,
1375 }
1376
1377 impl Serialize for LocalizedString {
1378 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1379 where
1380 S: Serializer,
1381 {
1382 let inner = self.inner.read();
1383
1384 let ser = LocalizedStringSerde {
1385 key: inner.key.clone(),
1386 placeholder: inner.placeholder.clone(),
1387 args: inner.args.clone(),
1388 };
1389
1390 ser.serialize(serializer)
1391 }
1392 }
1393
1394 impl<'de> Deserialize<'de> for LocalizedString {
1395 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1396 where
1397 D: Deserializer<'de>,
1398 {
1399 let de = LocalizedStringSerde::deserialize(deserializer)?;
1400
1401 #[allow(clippy::mutable_key_type)]
1405 let mut dependencies = DependencyMap::default();
1406 for (_, arg) in &de.args {
1407 if let (Some(key), Some(version)) = (arg.get_key(), arg.get_version()) {
1408 dependencies.record(key, version);
1409 }
1410 }
1411
1412 Ok(LocalizedString {
1413 inner: Arc::new(RwLock::new(LocalizedStringInner {
1414 last_loaded: None,
1415 key: de.key,
1416 placeholder: de.placeholder,
1417 args: de.args,
1418 last_locale: None,
1419 resolved_string: None,
1420 })),
1421 })
1422 }
1423 }
1424}