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