workshop_rs/catalog/
detect.rs1use crate::catalog::{Catalog, Locale};
2use crate::core::error::{Result, WorkshopError};
3
4#[derive(Debug, Clone, PartialEq)]
6pub struct Detection {
7 pub locale: Locale,
9 pub confidence: f64,
11 pub matches: usize,
13 pub candidates: Vec<(Locale, usize)>,
15}
16
17pub const MIN_MATCHES: usize = 2;
19
20pub fn detect(input: &str, catalog: &Catalog) -> Detection {
22 let mut candidates: Vec<(Locale, usize)> = catalog
23 .locales()
24 .iter()
25 .map(|locale| {
26 let matches = locale_alias_matches(input, catalog, locale);
27 (locale.clone(), matches)
28 })
29 .collect();
30 candidates.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
31
32 let (locale, matches) = candidates
33 .first()
34 .cloned()
35 .unwrap_or_else(|| (Locale::new("en-US"), 0));
36 let confidence = matches as f64 / (matches as f64 + 1.0);
37 Detection {
38 locale,
39 confidence,
40 matches,
41 candidates,
42 }
43}
44
45pub fn resolve_locale(
48 input: &str,
49 catalog: &Catalog,
50 override_locale: Option<&Locale>,
51) -> Result<Locale> {
52 if let Some(locale) = override_locale {
53 if !catalog.supports(locale) {
54 return Err(WorkshopError::Unknown {
55 kind: "locale",
56 spelling: locale.to_string(),
57 locale: locale.clone(),
58 span: None,
59 });
60 }
61 return Ok(locale.clone());
62 }
63 let detection = detect(input, catalog);
64 if detection.matches == 0 {
65 return Err(WorkshopError::Unknown {
66 kind: "language",
67 spelling: "<none>".to_string(),
68 locale: detection.locale,
69 span: None,
70 });
71 }
72 if detection.matches < MIN_MATCHES {
73 return Err(WorkshopError::Unsupported {
74 message: format!(
75 "insufficient evidence to detect the Workshop client language ({} distinct match(es))",
76 detection.matches
77 ),
78 span: None,
79 });
80 }
81 if detection.candidates.len() > 1
82 && detection.candidates[0].1 == detection.candidates[1].1
83 && detection.candidates[0].1 > 0
84 {
85 return Err(WorkshopError::Unsupported {
86 message: "ambiguous Workshop client language: multiple locales tie".to_string(),
87 span: None,
88 });
89 }
90 Ok(detection.locale)
91}
92
93fn locale_alias_matches(input: &str, catalog: &Catalog, locale: &Locale) -> usize {
95 let mut matches = 0usize;
96 for kind in [
97 crate::catalog::Kind::Structural,
98 crate::catalog::Kind::Action,
99 crate::catalog::Kind::Value,
100 crate::catalog::Kind::Event,
101 crate::catalog::Kind::Operator,
102 ] {
103 for entry in catalog.entries_of(kind) {
104 if let Some(spelling) = entry.spelling(locale) {
105 if contains_word(input, spelling) {
106 matches += 1;
107 }
108 }
109 }
110 }
111 for domain in catalog.enum_domains() {
113 for member in &domain.members {
114 if let Some(spelling) = member.spelling(locale) {
115 if contains_word(input, spelling) {
116 matches += 1;
117 }
118 }
119 }
120 }
121 matches
122}
123
124fn contains_word(haystack: &str, needle: &str) -> bool {
126 if needle.is_empty() {
127 return false;
128 }
129 for (start, _) in haystack.match_indices(needle) {
130 let end = start + needle.len();
131 let before_ok = start == 0
132 || !haystack[..start]
133 .chars()
134 .next_back()
135 .is_some_and(is_word_char);
136 let after_ok =
137 end >= haystack.len() || !haystack[end..].chars().next().is_some_and(is_word_char);
138 if before_ok && after_ok {
139 return true;
140 }
141 }
142 false
143}
144
145fn is_word_char(ch: char) -> bool {
146 ch.is_alphanumeric() || ch == '_' || ch == '-'
147}