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, 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}
272
273impl<T: Into<L10nArgument>> IntoL10nVar for L10nSpecialize<T> {
274 fn to_l10n_var(&mut self) -> Var<L10nArgument> {
275 const_var(self.0.take().unwrap().into())
276 }
277}
278impl<T: VarValue + Into<L10nArgument>> IntoL10nVar for &mut L10nSpecialize<Var<T>> {
279 fn to_l10n_var(&mut self) -> Var<L10nArgument> {
280 self.0.take().unwrap().map_into()
281 }
282}
283impl IntoL10nVar for &mut &mut L10nSpecialize<Var<L10nArgument>> {
284 fn to_l10n_var(&mut self) -> Var<L10nArgument> {
285 self.0.take().unwrap()
286 }
287}
288
289context_var! {
290 pub static LANG_VAR: Langs = {
296 if zng_app::APP.extensions().contains::<crate::L10nManager>() {
297 L10N.app_lang()
298 } else {
299 tracing::warn!("LANG_VAR default not connected to L10N.app_lang, L10nManager extension is missing");
300 Langs::default().into_var()
301 }
302 };
303}
304
305#[derive(PartialEq, Eq, Hash, Clone, Default, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
314#[serde(transparent)]
315pub struct Lang(pub unic_langid::LanguageIdentifier);
316impl Lang {
317 pub fn direction(&self) -> LayoutDirection {
319 crate::from_unic_char_direction(self.0.character_direction())
320 }
321
322 pub fn matches(&self, other: &Self, self_as_range: bool, other_as_range: bool) -> bool {
326 self.0.matches(&other.0, self_as_range, other_as_range)
327 }
328}
329impl ops::Deref for Lang {
330 type Target = unic_langid::LanguageIdentifier;
331
332 fn deref(&self) -> &Self::Target {
333 &self.0
334 }
335}
336impl fmt::Debug for Lang {
337 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338 write!(f, "{}", self.0)
339 }
340}
341impl fmt::Display for Lang {
342 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343 write!(f, "{}", self.0)
344 }
345}
346impl std::str::FromStr for Lang {
347 type Err = unic_langid::LanguageIdentifierError;
348
349 fn from_str(s: &str) -> Result<Self, Self::Err> {
350 let s = s.trim();
351 if s.is_empty() {
352 return Ok(lang!(und));
353 }
354 unic_langid::LanguageIdentifier::from_str(s).map(Lang)
355 }
356}
357
358#[derive(Clone, PartialEq, Eq, Default, Hash, serde::Serialize, serde::Deserialize)]
360#[serde(transparent)]
361pub struct Langs(pub Vec<Lang>);
362impl fmt::Debug for Langs {
363 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
364 struct DisplayLangs<'a>(&'a [Lang]);
365 impl fmt::Debug for DisplayLangs<'_> {
366 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367 f.debug_list().entries(self.0.iter()).finish()
368 }
369 }
370 if f.alternate() {
371 f.debug_tuple("Langs").field(&DisplayLangs(&self.0)).finish()
372 } else {
373 fmt::Debug::fmt(&DisplayLangs(&self.0), f)
374 }
375 }
376}
377impl Langs {
378 pub fn best(&self) -> &Lang {
380 static NONE: Lazy<Lang> = Lazy::new(|| lang!(und));
381 self.first().unwrap_or(&NONE)
382 }
383}
384impl ops::Deref for Langs {
385 type Target = Vec<Lang>;
386
387 fn deref(&self) -> &Self::Target {
388 &self.0
389 }
390}
391impl ops::DerefMut for Langs {
392 fn deref_mut(&mut self) -> &mut Self::Target {
393 &mut self.0
394 }
395}
396impl_from_and_into_var! {
397 fn from(lang: Lang) -> Langs {
398 Langs(vec![lang])
399 }
400 fn from(lang: Option<Lang>) -> Langs {
401 Langs(lang.into_iter().collect())
402 }
403}
404impl fmt::Display for Langs {
405 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
406 let mut sep = "";
407 for l in self.iter() {
408 write!(f, "{sep}{l}")?;
409 sep = ", ";
410 }
411 Ok(())
412 }
413}
414impl std::str::FromStr for Langs {
415 type Err = unic_langid::LanguageIdentifierError;
416
417 fn from_str(s: &str) -> Result<Self, Self::Err> {
418 if s.trim().is_empty() {
419 return Ok(Langs(vec![]));
420 }
421 let mut r = Self(vec![]);
422 for lang in s.split(',') {
423 r.0.push(lang.trim().parse()?)
424 }
425 Ok(r)
426 }
427}
428
429#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
431#[serde(transparent)]
432pub struct LangMap<V> {
433 inner: Vec<(Lang, V)>,
434}
435impl<V> Default for LangMap<V> {
436 fn default() -> Self {
437 Self { inner: Default::default() }
438 }
439}
440impl<V> LangMap<V> {
441 pub fn new() -> Self {
443 LangMap::default()
444 }
445
446 pub fn with_capacity(capacity: usize) -> Self {
448 LangMap {
449 inner: Vec::with_capacity(capacity),
450 }
451 }
452
453 fn exact_i(&self, lang: &Lang) -> Option<usize> {
454 for (i, (key, _)) in self.inner.iter().enumerate() {
455 if key == lang {
456 return Some(i);
457 }
458 }
459 None
460 }
461
462 fn best_i(&self, lang: &Lang) -> Option<usize> {
463 let mut best = None;
464 let mut best_weight = 0;
465
466 for (i, (key, _)) in self.inner.iter().enumerate() {
467 if lang.matches(key, true, true) {
468 let mut weight = 1;
469 let mut eq = 0;
470
471 if key.language == lang.language {
472 weight += 128;
473 eq += 1;
474 }
475 if key.region == lang.region {
476 weight += 40;
477 eq += 1;
478 }
479 if key.script == lang.script {
480 weight += 20;
481 eq += 1;
482 }
483
484 if eq == 3 && lang.variants().zip(key.variants()).all(|(a, b)| a == b) {
485 return Some(i);
486 }
487
488 if best_weight < weight {
489 best_weight = weight;
490 best = Some(i);
491 }
492 }
493 }
494
495 best
496 }
497
498 pub fn best_match(&self, lang: &Lang) -> Option<&Lang> {
500 if let Some(i) = self.best_i(lang) {
501 Some(&self.inner[i].0)
502 } else {
503 None
504 }
505 }
506
507 pub fn get(&self, lang: &Lang) -> Option<&V> {
509 if let Some(i) = self.best_i(lang) {
510 Some(&self.inner[i].1)
511 } else {
512 None
513 }
514 }
515
516 pub fn get_exact(&self, lang: &Lang) -> Option<&V> {
518 if let Some(i) = self.exact_i(lang) {
519 Some(&self.inner[i].1)
520 } else {
521 None
522 }
523 }
524
525 pub fn get_mut(&mut self, lang: &Lang) -> Option<&mut V> {
527 if let Some(i) = self.best_i(lang) {
528 Some(&mut self.inner[i].1)
529 } else {
530 None
531 }
532 }
533
534 pub fn get_exact_mut(&mut self, lang: &Lang) -> Option<&mut V> {
536 if let Some(i) = self.exact_i(lang) {
537 Some(&mut self.inner[i].1)
538 } else {
539 None
540 }
541 }
542
543 pub fn get_exact_or_insert(&mut self, lang: Lang, new: impl FnOnce() -> V) -> &mut V {
545 if let Some(i) = self.exact_i(&lang) {
546 return &mut self.inner[i].1;
547 }
548 let i = self.inner.len();
549 self.inner.push((lang, new()));
550 &mut self.inner[i].1
551 }
552
553 pub fn insert(&mut self, lang: Lang, value: V) -> Option<V> {
557 if let Some(i) = self.exact_i(&lang) {
558 Some(mem::replace(&mut self.inner[i].1, value))
559 } else {
560 self.inner.push((lang, value));
561 None
562 }
563 }
564
565 pub fn remove(&mut self, lang: &Lang) -> Option<V> {
567 if let Some(i) = self.exact_i(lang) {
568 Some(self.inner.swap_remove(i).1)
569 } else {
570 None
571 }
572 }
573
574 pub fn remove_all(&mut self, lang: &Lang) -> usize {
578 let mut count = 0;
579 self.inner.retain(|(key, _)| {
580 let rmv = lang.matches(key, true, false);
581 if rmv {
582 count += 1
583 }
584 !rmv
585 });
586 count
587 }
588
589 pub fn pop(&mut self) -> Option<(Lang, V)> {
591 self.inner.pop()
592 }
593
594 pub fn is_empty(&self) -> bool {
596 self.inner.is_empty()
597 }
598
599 pub fn len(&self) -> usize {
601 self.inner.len()
602 }
603
604 pub fn clear(&mut self) {
606 self.inner.clear()
607 }
608
609 pub fn keys(&self) -> impl std::iter::ExactSizeIterator<Item = &Lang> {
611 self.inner.iter().map(|(k, _)| k)
612 }
613
614 pub fn values(&self) -> impl std::iter::ExactSizeIterator<Item = &V> {
616 self.inner.iter().map(|(_, v)| v)
617 }
618
619 pub fn values_mut(&mut self) -> impl std::iter::ExactSizeIterator<Item = &mut V> {
621 self.inner.iter_mut().map(|(_, v)| v)
622 }
623
624 pub fn into_values(self) -> impl std::iter::ExactSizeIterator<Item = V> {
626 self.inner.into_iter().map(|(_, v)| v)
627 }
628
629 pub fn iter(&self) -> impl std::iter::ExactSizeIterator<Item = (&Lang, &V)> {
631 self.inner.iter().map(|(k, v)| (k, v))
632 }
633
634 pub fn iter_mut(&mut self) -> impl std::iter::ExactSizeIterator<Item = (&Lang, &mut V)> {
636 self.inner.iter_mut().map(|(k, v)| (&*k, v))
637 }
638}
639impl<V> LangMap<HashMap<LangFilePath, V>> {
640 pub fn get_file(&self, lang: &Lang, file: &LangFilePath) -> Option<&V> {
642 let files = self.get(lang)?;
643 if let Some(exact) = files.get(file) {
644 return Some(exact);
645 }
646 Self::best_file(files, file).map(|(_, v)| v)
647 }
648
649 pub fn best_file_match(&self, lang: &Lang, file: &LangFilePath) -> Option<&LangFilePath> {
651 let files = self.get(lang)?;
652 if let Some((exact, _)) = files.get_key_value(file) {
653 return Some(exact);
654 }
655 Self::best_file(files, file).map(|(k, _)| k)
656 }
657
658 fn best_file<'a>(files: &'a HashMap<LangFilePath, V>, file: &LangFilePath) -> Option<(&'a LangFilePath, &'a V)> {
659 let mut best = None;
660 let mut best_dist = u64::MAX;
661 for (k, v) in files {
662 if let Some(d) = k.matches(file)
663 && d < best_dist
664 {
665 best = Some((k, v));
666 best_dist = d;
667 }
668 }
669 best
670 }
671}
672impl<V> IntoIterator for LangMap<V> {
673 type Item = (Lang, V);
674
675 type IntoIter = std::vec::IntoIter<(Lang, V)>;
676
677 fn into_iter(self) -> Self::IntoIter {
678 self.inner.into_iter()
679 }
680}
681impl<V: PartialEq> PartialEq for LangMap<V> {
682 fn eq(&self, other: &Self) -> bool {
683 if self.len() != other.len() {
684 return false;
685 }
686 for (k, v) in &self.inner {
687 if other.get_exact(k) != Some(v) {
688 return false;
689 }
690 }
691 true
692 }
693}
694impl<V: Eq> Eq for LangMap<V> {}
695
696#[derive(Clone, Debug)]
698pub struct FluentParserErrors(pub Vec<fluent_syntax::parser::ParserError>);
699impl fmt::Display for FluentParserErrors {
700 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
701 let mut sep = "";
702 for e in &self.0 {
703 write!(f, "{sep}{e}")?;
704 sep = "\n";
705 }
706 Ok(())
707 }
708}
709impl std::error::Error for FluentParserErrors {
710 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
711 if self.0.len() == 1 { Some(&self.0[0]) } else { None }
712 }
713}
714
715#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
723#[non_exhaustive]
724pub struct LangFilePath {
725 pub pkg_name: Txt,
727 pub pkg_version: Version,
729 pub file: Txt,
731}
732impl Ord for LangFilePath {
733 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
734 let self_pkg = self.actual_pkg_data();
735 let other_pkg = other.actual_pkg_data();
736 match self_pkg.0.cmp(other_pkg.0) {
737 core::cmp::Ordering::Equal => {}
738 ord => return ord,
739 }
740 match self_pkg.1.cmp(other_pkg.1) {
741 core::cmp::Ordering::Equal => {}
742 ord => return ord,
743 }
744 self.file().cmp(&other.file())
745 }
746}
747impl PartialOrd for LangFilePath {
748 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
749 Some(self.cmp(other))
750 }
751}
752impl std::hash::Hash for LangFilePath {
753 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
754 self.actual_pkg_data().hash(state);
755 self.file().hash(state);
756 }
757}
758impl Eq for LangFilePath {}
759impl PartialEq for LangFilePath {
760 fn eq(&self, other: &Self) -> bool {
761 self.actual_pkg_data() == other.actual_pkg_data() && self.file() == other.file()
762 }
763}
764impl LangFilePath {
765 pub fn new(pkg_name: impl Into<Txt>, pkg_version: Version, file: impl Into<Txt>) -> Self {
767 let r = Self {
768 pkg_name: pkg_name.into(),
769 pkg_version,
770 file: file.into(),
771 };
772 debug_assert!(
774 r.file
775 .rsplit_once('.')
776 .map(|(_, ext)| !ext.eq_ignore_ascii_case("ftl"))
777 .unwrap_or(true),
778 "file `{}` must not have extension",
779 r.file
780 );
781 debug_assert!(r.file != "_", "file `_` should be an empty string");
782 r
783 }
784
785 pub fn current_app(file: impl Into<Txt>) -> LangFilePath {
792 let about = zng_env::about();
793 Self::new(about.pkg_name.clone(), about.version.clone(), file.into())
794 }
795
796 pub fn is_current_app(&self) -> bool {
800 self.is_current_app_no_check() || {
801 let about = zng_env::about();
802 self.pkg_name == about.pkg_name && self.pkg_version == about.version
803 }
804 }
805
806 fn is_current_app_no_check(&self) -> bool {
807 self.pkg_name.is_empty() || self.pkg_version.pre.as_str() == "local"
808 }
809
810 fn actual_pkg_data(&self) -> (&Txt, &Version) {
811 if self.is_current_app_no_check() {
812 let about = zng_env::about();
813 (&about.pkg_name, &about.version)
814 } else {
815 (&self.pkg_name, &self.pkg_version)
816 }
817 }
818
819 pub fn pkg_name(&self) -> Txt {
825 self.actual_pkg_data().0.clone()
826 }
827
828 pub fn pkg_version(&self) -> Version {
834 self.actual_pkg_data().1.clone()
835 }
836
837 pub fn file(&self) -> Txt {
841 if self.file.is_empty() {
842 Txt::from_char('_')
843 } else {
844 self.file.clone()
845 }
846 }
847
848 pub fn to_path(&self, lang: &Lang) -> PathBuf {
856 let mut file = self.file.as_str();
857 if file.is_empty() {
858 file = "_";
859 }
860 if self.is_current_app() {
861 format!("{lang}/{file}.ftl")
862 } else {
863 format!("{lang}/deps/{}/{}/{file}.ftl", self.pkg_name, self.pkg_version)
864 }
865 .into()
866 }
867
868 pub fn matches(&self, search: &Self) -> Option<u64> {
880 let (self_name, self_version) = self.actual_pkg_data();
881 let (search_name, search_version) = search.actual_pkg_data();
882
883 if self_name != search_name {
884 return None;
885 }
886
887 if self.file != search.file {
888 let file_a = self.file.rsplit_once('.').map(|t| t.0).unwrap_or(self.file.as_str());
889 let file_b = search.file.rsplit_once('.').map(|t| t.0).unwrap_or(search.file.as_str());
890 if file_a != file_b {
891 let is_empty_a = file_a == "_" || file_a.is_empty();
892 let is_empty_b = file_b == "_" || file_b.is_empty();
893 if !(is_empty_a && is_empty_b) {
894 return None;
895 }
896 }
897 tracing::warn!(
898 "fallback matching `{}` with `{}`, file was not expected to have extension",
899 self.file,
900 search.file
901 )
902 }
903
904 fn dist(a: u64, b: u64, shift: u64) -> u64 {
905 let (l, s) = match a.cmp(&b) {
906 std::cmp::Ordering::Equal => return 0,
907 std::cmp::Ordering::Less => (b, a),
908 std::cmp::Ordering::Greater => (a, b),
909 };
910
911 (l - s).min(u16::MAX as u64) << (16 * shift)
912 }
913
914 let mut d = 0;
915 if self_version.build != search_version.build {
916 d = 1;
917 }
918 if self_version.pre != search_version.pre {
919 d |= 0b10;
920 }
921
922 d |= dist(self_version.patch, search_version.patch, 1);
923 d |= dist(self_version.minor, search_version.minor, 2);
924 d |= dist(self_version.major, search_version.major, 3);
925
926 Some(d)
927 }
928}
929impl_from_and_into_var! {
930 fn from(file: Txt) -> LangFilePath {
931 LangFilePath::current_app(file)
932 }
933
934 fn from(file: &'static str) -> LangFilePath {
935 LangFilePath::current_app(file)
936 }
937
938 fn from(file: String) -> LangFilePath {
939 LangFilePath::current_app(file)
940 }
941}
942
943#[cfg(test)]
944mod tests {
945 use super::*;
946
947 #[test]
948 fn file_matches() {
949 fn check(a: &str, b: &str, c: &str) {
950 let ap = LangFilePath::new("name", a.parse().unwrap(), "file");
951 let bp = LangFilePath::new("name", b.parse().unwrap(), "file");
952 let cp = LangFilePath::new("name", c.parse().unwrap(), "file");
953
954 let ab = ap.matches(&bp);
955 let ac = ap.matches(&cp);
956
957 assert!(ab < ac, "expected {a}.matches({b}) < {a}.matches({c})")
958 }
959
960 check("0.0.0", "0.0.1", "0.1.0");
961 check("0.0.1", "0.1.0", "1.0.0");
962 check("0.0.0-pre", "0.0.0-pre+build", "0.0.0-other+build");
963 check("0.0.0+build", "0.0.0+build", "0.0.0+other");
964 check("0.0.1", "0.0.2", "0.0.3");
965 check("0.1.0", "0.2.0", "0.3.0");
966 check("1.0.0", "2.0.0", "3.0.0");
967 check("1.0.0", "1.1.0", "2.0.0");
968 }
969
970 #[test]
971 fn file_name_mismatches() {
972 let ap = LangFilePath::new("name", "1.0.0".parse().unwrap(), "file-a");
973 let bp = LangFilePath::new("name", "1.0.0".parse().unwrap(), "file-b");
974
975 assert!(ap.matches(&bp).is_none());
976 }
977}