Skip to main content

zng_ext_l10n/
types.rs

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/// Handle to multiple localization resources.
14#[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    /// Wait for all the resources to load.
30    pub async fn wait(&self) {
31        for res in &self.0 {
32            res.wait().await;
33        }
34    }
35
36    /// Drop all handles without dropping the resource.
37    pub fn perm(self) {
38        for res in self.0 {
39            res.perm()
40        }
41    }
42}
43
44/// Handle to a localization resource.
45#[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    /// Read-only variable with the resource.
61    pub fn resource(&self) -> &Var<Option<ArcEq<fluent::FluentResource>>> {
62        &self.res
63    }
64
65    /// Read-only variable with the resource status.
66    pub fn status(&self) -> &Var<LangResourceStatus> {
67        &self.status
68    }
69
70    /// Drop the handle without unloading the resource.
71    pub fn perm(self) {
72        L10N_SV.write().push_perm_resource(self);
73    }
74
75    /// Await resource status to not be loading.
76    pub async fn wait(&self) {
77        while matches!(self.status.get(), LangResourceStatus::Loading) {
78            self.status.wait_update().await;
79        }
80    }
81}
82
83/// Status of a localization resource.
84#[derive(Clone, Debug)]
85pub enum LangResourceStatus {
86    /// Resource not available.
87    ///
88    /// This can change if the localization directory changes, or the file is created.
89    NotAvailable,
90    /// Resource is loading.
91    Loading,
92    /// Resource loaded ok.
93    Loaded,
94    /// Resource failed to load.
95    ///
96    /// This can be any IO or parse errors. If the resource if *not found* the status is set to
97    /// `NotAvailable`, not an error. Localization messages fallback on error just like they do
98    /// for not available.
99    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
155/// Localized message variable builder.
156///
157/// See [`L10N.message`] for more details.
158///
159/// [`L10N.message`]: L10N::message
160pub 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    /// Add a format arg variable.
169    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    /// Build the message var for the given languages.
179    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    /// Build the message var for the contextual language.
186    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/// Represents an argument value for a localization message.
208///
209/// See [`L10nMessageBuilder::arg`] for more details.
210#[derive(Clone, Debug, PartialEq)]
211pub enum L10nArgument {
212    /// String.
213    Txt(Txt),
214    /// Number, with optional style details.
215    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    /// Borrow argument as a fluent value.
251    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    /// Clone argument as a fluent value.
258    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    /// Language of text in a widget context.
301    ///
302    /// Is [`L10N.app_lang`] by default.
303    ///
304    /// [`L10N.app_lang`]: L10N::app_lang
305    pub static LANG_VAR: Langs = L10N.app_lang();
306}
307
308/// Identifies the language, region and script of text.
309///
310/// Use the [`lang!`] macro to construct one, it does compile-time validation.
311///
312/// Use the [`unic_langid`] crate for more advanced operations such as runtime parsing and editing identifiers, this
313/// type is just an alias for the core struct of that crate.
314///
315/// [`unic_langid`]: https://docs.rs/unic-langid
316#[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    /// Returns character direction of the language.
321    pub fn direction(&self) -> LayoutDirection {
322        if self == &lang!("ms") {
323            // Malay should default to "ms-Latn" that LTR, but unic_langid is matching "ms-Arab"
324            return LayoutDirection::LTR;
325        }
326        if self == &lang!("uz") {
327            // Uzbek should default to "uz-Latn"
328            return LayoutDirection::LTR;
329        }
330        crate::from_unic_char_direction(self.0.character_direction())
331    }
332
333    /// Compares a language to another allowing for either side to use the missing fields as wildcards.
334    ///
335    /// This allows for matching between `en` (treated as `en-*-*-*`) and `en-US`.
336    ///
337    /// # Examples
338    ///
339    /// ```
340    /// # use zng_ext_l10n::*;
341    /// #
342    /// let l1 = lang!("en");
343    /// let l2 = lang!("en-US");
344    ///
345    /// assert_ne!(l1, l2); // "en" != "en-US"
346    /// assert!(!l1.matches(&l2, false, false)); // "en" != "en-US"
347    ///
348    /// assert!(l1.matches(&l2, true, false)); // "en-*-*-*" == "en-US"
349    /// assert!(!l1.matches(&l2, false, true)); // "en" != "en-*-US-*"
350    /// assert!(l1.matches(&l2, true, true)); // "en-*-*-*" == "en-*-US-*"
351    /// ```
352    ///
353    /// Note that `machine` variant is ignored when matching.
354    ///
355    /// ```
356    /// # use zng_ext_l10n::*;
357    ///
358    /// let pt_br = lang!("pt-BR");
359    /// let pt_pt = lang!("pt-PT");
360    /// let pt = lang!("pt");
361    /// let pt_m = lang!("pt-machine");
362    ///
363    /// assert!(!pt_br.matches(&pt_pt, true, false)); // "pt-BR-*-*" != "pt-PT"
364    /// assert!(pt.matches(&pt_pt, true, false)); // "pt-*-*-*" == "pt-PT"
365    /// assert!(pt_m.matches(&pt_pt, true, false)); // "pt-machine-*-*" == "pt-PT"
366    ///
367    /// assert!(pt_m.matches(&pt, false, false)); // "pt-machine" == "pt"
368    /// assert_ne!(pt_m, pt); // but "pt-machine" != "pt" in `PartialEq`
369    /// ```
370    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    /// If the language has a `machine` variant.
393    ///
394    /// Machine translated resources are expected to be of lesser quality, but better than nothing.
395    /// This variant indicates that the resource should be given lower priority. If a machine generated
396    /// resource if reviewed by a human translator the `machine` variant must be removed.
397    pub fn is_machine_translation(&self) -> bool {
398        self.0.variants().any(|v| v == "machine")
399    }
400
401    /// Returns the name of the language and region in own language.
402    ///
403    /// The names are in localized title case, when the name is the same in different scripts the script name
404    /// is part of the language name (currently Hans/Hant).
405    ///
406    /// Returns `None` if the locale is not one of the 90 most common locales embedded. Language must match,
407    /// script must match exactly (omitted different from explicit), region is optional (if not matched returns only language name),
408    /// variants are ignored.
409    #[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            // generated with cargo run --manifest-path tools/lang-autonym-gen/Cargo.toml
416            // please implement any manual corrections in the tool, not here
417            ("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    /// Get ordering of this and `other` as [`fmt::Display`] text.
532    ///
533    /// This method avoids actually allocating a string.
534    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}
561/// Prints the autonym if built with `"lang_autonym"` and has one, otherwise prints the tags.
562///
563/// `{}` prints language and region (if set), `{:#}` only prints language.
564impl 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/// Represents the translated name of a language and region in own language, using the selected script.
589#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
590#[cfg(feature = "lang_autonym")]
591pub struct LangAutonym {
592    /// Language name in its own language.
593    pub language: &'static str,
594    /// Region name in the language.
595    ///
596    /// Can be `None` when a region was requested and only the language name was available.
597    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/// `{}` prints language and region (if set), `{:#}` only prints language.
613#[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/// List of languages, in priority order.
627#[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    /// The first lang on the list or `und` if the list is empty.
647    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/// Represents a map of [`Lang`] keys that can be partially matched.
698#[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    /// New empty default.
710    pub fn new() -> Self {
711        LangMap::default()
712    }
713
714    /// New empty with pre-allocated capacity.
715    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                // must match language
740                if key.language == lang.language {
741                    weight += 200;
742                    eq += 1;
743                }
744                // must match script
745                if key.script == lang.script {
746                    weight += 100;
747                    eq += 1;
748                }
749                // can fallback to different region
750                if key.region == lang.region {
751                    weight += 30;
752                    eq += 1;
753                }
754
755                if eq == 3 && lang.variants().eq(key.variants()) {
756                    // exact match
757                    return Some(i);
758                }
759
760                // human translated of different regions has priority over machine translated
761                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    /// Returns the best match to `lang` currently in the map.
776    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    /// Returns the best match for `lang`.
785    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    /// Returns the exact match for `lang`.
794    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    /// Returns the best match for `lang`.
803    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    /// Returns the exact match for `lang`.
812    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    /// Returns the current value or insert `new` and return a reference to it.
821    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    /// Insert the value with the exact match of `lang`.
831    ///
832    /// Returns the previous exact match.
833    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    /// Remove the exact match of `lang`.
843    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    /// Remove all exact and partial matches of `lang`.
852    ///
853    /// Returns a count of items removed.
854    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    /// Remove the last inserted lang.
867    pub fn pop(&mut self) -> Option<(Lang, V)> {
868        self.inner.pop()
869    }
870
871    /// Returns if the map is empty.
872    pub fn is_empty(&self) -> bool {
873        self.inner.is_empty()
874    }
875
876    /// Returns the number of languages in the map.
877    pub fn len(&self) -> usize {
878        self.inner.len()
879    }
880
881    /// Remove all entries.
882    pub fn clear(&mut self) {
883        self.inner.clear()
884    }
885
886    /// Iterator over lang keys.
887    pub fn keys(&self) -> impl std::iter::ExactSizeIterator<Item = &Lang> {
888        self.inner.iter().map(|(k, _)| k)
889    }
890
891    /// Iterator over values.
892    pub fn values(&self) -> impl std::iter::ExactSizeIterator<Item = &V> {
893        self.inner.iter().map(|(_, v)| v)
894    }
895
896    /// Iterator over values.
897    pub fn values_mut(&mut self) -> impl std::iter::ExactSizeIterator<Item = &mut V> {
898        self.inner.iter_mut().map(|(_, v)| v)
899    }
900
901    /// Into iterator of values.
902    pub fn into_values(self) -> impl std::iter::ExactSizeIterator<Item = V> {
903        self.inner.into_iter().map(|(_, v)| v)
904    }
905
906    /// Iterate over key-value pairs.
907    pub fn iter(&self) -> impl std::iter::ExactSizeIterator<Item = (&Lang, &V)> {
908        self.inner.iter().map(|(k, v)| (k, v))
909    }
910
911    /// Iterate over key-value pairs with mutable values.
912    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    /// Shrink capacity as much as possible.
917    pub fn shrink_to_fit(&mut self) {
918        self.inner.shrink_to_fit();
919    }
920}
921impl<V> LangMap<HashMap<LangFilePath, V>> {
922    /// Returns the match for `lang` and `file`.
923    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    /// Returns the best match to `lang` and `file` currently in the map.
932    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/// Errors found parsing a fluent resource file.
979#[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/// Localization resource file path in the localization directory.
998///
999/// In the default directory layout, localization dependencies are collected using `cargo zng l10n`
1000/// and copied to `l10n/{lang}/deps/{name}/{version}/`, and localization for the app ([`is_current_app`])
1001/// is placed in `l10n/{lang}/`.
1002///
1003/// [`is_current_app`]: Self::is_current_app
1004#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1005#[non_exhaustive]
1006pub struct LangFilePath {
1007    /// Package name.
1008    pub pkg_name: Txt,
1009    /// Package version.
1010    pub pkg_version: Version,
1011    /// The localization file name, without extension.
1012    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    /// New from package name, version and file.
1048    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        // these non-standard names are matched on fallback, but they can cause duplicate caching
1055        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    /// Gets a file in the current app.
1068    ///
1069    /// This value indicates that the localization resources are directly on the `l10n/{lang?}/` directories, not
1070    /// in the dependencies directories.
1071    ///
1072    /// See [`zng_env::about()`] for more details.
1073    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    /// Gets if this file is in the [`current_app`] resources, or the `pkg_name` is empty or the `pkg_version.pre` is `#.#.#-local`.
1079    ///
1080    /// [`current_app`]: Self::current_app
1081    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    /// Gets the normalized package name.
1102    ///
1103    /// This is the app package name if [`is_current_app`], otherwise is just the `pkg_name` value.
1104    ///
1105    /// [`is_current_app`]: Self::is_current_app
1106    pub fn pkg_name(&self) -> Txt {
1107        self.actual_pkg_data().0.clone()
1108    }
1109
1110    /// Gets the normalized package version.
1111    ///
1112    /// This is the app version if [`is_current_app`], otherwise is just the `pkg_version` value.
1113    ///
1114    /// [`is_current_app`]: Self::is_current_app
1115    pub fn pkg_version(&self) -> Version {
1116        self.actual_pkg_data().1.clone()
1117    }
1118
1119    /// Gets the normalized file name.
1120    ///
1121    /// This `"_"` for empty file or the file.
1122    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    /// Get the file path, relative to the localization dir.
1131    ///
1132    /// * Empty file name is the same as `_`.
1133    /// * If package [`is_current_app`] gets `{lang}/{file}.ftl`.
1134    /// * Else if is another package gets `{lang}/deps/{pkg_name}/{pkg_version}/{file}.ftl`.
1135    ///
1136    /// [`is_current_app`]: Self::is_current_app
1137    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    /// Gets a value that indicates if the resources represented by `self` can be used for `search`.
1151    ///
1152    /// The number indicates the quality of the match:
1153    ///
1154    /// * `0` is an exact match.
1155    /// * `b1` is a match with only version `build` differences.
1156    /// * `b10` is a match with only version `pre` differences.
1157    /// * `(0..u16::MAX) << 16` is a match with only `patch` differences and the absolute distance.
1158    /// * `(0..u16::MAX) << 16 * 2` is a match with `minor` differences and the absolute distance.
1159    /// * `(0..u16::MAX) << 16 * 3` is a match with `major` differences and the absolute distance.
1160    /// * `None`` is a `pkg_name` mismatch.
1161    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}