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