1use std::fmt;
5
6use log::{debug, warn};
7
8use super::{Localizer, supports_locale};
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum LocaleSource {
13 ExplicitArgument,
15 EnvironmentVariable,
17 Configuration,
19 Fallback,
21}
22
23impl fmt::Display for LocaleSource {
24 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
25 match self {
26 Self::ExplicitArgument => formatter.write_str("explicit locale override"),
27 Self::EnvironmentVariable => formatter.write_str("DYLINT_LOCALE"),
28 Self::Configuration => formatter.write_str("configuration locale"),
29 Self::Fallback => formatter.write_str("fallback locale"),
30 }
31 }
32}
33
34#[derive(Clone, Debug)]
36pub struct LocaleSelection {
37 localizer: Localizer,
38 source: LocaleSource,
39 requested: Option<String>,
40}
41
42impl LocaleSelection {
43 const fn new(localizer: Localizer, source: LocaleSource, requested: Option<String>) -> Self {
44 Self {
45 localizer,
46 source,
47 requested,
48 }
49 }
50
51 #[must_use]
53 pub const fn source(&self) -> LocaleSource {
54 self.source
55 }
56
57 #[must_use]
59 pub fn requested(&self) -> Option<&str> {
60 self.requested.as_deref()
61 }
62
63 #[must_use]
65 pub fn locale(&self) -> &str {
66 self.localizer.locale()
67 }
68
69 #[must_use]
71 pub fn used_fallback(&self) -> bool {
72 self.localizer.used_fallback()
73 }
74
75 #[must_use]
77 pub fn localizer(&self) -> &Localizer {
78 &self.localizer
79 }
80
81 #[must_use]
83 pub fn into_localizer(self) -> Localizer {
84 self.localizer
85 }
86
87 pub fn log_outcome(&self, target: &str) {
89 debug!(
90 target: target,
91 "resolved {} to `{}`",
92 self.source(),
93 self.locale(),
94 );
95 }
96}
97
98fn try_resolve_candidate(source: LocaleSource, raw: Option<&str>) -> Option<LocaleSelection> {
100 let candidate = normalise_locale(raw)?;
101
102 if supports_locale(candidate) {
103 return Some(LocaleSelection::new(
104 Localizer::new(Some(candidate)),
105 source,
106 Some(candidate.to_owned()),
107 ));
108 }
109
110 warn!(
111 target: "i18n::selection",
112 "skipping unsupported {source} `{candidate}`; continuing locale resolution",
113 );
114
115 None
116}
117
118#[must_use]
127pub fn resolve_localizer(
128 explicit: Option<&str>,
129 environment: Option<String>,
130 configuration: Option<&str>,
131) -> LocaleSelection {
132 let candidates = [
133 (LocaleSource::ExplicitArgument, explicit),
134 (LocaleSource::EnvironmentVariable, environment.as_deref()),
135 (LocaleSource::Configuration, configuration),
136 ];
137
138 candidates
139 .into_iter()
140 .find_map(|(source, raw)| try_resolve_candidate(source, raw))
141 .unwrap_or_else(|| LocaleSelection::new(Localizer::new(None), LocaleSource::Fallback, None))
142}
143
144#[must_use]
146pub fn normalise_locale(input: Option<&str>) -> Option<&str> {
147 input.map(str::trim).filter(|value| !value.is_empty())
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use rstest::rstest;
154
155 #[derive(Clone, Copy, Debug)]
156 struct ResolutionCase {
157 explicit: Option<&'static str>,
158 environment: Option<&'static str>,
159 configuration: Option<&'static str>,
160 expected_source: LocaleSource,
161 expected_locale: &'static str,
162 expected_fallback: bool,
163 }
164
165 impl ResolutionCase {
166 fn environment(&self) -> Option<String> {
167 self.environment.map(String::from)
168 }
169 }
170
171 #[rstest]
172 #[case(ResolutionCase {
173 explicit: None,
174 environment: None,
175 configuration: None,
176 expected_source: LocaleSource::Fallback,
177 expected_locale: "en-GB",
178 expected_fallback: true,
179 })]
180 #[case(ResolutionCase {
181 explicit: Some("cy"),
182 environment: None,
183 configuration: None,
184 expected_source: LocaleSource::ExplicitArgument,
185 expected_locale: "cy",
186 expected_fallback: false,
187 })]
188 #[case(ResolutionCase {
189 explicit: None,
190 environment: Some("gd"),
191 configuration: None,
192 expected_source: LocaleSource::EnvironmentVariable,
193 expected_locale: "gd",
194 expected_fallback: false,
195 })]
196 #[case(ResolutionCase {
197 explicit: None,
198 environment: None,
199 configuration: Some("cy"),
200 expected_source: LocaleSource::Configuration,
201 expected_locale: "cy",
202 expected_fallback: false,
203 })]
204 #[case(ResolutionCase {
205 explicit: Some("zz"),
206 environment: Some("yy"),
207 configuration: Some("cy"),
208 expected_source: LocaleSource::Configuration,
209 expected_locale: "cy",
210 expected_fallback: false,
211 })]
212 fn resolves_sources(#[case] case: ResolutionCase) {
213 let selection = resolve_localizer(case.explicit, case.environment(), case.configuration);
214
215 assert_eq!(selection.source(), case.expected_source);
216 assert_eq!(selection.locale(), case.expected_locale);
217 assert_eq!(selection.used_fallback(), case.expected_fallback);
218 }
219
220 #[rstest]
221 #[case(None, None)]
222 #[case(Some(""), None)]
223 #[case(Some(" "), None)]
224 #[case(Some("cy"), Some("cy"))]
225 #[case(Some(" cy "), Some("cy"))]
226 fn normalises_candidates(#[case] input: Option<&str>, #[case] expected: Option<&str>) {
227 assert_eq!(normalise_locale(input), expected);
228 }
229}