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 locale != catalog.primary_locale()
106 && entry.spelling(catalog.primary_locale()) == Some(spelling)
107 {
108 continue;
109 }
110 if contains_word(input, spelling) {
111 matches += 1;
112 }
113 }
114 }
115 }
116 for domain in catalog.enum_domains() {
118 for member in &domain.members {
119 if let Some(spelling) = member.spelling(locale) {
120 if locale != catalog.primary_locale()
121 && member.spelling(catalog.primary_locale()) == Some(spelling)
122 {
123 continue;
124 }
125 if contains_word(input, spelling) {
126 matches += 1;
127 }
128 }
129 }
130 }
131 matches
132}
133
134fn contains_word(haystack: &str, needle: &str) -> bool {
136 if needle.is_empty() {
137 return false;
138 }
139 for (start, _) in haystack.match_indices(needle) {
140 let end = start + needle.len();
141 let before_ok = start == 0
142 || !haystack[..start]
143 .chars()
144 .next_back()
145 .is_some_and(is_word_char);
146 let after_ok =
147 end >= haystack.len() || !haystack[end..].chars().next().is_some_and(is_word_char);
148 if before_ok && after_ok {
149 return true;
150 }
151 }
152 false
153}
154
155fn is_word_char(ch: char) -> bool {
156 unicode_ident::is_xid_continue(ch) || ch == '_' || ch == '-'
157}