1use std::{borrow::Cow, collections::HashMap, fmt, mem, ops, path::PathBuf, sync::Arc};
2
3use fluent::types::FluentNumber;
4use once_cell::sync::Lazy;
5use semver::Version;
6use zng_ext_fs_watcher::WatcherReadStatus;
7use zng_layout::context::LayoutDirection;
8use zng_txt::{ToTxt, Txt};
9use zng_var::{ArcEq, ContextVar, IntoVar, Var, VarValue, const_var, context_var, impl_from_and_into_var};
10
11use crate::{L10N, lang, service::L10N_SV};
12
13#[derive(Clone, Debug)]
15pub struct LangResources(pub Vec<LangResource>);
16impl ops::Deref for LangResources {
17 type Target = Vec<LangResource>;
18
19 fn deref(&self) -> &Self::Target {
20 &self.0
21 }
22}
23impl ops::DerefMut for LangResources {
24 fn deref_mut(&mut self) -> &mut Self::Target {
25 &mut self.0
26 }
27}
28impl LangResources {
29 pub async fn wait(&self) {
31 for res in &self.0 {
32 res.wait().await;
33 }
34 }
35
36 pub fn perm(self) {
38 for res in self.0 {
39 res.perm()
40 }
41 }
42}
43
44#[derive(Clone)]
46#[must_use = "resource can unload if dropped"]
47pub struct LangResource {
48 pub(super) res: Var<Option<ArcEq<fluent::FluentResource>>>,
49 pub(super) status: Var<LangResourceStatus>,
50}
51
52impl fmt::Debug for LangResource {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 f.debug_struct("LangResource")
55 .field("status", &self.status.get())
56 .finish_non_exhaustive()
57 }
58}
59impl LangResource {
60 pub fn resource(&self) -> &Var<Option<ArcEq<fluent::FluentResource>>> {
62 &self.res
63 }
64
65 pub fn status(&self) -> &Var<LangResourceStatus> {
67 &self.status
68 }
69
70 pub fn perm(self) {
72 L10N_SV.write().push_perm_resource(self);
73 }
74
75 pub async fn wait(&self) {
77 while matches!(self.status.get(), LangResourceStatus::Loading) {
78 self.status.wait_update().await;
79 }
80 }
81}
82
83#[derive(Clone, Debug)]
85pub enum LangResourceStatus {
86 NotAvailable,
90 Loading,
92 Loaded,
94 Errors(StatusError),
100}
101impl fmt::Display for LangResourceStatus {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 match self {
104 LangResourceStatus::NotAvailable => write!(f, "not available"),
105 LangResourceStatus::Loading => write!(f, "loading…"),
106 LangResourceStatus::Loaded => write!(f, "loaded"),
107 LangResourceStatus::Errors(e) => {
108 writeln!(f, "errors:")?;
109 for e in e {
110 writeln!(f, " {e}")?;
111 }
112 Ok(())
113 }
114 }
115 }
116}
117impl PartialEq for LangResourceStatus {
118 fn eq(&self, other: &Self) -> bool {
119 match (self, other) {
120 (Self::Errors(a), Self::Errors(b)) => a.is_empty() && b.is_empty(),
121 _ => core::mem::discriminant(self) == core::mem::discriminant(other),
122 }
123 }
124}
125impl Eq for LangResourceStatus {}
126impl WatcherReadStatus<StatusError> for LangResourceStatus {
127 fn idle() -> Self {
128 Self::Loaded
129 }
130
131 fn reading() -> Self {
132 Self::Loading
133 }
134
135 fn read_error(e: StatusError) -> Self {
136 Self::Errors(e)
137 }
138}
139impl WatcherReadStatus<LangResourceStatus> for LangResourceStatus {
140 fn idle() -> Self {
141 Self::Loaded
142 }
143
144 fn reading() -> Self {
145 Self::Loading
146 }
147
148 fn read_error(e: LangResourceStatus) -> Self {
149 e
150 }
151}
152
153type StatusError = Vec<Arc<dyn std::error::Error + Send + Sync>>;
154
155pub struct L10nMessageBuilder {
161 pub(super) file: LangFilePath,
162 pub(super) id: Txt,
163 pub(super) attribute: Txt,
164 pub(super) fallback: Txt,
165 pub(super) args: Vec<(Txt, Var<L10nArgument>)>,
166}
167impl L10nMessageBuilder {
168 pub fn arg(mut self, name: Txt, value: impl IntoVar<L10nArgument>) -> Self {
170 self.args.push((name, value.into_var()));
171 self
172 }
173 #[doc(hidden)]
174 pub fn l10n_arg(self, name: &'static str, value: Var<L10nArgument>) -> Self {
175 self.arg(Txt::from_static(name), value)
176 }
177
178 pub fn build_for(self, lang: impl Into<Langs>) -> Var<Txt> {
180 L10N_SV
181 .write()
182 .localized_message(lang.into(), self.file, self.id, self.attribute, self.fallback, self.args)
183 }
184
185 pub fn build(self) -> Var<Txt> {
187 let Self {
188 file,
189 id,
190 attribute,
191 fallback,
192 args,
193 } = self;
194 LANG_VAR.flat_map(move |l| {
195 L10N_SV.write().localized_message(
196 l.clone(),
197 file.clone(),
198 id.clone(),
199 attribute.clone(),
200 fallback.clone(),
201 args.clone(),
202 )
203 })
204 }
205}
206
207#[derive(Clone, Debug, PartialEq)]
211pub enum L10nArgument {
212 Txt(Txt),
214 Number(FluentNumber),
216}
217impl_from_and_into_var! {
218 fn from(txt: Txt) -> L10nArgument {
219 L10nArgument::Txt(txt)
220 }
221 fn from(txt: &'static str) -> L10nArgument {
222 L10nArgument::Txt(Txt::from_static(txt))
223 }
224 fn from(txt: String) -> L10nArgument {
225 L10nArgument::Txt(Txt::from(txt))
226 }
227 fn from(t: char) -> L10nArgument {
228 L10nArgument::Txt(Txt::from_char(t))
229 }
230 fn from(number: FluentNumber) -> L10nArgument {
231 L10nArgument::Number(number)
232 }
233 fn from(b: bool) -> L10nArgument {
234 b.to_txt().into()
235 }
236}
237macro_rules! impl_from_and_into_var_number {
238 ($($literal:tt),+) => {
239 impl_from_and_into_var! {
240 $(
241 fn from(number: $literal) -> L10nArgument {
242 FluentNumber::from(number).into()
243 }
244 )+
245 }
246 }
247}
248impl_from_and_into_var_number! { u8, i8, u16, i16, u32, i32, u64, i64, u128, i128, usize, isize, f32, f64 }
249impl L10nArgument {
250 pub fn fluent_value(&self) -> fluent::FluentValue<'_> {
252 match self {
253 L10nArgument::Txt(t) => fluent::FluentValue::String(Cow::Borrowed(t.as_str())),
254 L10nArgument::Number(n) => fluent::FluentValue::Number(n.clone()),
255 }
256 }
257 pub fn to_fluent_value(&self) -> fluent::FluentValue<'static> {
259 match self {
260 L10nArgument::Txt(t) => fluent::FluentValue::String(Cow::Owned(t.to_string())),
261 L10nArgument::Number(n) => fluent::FluentValue::Number(n.clone()),
262 }
263 }
264}
265
266#[doc(hidden)]
267pub struct L10nSpecialize<T>(pub Option<T>);
268#[doc(hidden)]
269pub trait IntoL10nVar {
270 fn to_l10n_var(&mut self) -> Var<L10nArgument>;
271}
272impl<T: fmt::Display> IntoL10nVar for L10nSpecialize<T> {
273 fn to_l10n_var(&mut self) -> Var<L10nArgument> {
274 const_var(self.0.take().unwrap().to_txt().into())
275 }
276}
277impl<T: Into<L10nArgument>> IntoL10nVar for &mut L10nSpecialize<T> {
278 fn to_l10n_var(&mut self) -> Var<L10nArgument> {
279 const_var(self.0.take().unwrap().into())
280 }
281}
282
283impl<T: VarValue + Into<L10nArgument>> IntoL10nVar for &mut &mut L10nSpecialize<ContextVar<T>> {
284 fn to_l10n_var(&mut self) -> Var<L10nArgument> {
285 self.0.take().unwrap().into_var().map_into()
286 }
287}
288impl<T: VarValue + Into<L10nArgument>> IntoL10nVar for &mut &mut &mut L10nSpecialize<Var<T>> {
289 fn to_l10n_var(&mut self) -> Var<L10nArgument> {
290 self.0.take().unwrap().map_into()
291 }
292}
293impl IntoL10nVar for &mut &mut &mut &mut L10nSpecialize<Var<L10nArgument>> {
294 fn to_l10n_var(&mut self) -> Var<L10nArgument> {
295 self.0.take().unwrap()
296 }
297}
298
299context_var! {
300 pub static LANG_VAR: Langs = L10N.app_lang();
306}
307
308#[derive(PartialEq, Eq, Hash, Clone, Default, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
317#[serde(transparent)]
318pub struct Lang(pub unic_langid::LanguageIdentifier);
319impl Lang {
320 pub fn direction(&self) -> LayoutDirection {
322 if self == &lang!("ms") {
323 return LayoutDirection::LTR;
325 }
326 if self == &lang!("uz") {
327 return LayoutDirection::LTR;
329 }
330 crate::from_unic_char_direction(self.0.character_direction())
331 }
332
333 pub fn matches(&self, other: &Self, self_as_range: bool, other_as_range: bool) -> bool {
371 if !self.0.language.matches(other.0.language, self_as_range, other_as_range) {
372 return false;
373 }
374
375 fn tag_matches<P: PartialEq>(tag1: &Option<P>, tag2: &Option<P>, as_range1: bool, as_range2: bool) -> bool {
376 (as_range1 && tag1.is_none()) || (as_range2 && tag2.is_none()) || tag1 == tag2
377 }
378 if !tag_matches(&self.0.script, &other.0.script, self_as_range, other_as_range)
379 || !tag_matches(&self.region, &other.region, self_as_range, other_as_range)
380 {
381 return false;
382 }
383
384 (self_as_range && self.variants_no_machine().next().is_none())
385 || (other_as_range && other.variants_no_machine().next().is_none())
386 || self.variants_no_machine().eq(other.variants_no_machine())
387 }
388 fn variants_no_machine(&self) -> impl Iterator<Item = &unic_langid::subtags::Variant> {
389 self.0.variants().filter(|v| v.as_str() != "machine")
390 }
391
392 pub fn is_machine_translation(&self) -> bool {
398 self.0.variants().any(|v| v == "machine")
399 }
400
401 #[cfg(feature = "lang_autonym")]
410 pub fn autonym(&self) -> Option<LangAutonym> {
411 let lang = self.0.language.as_str();
412 let script = self.0.script.as_ref().map(|s| s.as_str()).unwrap_or("");
413 let region = self.0.region.as_ref().map(|r| r.as_str()).unwrap_or("");
414 let (name, region) = match (lang, script, region) {
415 ("af", "", "") => ("Afrikaans", ""),
418 ("am", "", "") => ("አማርኛ", ""),
419 ("ar", "", "") => ("العربية", ""),
420 ("as", "", "") => ("অসমীয়া", ""),
421 ("az", "", "") => ("Azərbaycan", ""),
422 ("be", "", "") => ("Беларуская", ""),
423 ("bg", "", "") => ("Български", ""),
424 ("bn", "", "") => ("বাংলা", ""),
425 ("bs", "", "") => ("Bosanski", ""),
426 ("ca", "", "") => ("Català", ""),
427 ("cs", "", "") => ("Čeština", ""),
428 ("cy", "", "") => ("Cymraeg", ""),
429 ("da", "", "") => ("Dansk", ""),
430 ("de", "", "") => ("Deutsch", ""),
431 ("el", "", "") => ("Ελληνικά", ""),
432 ("en", "", "") => ("English", ""),
433 ("en", "", "GB") => ("English", "United Kingdom"),
434 ("en", "", "US") => ("English", "United States"),
435 ("es", "", "") => ("Español", ""),
436 ("es", "", "419") => ("Español", "Latinoamérica"),
437 ("es", "", "ES") => ("Español", "España"),
438 ("et", "", "") => ("Eesti", ""),
439 ("eu", "", "") => ("Euskara", ""),
440 ("fa", "", "") => ("فارسی", ""),
441 ("fi", "", "") => ("Suomi", ""),
442 ("fil", "", "") => ("Filipino", ""),
443 ("fr", "", "") => ("Français", ""),
444 ("fr", "", "FR") => ("Français", "France"),
445 ("fr", "", "CA") => ("Français", "Canada"),
446 ("ga", "", "") => ("Gaeilge", ""),
447 ("gd", "", "") => ("Gàidhlig", ""),
448 ("gl", "", "") => ("Galego", ""),
449 ("gu", "", "") => ("ગુજરાતી", ""),
450 ("he", "", "") => ("עברית", ""),
451 ("hi", "", "") => ("हिन्दी", ""),
452 ("hr", "", "") => ("Hrvatski", ""),
453 ("hu", "", "") => ("Magyar", ""),
454 ("hy", "", "") => ("Հայերեն", ""),
455 ("id", "", "") => ("Indonesia", ""),
456 ("is", "", "") => ("Íslenska", ""),
457 ("it", "", "") => ("Italiano", ""),
458 ("ja", "", "") => ("日本語", ""),
459 ("ka", "", "") => ("ქართული", ""),
460 ("kk", "", "") => ("Қазақ тілі", ""),
461 ("km", "", "") => ("ខ្មែរ", ""),
462 ("kn", "", "") => ("ಕನ್ನಡ", ""),
463 ("ko", "", "") => ("한국어", ""),
464 ("ky", "", "") => ("Кыргызча", ""),
465 ("lo", "", "") => ("ລາວ", ""),
466 ("lt", "", "") => ("Lietuvių", ""),
467 ("lv", "", "") => ("Latviešu", ""),
468 ("mk", "", "") => ("Македонски", ""),
469 ("ml", "", "") => ("മലയാളം", ""),
470 ("mn", "", "") => ("Монгол", ""),
471 ("mr", "", "") => ("मराठी", ""),
472 ("ms", "", "") => ("Melayu", ""),
473 ("my", "", "") => ("မြန်မာ", ""),
474 ("nb", "", "") => ("Norsk bokmål", ""),
475 ("ne", "", "") => ("नेपाली", ""),
476 ("nl", "", "") => ("Nederlands", ""),
477 ("nn", "", "") => ("Norsk nynorsk", ""),
478 ("or", "", "") => ("ଓଡ଼ିଆ", ""),
479 ("pa", "", "") => ("ਪੰਜਾਬੀ", ""),
480 ("pl", "", "") => ("Polski", ""),
481 ("ps", "", "") => ("پښتو", ""),
482 ("pt", "", "") => ("Português", ""),
483 ("pt", "", "BR") => ("Português", "Brasil"),
484 ("pt", "", "PT") => ("Português", "Portugal"),
485 ("ro", "", "") => ("Română", ""),
486 ("ru", "", "") => ("Русский", ""),
487 ("si", "", "") => ("සිංහල", ""),
488 ("sk", "", "") => ("Slovenčina", ""),
489 ("sl", "", "") => ("Slovenščina", ""),
490 ("sq", "", "") => ("Shqip", ""),
491 ("sr", "", "") => ("Српски", ""),
492 ("sr", "Latn", "") => ("Srpski", ""),
493 ("sv", "", "") => ("Svenska", ""),
494 ("sw", "", "") => ("Kiswahili", ""),
495 ("ta", "", "") => ("தமிழ்", ""),
496 ("te", "", "") => ("తెలుగు", ""),
497 ("th", "", "") => ("ไทย", ""),
498 ("tr", "", "") => ("Türkçe", ""),
499 ("uk", "", "") => ("Українська", ""),
500 ("ur", "", "") => ("اردو", ""),
501 ("uz", "", "") => ("O‘zbek", ""),
502 ("vi", "", "") => ("Tiếng việt", ""),
503 ("zh", "", "") => ("中文", ""),
504 ("zh", "Hans", "") => ("简体中文", ""),
505 ("zh", "Hant", "") => ("繁體中文", ""),
506 ("zh", "", "TW") => ("繁體中文", "台灣"),
507 ("zu", "", "") => ("Isizulu", ""),
508 ("pseudo", "", "") => ("Ƥşeuḓo", ""),
509 ("pseudo", "Mirr", "") => ("Ԁsǝnpo-Wıɹɹoɹǝp", ""),
510 ("pseudo", "Wide", "") => ("Ƥşeeuuḓoo-Ẇiḓee", ""),
511 _ => {
512 if self.0.region.is_some() {
513 let q = Lang(unic_langid::LanguageIdentifier::from_parts(
514 self.0.language,
515 self.0.script,
516 None,
517 &[],
518 ));
519 return q.autonym();
520 } else {
521 return None;
522 }
523 }
524 };
525 Some(LangAutonym {
526 language: name,
527 region: if region.is_empty() { None } else { Some(region) },
528 })
529 }
530
531 pub fn cmp_display(&self, other: &Self) -> std::cmp::Ordering {
535 let (name, region) = self.name_region_str();
536 let (other_name, other_region) = other.name_region_str();
537 name.cmp(other_name).then_with(|| region.cmp(&other_region))
538 }
539
540 fn name_region_str(&self) -> (&str, Option<&str>) {
541 #[cfg(feature = "lang_autonym")]
542 if let Some(a) = self.autonym() {
543 let region = a.region.or_else(|| self.0.region.as_ref().map(|r| r.as_str()));
544 return (a.language, region);
545 }
546 (self.0.language.as_str(), self.0.region.as_ref().map(|r| r.as_str()))
547 }
548}
549impl ops::Deref for Lang {
550 type Target = unic_langid::LanguageIdentifier;
551
552 fn deref(&self) -> &Self::Target {
553 &self.0
554 }
555}
556impl fmt::Debug for Lang {
557 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
558 write!(f, "{}", self.0)
559 }
560}
561impl fmt::Display for Lang {
565 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
566 let (name, region) = self.name_region_str();
567 if f.alternate()
568 && let Some(r) = region
569 {
570 write!(f, "{name} ({r})")
571 } else {
572 write!(f, "{name}")
573 }
574 }
575}
576impl std::str::FromStr for Lang {
577 type Err = unic_langid::LanguageIdentifierError;
578
579 fn from_str(s: &str) -> Result<Self, Self::Err> {
580 let s = s.trim();
581 if s.is_empty() {
582 return Ok(lang!(und));
583 }
584 unic_langid::LanguageIdentifier::from_str(s).map(Lang)
585 }
586}
587
588#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
590#[cfg(feature = "lang_autonym")]
591pub struct LangAutonym {
592 pub language: &'static str,
594 pub region: Option<&'static str>,
598}
599#[cfg(feature = "lang_autonym")]
600impl fmt::Debug for LangAutonym {
601 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
602 if f.alternate() {
603 f.debug_struct("LangAutonym")
604 .field("language", &self.language)
605 .field("region", &self.region)
606 .finish()
607 } else {
608 write!(f, "{self}")
609 }
610 }
611}
612#[cfg(feature = "lang_autonym")]
614impl fmt::Display for LangAutonym {
615 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
616 write!(f, "{}", self.language)?;
617 if let Some(r) = self.region
618 && !f.alternate()
619 {
620 write!(f, " ({r})")?;
621 }
622 Ok(())
623 }
624}
625
626#[derive(Clone, PartialEq, Eq, Default, Hash, serde::Serialize, serde::Deserialize)]
628#[serde(transparent)]
629pub struct Langs(pub Vec<Lang>);
630impl fmt::Debug for Langs {
631 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
632 struct DisplayLangs<'a>(&'a [Lang]);
633 impl fmt::Debug for DisplayLangs<'_> {
634 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
635 f.debug_list().entries(self.0.iter()).finish()
636 }
637 }
638 if f.alternate() {
639 f.debug_tuple("Langs").field(&DisplayLangs(&self.0)).finish()
640 } else {
641 fmt::Debug::fmt(&DisplayLangs(&self.0), f)
642 }
643 }
644}
645impl Langs {
646 pub fn best(&self) -> &Lang {
648 static NONE: Lazy<Lang> = Lazy::new(|| lang!(und));
649 self.first().unwrap_or(&NONE)
650 }
651}
652impl ops::Deref for Langs {
653 type Target = Vec<Lang>;
654
655 fn deref(&self) -> &Self::Target {
656 &self.0
657 }
658}
659impl ops::DerefMut for Langs {
660 fn deref_mut(&mut self) -> &mut Self::Target {
661 &mut self.0
662 }
663}
664impl_from_and_into_var! {
665 fn from(lang: Lang) -> Langs {
666 Langs(vec![lang])
667 }
668 fn from(lang: Option<Lang>) -> Langs {
669 Langs(lang.into_iter().collect())
670 }
671}
672impl fmt::Display for Langs {
673 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
674 let mut sep = "";
675 for l in self.iter() {
676 write!(f, "{sep}{l}")?;
677 sep = ", ";
678 }
679 Ok(())
680 }
681}
682impl std::str::FromStr for Langs {
683 type Err = unic_langid::LanguageIdentifierError;
684
685 fn from_str(s: &str) -> Result<Self, Self::Err> {
686 if s.trim().is_empty() {
687 return Ok(Langs(vec![]));
688 }
689 let mut r = Self(vec![]);
690 for lang in s.split(',') {
691 r.0.push(lang.trim().parse()?)
692 }
693 Ok(r)
694 }
695}
696
697#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
699#[serde(transparent)]
700pub struct LangMap<V> {
701 inner: Vec<(Lang, V)>,
702}
703impl<V> Default for LangMap<V> {
704 fn default() -> Self {
705 Self { inner: Default::default() }
706 }
707}
708impl<V> LangMap<V> {
709 pub fn new() -> Self {
711 LangMap::default()
712 }
713
714 pub fn with_capacity(capacity: usize) -> Self {
716 LangMap {
717 inner: Vec::with_capacity(capacity),
718 }
719 }
720
721 fn exact_i(&self, lang: &Lang) -> Option<usize> {
722 for (i, (key, _)) in self.inner.iter().enumerate() {
723 if key == lang {
724 return Some(i);
725 }
726 }
727 None
728 }
729
730 fn best_i(&self, lang: &Lang) -> Option<usize> {
731 let mut best = None;
732 let mut best_weight = 0;
733
734 for (i, (key, _)) in self.inner.iter().enumerate() {
735 if lang.matches(key, true, true) {
736 let mut weight = 1;
737 let mut eq = 0;
738
739 if key.language == lang.language {
741 weight += 200;
742 eq += 1;
743 }
744 if key.script == lang.script {
746 weight += 100;
747 eq += 1;
748 }
749 if key.region == lang.region {
751 weight += 30;
752 eq += 1;
753 }
754
755 if eq == 3 && lang.variants().eq(key.variants()) {
756 return Some(i);
758 }
759
760 if !key.is_machine_translation() {
762 weight += 60;
763 }
764
765 if best_weight < weight {
766 best_weight = weight;
767 best = Some(i);
768 }
769 }
770 }
771
772 best
773 }
774
775 pub fn best_match(&self, lang: &Lang) -> Option<&Lang> {
777 if let Some(i) = self.best_i(lang) {
778 Some(&self.inner[i].0)
779 } else {
780 None
781 }
782 }
783
784 pub fn get(&self, lang: &Lang) -> Option<&V> {
786 if let Some(i) = self.best_i(lang) {
787 Some(&self.inner[i].1)
788 } else {
789 None
790 }
791 }
792
793 pub fn get_exact(&self, lang: &Lang) -> Option<&V> {
795 if let Some(i) = self.exact_i(lang) {
796 Some(&self.inner[i].1)
797 } else {
798 None
799 }
800 }
801
802 pub fn get_mut(&mut self, lang: &Lang) -> Option<&mut V> {
804 if let Some(i) = self.best_i(lang) {
805 Some(&mut self.inner[i].1)
806 } else {
807 None
808 }
809 }
810
811 pub fn get_exact_mut(&mut self, lang: &Lang) -> Option<&mut V> {
813 if let Some(i) = self.exact_i(lang) {
814 Some(&mut self.inner[i].1)
815 } else {
816 None
817 }
818 }
819
820 pub fn get_exact_or_insert(&mut self, lang: Lang, new: impl FnOnce() -> V) -> &mut V {
822 if let Some(i) = self.exact_i(&lang) {
823 return &mut self.inner[i].1;
824 }
825 let i = self.inner.len();
826 self.inner.push((lang, new()));
827 &mut self.inner[i].1
828 }
829
830 pub fn insert(&mut self, lang: Lang, value: V) -> Option<V> {
834 if let Some(i) = self.exact_i(&lang) {
835 Some(mem::replace(&mut self.inner[i].1, value))
836 } else {
837 self.inner.push((lang, value));
838 None
839 }
840 }
841
842 pub fn remove(&mut self, lang: &Lang) -> Option<V> {
844 if let Some(i) = self.exact_i(lang) {
845 Some(self.inner.swap_remove(i).1)
846 } else {
847 None
848 }
849 }
850
851 pub fn remove_all(&mut self, lang: &Lang) -> usize {
855 let mut count = 0;
856 self.inner.retain(|(key, _)| {
857 let rmv = lang.matches(key, true, false);
858 if rmv {
859 count += 1
860 }
861 !rmv
862 });
863 count
864 }
865
866 pub fn pop(&mut self) -> Option<(Lang, V)> {
868 self.inner.pop()
869 }
870
871 pub fn is_empty(&self) -> bool {
873 self.inner.is_empty()
874 }
875
876 pub fn len(&self) -> usize {
878 self.inner.len()
879 }
880
881 pub fn clear(&mut self) {
883 self.inner.clear()
884 }
885
886 pub fn keys(&self) -> impl std::iter::ExactSizeIterator<Item = &Lang> {
888 self.inner.iter().map(|(k, _)| k)
889 }
890
891 pub fn values(&self) -> impl std::iter::ExactSizeIterator<Item = &V> {
893 self.inner.iter().map(|(_, v)| v)
894 }
895
896 pub fn values_mut(&mut self) -> impl std::iter::ExactSizeIterator<Item = &mut V> {
898 self.inner.iter_mut().map(|(_, v)| v)
899 }
900
901 pub fn into_values(self) -> impl std::iter::ExactSizeIterator<Item = V> {
903 self.inner.into_iter().map(|(_, v)| v)
904 }
905
906 pub fn iter(&self) -> impl std::iter::ExactSizeIterator<Item = (&Lang, &V)> {
908 self.inner.iter().map(|(k, v)| (k, v))
909 }
910
911 pub fn iter_mut(&mut self) -> impl std::iter::ExactSizeIterator<Item = (&Lang, &mut V)> {
913 self.inner.iter_mut().map(|(k, v)| (&*k, v))
914 }
915
916 pub fn shrink_to_fit(&mut self) {
918 self.inner.shrink_to_fit();
919 }
920}
921impl<V> LangMap<HashMap<LangFilePath, V>> {
922 pub fn get_file(&self, lang: &Lang, file: &LangFilePath) -> Option<&V> {
924 let files = self.get(lang)?;
925 if let Some(exact) = files.get(file) {
926 return Some(exact);
927 }
928 Self::best_file(files, file).map(|(_, v)| v)
929 }
930
931 pub fn best_file_match(&self, lang: &Lang, file: &LangFilePath) -> Option<&LangFilePath> {
933 let files = self.get(lang)?;
934 if let Some((exact, _)) = files.get_key_value(file) {
935 return Some(exact);
936 }
937 Self::best_file(files, file).map(|(k, _)| k)
938 }
939
940 fn best_file<'a>(files: &'a HashMap<LangFilePath, V>, file: &LangFilePath) -> Option<(&'a LangFilePath, &'a V)> {
941 let mut best = None;
942 let mut best_dist = u64::MAX;
943 for (k, v) in files {
944 if let Some(d) = k.matches(file)
945 && d < best_dist
946 {
947 best = Some((k, v));
948 best_dist = d;
949 }
950 }
951 best
952 }
953}
954impl<V> IntoIterator for LangMap<V> {
955 type Item = (Lang, V);
956
957 type IntoIter = std::vec::IntoIter<(Lang, V)>;
958
959 fn into_iter(self) -> Self::IntoIter {
960 self.inner.into_iter()
961 }
962}
963impl<V: PartialEq> PartialEq for LangMap<V> {
964 fn eq(&self, other: &Self) -> bool {
965 if self.len() != other.len() {
966 return false;
967 }
968 for (k, v) in &self.inner {
969 if other.get_exact(k) != Some(v) {
970 return false;
971 }
972 }
973 true
974 }
975}
976impl<V: Eq> Eq for LangMap<V> {}
977
978#[derive(Clone, Debug)]
980pub struct FluentParserErrors(pub Vec<fluent_syntax::parser::ParserError>);
981impl fmt::Display for FluentParserErrors {
982 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
983 let mut sep = "";
984 for e in &self.0 {
985 write!(f, "{sep}{e}")?;
986 sep = "\n";
987 }
988 Ok(())
989 }
990}
991impl std::error::Error for FluentParserErrors {
992 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
993 if self.0.len() == 1 { Some(&self.0[0]) } else { None }
994 }
995}
996
997#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1005#[non_exhaustive]
1006pub struct LangFilePath {
1007 pub pkg_name: Txt,
1009 pub pkg_version: Version,
1011 pub file: Txt,
1013}
1014impl Ord for LangFilePath {
1015 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1016 let self_pkg = self.actual_pkg_data();
1017 let other_pkg = other.actual_pkg_data();
1018 match self_pkg.0.cmp(other_pkg.0) {
1019 core::cmp::Ordering::Equal => {}
1020 ord => return ord,
1021 }
1022 match self_pkg.1.cmp(other_pkg.1) {
1023 core::cmp::Ordering::Equal => {}
1024 ord => return ord,
1025 }
1026 self.file().cmp(&other.file())
1027 }
1028}
1029impl PartialOrd for LangFilePath {
1030 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1031 Some(self.cmp(other))
1032 }
1033}
1034impl std::hash::Hash for LangFilePath {
1035 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1036 self.actual_pkg_data().hash(state);
1037 self.file().hash(state);
1038 }
1039}
1040impl Eq for LangFilePath {}
1041impl PartialEq for LangFilePath {
1042 fn eq(&self, other: &Self) -> bool {
1043 self.actual_pkg_data() == other.actual_pkg_data() && self.file() == other.file()
1044 }
1045}
1046impl LangFilePath {
1047 pub fn new(pkg_name: impl Into<Txt>, pkg_version: Version, file: impl Into<Txt>) -> Self {
1049 let r = Self {
1050 pkg_name: pkg_name.into(),
1051 pkg_version,
1052 file: file.into(),
1053 };
1054 debug_assert!(
1056 r.file
1057 .rsplit_once('.')
1058 .map(|(_, ext)| !ext.eq_ignore_ascii_case("ftl"))
1059 .unwrap_or(true),
1060 "file `{}` must not have extension",
1061 r.file
1062 );
1063 debug_assert!(r.file != "_", "file `_` should be an empty string");
1064 r
1065 }
1066
1067 pub fn current_app(file: impl Into<Txt>) -> LangFilePath {
1074 let about = zng_env::about();
1075 Self::new(about.pkg_name.clone(), about.version.clone(), file.into())
1076 }
1077
1078 pub fn is_current_app(&self) -> bool {
1082 self.is_current_app_no_check() || {
1083 let about = zng_env::about();
1084 self.pkg_name == about.pkg_name && self.pkg_version == about.version
1085 }
1086 }
1087
1088 fn is_current_app_no_check(&self) -> bool {
1089 self.pkg_name.is_empty() || self.pkg_version.pre.as_str() == "local"
1090 }
1091
1092 fn actual_pkg_data(&self) -> (&Txt, &Version) {
1093 if self.is_current_app_no_check() {
1094 let about = zng_env::about();
1095 (&about.pkg_name, &about.version)
1096 } else {
1097 (&self.pkg_name, &self.pkg_version)
1098 }
1099 }
1100
1101 pub fn pkg_name(&self) -> Txt {
1107 self.actual_pkg_data().0.clone()
1108 }
1109
1110 pub fn pkg_version(&self) -> Version {
1116 self.actual_pkg_data().1.clone()
1117 }
1118
1119 pub fn file(&self) -> Txt {
1123 if self.file.is_empty() {
1124 Txt::from_char('_')
1125 } else {
1126 self.file.clone()
1127 }
1128 }
1129
1130 pub fn to_path(&self, lang: &Lang) -> PathBuf {
1138 let mut file = self.file.as_str();
1139 if file.is_empty() {
1140 file = "_";
1141 }
1142 if self.is_current_app() {
1143 format!("{lang}/{file}.ftl")
1144 } else {
1145 format!("{lang}/deps/{}/{}/{file}.ftl", self.pkg_name, self.pkg_version)
1146 }
1147 .into()
1148 }
1149
1150 pub fn matches(&self, search: &Self) -> Option<u64> {
1162 let (self_name, self_version) = self.actual_pkg_data();
1163 let (search_name, search_version) = search.actual_pkg_data();
1164
1165 if self_name != search_name {
1166 return None;
1167 }
1168
1169 if self.file != search.file {
1170 let file_a = self.file.rsplit_once('.').map(|t| t.0).unwrap_or(self.file.as_str());
1171 let file_b = search.file.rsplit_once('.').map(|t| t.0).unwrap_or(search.file.as_str());
1172 if file_a != file_b {
1173 let is_empty_a = file_a == "_" || file_a.is_empty();
1174 let is_empty_b = file_b == "_" || file_b.is_empty();
1175 if !(is_empty_a && is_empty_b) {
1176 return None;
1177 }
1178 }
1179 tracing::warn!(
1180 "fallback matching `{}` with `{}`, file was not expected to have extension",
1181 self.file,
1182 search.file
1183 )
1184 }
1185
1186 fn dist(a: u64, b: u64, shift: u64) -> u64 {
1187 let (l, s) = match a.cmp(&b) {
1188 std::cmp::Ordering::Equal => return 0,
1189 std::cmp::Ordering::Less => (b, a),
1190 std::cmp::Ordering::Greater => (a, b),
1191 };
1192
1193 (l - s).min(u16::MAX as u64) << (16 * shift)
1194 }
1195
1196 let mut d = 0;
1197 if self_version.build != search_version.build {
1198 d = 1;
1199 }
1200 if self_version.pre != search_version.pre {
1201 d |= 0b10;
1202 }
1203
1204 d |= dist(self_version.patch, search_version.patch, 1);
1205 d |= dist(self_version.minor, search_version.minor, 2);
1206 d |= dist(self_version.major, search_version.major, 3);
1207
1208 Some(d)
1209 }
1210}
1211impl_from_and_into_var! {
1212 fn from(file: Txt) -> LangFilePath {
1213 LangFilePath::current_app(file)
1214 }
1215
1216 fn from(file: &'static str) -> LangFilePath {
1217 LangFilePath::current_app(file)
1218 }
1219
1220 fn from(file: String) -> LangFilePath {
1221 LangFilePath::current_app(file)
1222 }
1223}
1224
1225#[cfg(test)]
1226mod tests {
1227 use super::*;
1228
1229 #[test]
1230 fn file_matches() {
1231 fn check(a: &str, b: &str, c: &str) {
1232 let ap = LangFilePath::new("name", a.parse().unwrap(), "file");
1233 let bp = LangFilePath::new("name", b.parse().unwrap(), "file");
1234 let cp = LangFilePath::new("name", c.parse().unwrap(), "file");
1235
1236 let ab = ap.matches(&bp);
1237 let ac = ap.matches(&cp);
1238
1239 assert!(ab < ac, "expected {a}.matches({b}) < {a}.matches({c})")
1240 }
1241
1242 check("0.0.0", "0.0.1", "0.1.0");
1243 check("0.0.1", "0.1.0", "1.0.0");
1244 check("0.0.0-pre", "0.0.0-pre+build", "0.0.0-other+build");
1245 check("0.0.0+build", "0.0.0+build", "0.0.0+other");
1246 check("0.0.1", "0.0.2", "0.0.3");
1247 check("0.1.0", "0.2.0", "0.3.0");
1248 check("1.0.0", "2.0.0", "3.0.0");
1249 check("1.0.0", "1.1.0", "2.0.0");
1250 }
1251
1252 #[test]
1253 fn file_name_mismatches() {
1254 let ap = LangFilePath::new("name", "1.0.0".parse().unwrap(), "file-a");
1255 let bp = LangFilePath::new("name", "1.0.0".parse().unwrap(), "file-b");
1256
1257 assert!(ap.matches(&bp).is_none());
1258 }
1259}