1use super::LocaleFallbackPriority;
6use icu_locale_core::subtags::{Language, Region, Script};
7
8use super::*;
9
10impl LocaleFallbackerWithConfig<'_> {
11 pub(crate) fn normalize(&self, locale: &mut DataLocale, default_script: &mut Option<Script>) {
12 if let Some(subdivision) = locale.subdivision.take() {
14 if let Some(region) = locale.region {
15 if subdivision
16 .as_str()
17 .starts_with(region.to_tinystr().to_ascii_lowercase().as_str())
18 {
19 locale.subdivision = Some(subdivision);
20 }
21 }
22 }
23 let language = locale.language;
24 if self.config.priority == LocaleFallbackPriority::Region && locale.region.is_none() {
26 if let Some(script) = locale.script {
28 locale.region = self
29 .likely_subtags
30 .language_script
31 .get(&(
32 language.to_tinystr().to_unvalidated(),
33 script.to_tinystr().to_unvalidated(),
34 ))
35 .copied();
36 }
37 if locale.region.is_none() {
39 locale.region = self
40 .likely_subtags
41 .language
42 .get_copied(&language.to_tinystr().to_unvalidated())
43 .map(|(_s, r)| r);
44 }
45 }
46 if locale.script.is_some() || self.config.priority == LocaleFallbackPriority::Script {
48 *default_script = locale
49 .region
50 .and_then(|region| {
51 self.likely_subtags.language_region.get_copied(&(
52 language.to_tinystr().to_unvalidated(),
53 region.to_tinystr().to_unvalidated(),
54 ))
55 })
56 .or_else(|| {
57 self.likely_subtags
58 .language
59 .get_copied(&language.to_tinystr().to_unvalidated())
60 .map(|(s, _r)| s)
61 });
62 if locale.script == *default_script {
63 locale.script = None;
64 }
65 }
66 }
67}
68
69impl LocaleFallbackIteratorInner<'_> {
70 pub fn step(&mut self, locale: &mut DataLocale) {
71 match self.config.priority {
72 LocaleFallbackPriority::Language => self.step_language(locale),
73 LocaleFallbackPriority::Script => self.step_script(locale),
74 LocaleFallbackPriority::Region => self.step_region(locale),
75 _ => {
78 debug_assert!(
79 false,
80 "Unknown LocaleFallbackPriority: {:?}",
81 self.config.priority
82 );
83 *locale = Default::default()
84 }
85 }
86 }
87
88 fn step_language(&mut self, locale: &mut DataLocale) {
89 if let Some(value) = locale.subdivision.take() {
91 self.backup_subdivision = Some(value);
92 return;
93 }
94 if let Some(single_variant) = locale.variant.take() {
96 self.backup_variant = Some(single_variant);
97 return;
98 }
99 if let Some((language, script, region)) = self.get_explicit_parent(locale) {
101 locale.language = language;
102 locale.script = script;
103 locale.region = region;
104 locale.variant = self.backup_variant.take();
105 return;
106 }
107 if let Some(region) = locale.region {
109 if locale.script.is_none() {
111 let language = locale.language;
112 if let Some(script) = self.likely_subtags.language_region.get_copied(&(
113 language.to_tinystr().to_unvalidated(),
114 region.to_tinystr().to_unvalidated(),
115 )) {
116 locale.script = Some(script);
117 }
118 }
119 locale.region = None;
120 locale.variant = self.backup_variant.take();
121 return;
122 }
123 debug_assert!(!locale.language.is_unknown() || locale.script.is_some()); locale.script = None;
126 locale.language = Language::UNKNOWN;
127 }
128
129 fn step_region(&mut self, locale: &mut DataLocale) {
130 if let Some(value) = locale.subdivision.take() {
133 self.backup_subdivision = Some(value);
134 return;
135 }
136 if let Some(variant) = locale.variant.take() {
138 self.backup_variant = Some(variant);
139 return;
140 }
141 if !locale.language.is_unknown() || locale.script.is_some() {
143 locale.script = None;
144 locale.language = Language::UNKNOWN;
145 if locale.region.is_some() {
147 locale.variant = self.backup_variant.take();
148 locale.subdivision = self.backup_subdivision.take();
149 }
150 return;
151 }
152 debug_assert!(locale.region.is_some()); locale.region = None;
155 }
156
157 fn step_script(&mut self, locale: &mut DataLocale) {
158 if let Some(value) = locale.subdivision.take() {
160 self.backup_subdivision = Some(value);
161 return;
162 }
163 if let Some(variant) = locale.variant.take() {
165 self.backup_variant = Some(variant);
166 return;
167 }
168 if let Some((language, script, region)) = self.get_explicit_parent(locale) {
170 locale.language = language;
171 locale.script = script;
172 locale.region = region;
173 locale.variant = self.backup_variant.take();
174 return;
175 }
176 if let Some(region) = locale.region {
178 self.backup_region = Some(region);
179 let language_implied_script = self
180 .likely_subtags
181 .language
182 .get_copied(&locale.language.to_tinystr().to_unvalidated())
183 .map(|(s, _r)| s);
184 if language_implied_script != self.max_script {
185 locale.script = self.max_script;
186 }
187 locale.region = None;
188 locale.variant = self.backup_variant.take();
189 return;
190 }
191
192 if !locale.language.is_unknown() {
194 let language_implied_script = self
195 .likely_subtags
196 .language
197 .get_copied(&locale.language.to_tinystr().to_unvalidated())
198 .map(|(s, _r)| s);
199 if locale.script.is_some() && language_implied_script == locale.script {
200 locale.script = None;
201 if let Some(region) = self.backup_region.take() {
202 locale.region = Some(region);
203 locale.subdivision = self.backup_subdivision.take();
204 locale.variant = self.backup_variant.take();
205 }
206 return;
208 } else {
209 locale.language = Language::UNKNOWN;
211 locale.script = self.max_script;
212 if locale.script.is_some() {
214 locale.variant = self.backup_variant.take();
215 }
216 return;
218 }
219 }
220
221 if locale.script.is_some() {
227 locale.script = None;
228 }
229 }
230
231 fn get_explicit_parent(
232 &self,
233 locale: &DataLocale,
234 ) -> Option<(Language, Option<Script>, Option<Region>)> {
235 self.parents
236 .parents
237 .get_copied_by(|uvstr| locale.strict_cmp(uvstr).reverse())
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244 use writeable::Writeable;
245
246 struct TestCase {
247 input: &'static str,
248 requires_data: bool,
249 expected_language_chain: &'static [&'static str],
251 expected_script_chain: &'static [&'static str],
252 expected_region_chain: &'static [&'static str],
253 }
254
255 const TEST_CASES: &[TestCase] = &[
257 TestCase {
258 input: "en-fonipa",
259 requires_data: false,
260 expected_language_chain: &["en-fonipa", "en"],
261 expected_script_chain: &["en-fonipa", "en"],
262 expected_region_chain: &["en-fonipa", "en"],
263 },
264 TestCase {
265 input: "en-US-u-sd-usca",
266 requires_data: false,
267 expected_language_chain: &["en-US-u-sd-usca", "en-US", "en"],
268 expected_script_chain: &["en-US-u-sd-usca", "en-US", "en"],
269 expected_region_chain: &["en-US-u-sd-usca", "en-US", "und-US-u-sd-usca", "und-US"],
270 },
271 TestCase {
272 input: "en-US-fonipa-u-sd-usca",
273 requires_data: false,
274 expected_language_chain: &[
275 "en-US-fonipa-u-sd-usca",
276 "en-US-fonipa",
277 "en-US",
278 "en-fonipa",
279 "en",
280 ],
281 expected_script_chain: &[
282 "en-US-fonipa-u-sd-usca",
283 "en-US-fonipa",
284 "en-US",
285 "en-fonipa",
286 "en",
287 ],
288 expected_region_chain: &[
289 "en-US-fonipa-u-sd-usca",
290 "en-US-fonipa",
291 "en-US",
292 "und-US-fonipa-u-sd-usca",
293 "und-US-fonipa",
294 "und-US",
295 ],
296 },
297 TestCase {
298 input: "en-fonipa",
299 requires_data: true,
300 expected_language_chain: &["en-fonipa", "en"],
301 expected_script_chain: &["en-fonipa", "en", "und-Latn-fonipa", "und-Latn"],
302 expected_region_chain: &["en-US-fonipa", "en-US", "und-US-fonipa", "und-US"],
303 },
304 TestCase {
305 input: "en-Latn-fonipa",
306 requires_data: true,
307 expected_language_chain: &["en-fonipa", "en"],
308 expected_script_chain: &["en-fonipa", "en", "und-Latn-fonipa", "und-Latn"],
309 expected_region_chain: &["en-US-fonipa", "en-US", "und-US-fonipa", "und-US"],
310 },
311 TestCase {
312 input: "en-Latn-US-u-sd-usca",
313 requires_data: true,
314 expected_language_chain: &["en-US-u-sd-usca", "en-US", "en"],
315 expected_script_chain: &["en-US-u-sd-usca", "en-US", "en", "und-Latn"],
316 expected_region_chain: &["en-US-u-sd-usca", "en-US", "und-US-u-sd-usca", "und-US"],
317 },
318 TestCase {
319 input: "sr-ME",
320 requires_data: true,
321 expected_language_chain: &["sr-ME", "sr-Latn"],
322 expected_script_chain: &["sr-ME", "sr-Latn", "und-Latn"],
323 expected_region_chain: &["sr-ME", "und-ME"],
324 },
325 TestCase {
326 input: "sr-Latn-ME",
327 requires_data: true,
328 expected_language_chain: &["sr-ME", "sr-Latn"],
329 expected_script_chain: &["sr-ME", "sr-Latn", "und-Latn"],
330 expected_region_chain: &["sr-ME", "und-ME"],
331 },
332 TestCase {
333 input: "sr-ME-fonipa",
334 requires_data: true,
335 expected_language_chain: &["sr-ME-fonipa", "sr-ME", "sr-Latn-fonipa", "sr-Latn"],
336 expected_script_chain: &[
337 "sr-ME-fonipa",
338 "sr-ME",
339 "sr-Latn-fonipa",
340 "sr-Latn",
341 "und-Latn-fonipa",
342 "und-Latn",
343 ],
344 expected_region_chain: &["sr-ME-fonipa", "sr-ME", "und-ME-fonipa", "und-ME"],
345 },
346 TestCase {
347 input: "sr-RS",
348 requires_data: true,
349 expected_language_chain: &["sr-RS", "sr"],
350 expected_script_chain: &["sr-RS", "sr", "und-Cyrl"],
351 expected_region_chain: &["sr-RS", "und-RS"],
352 },
353 TestCase {
354 input: "sr-Cyrl-RS",
355 requires_data: true,
356 expected_language_chain: &["sr-RS", "sr"],
357 expected_script_chain: &["sr-RS", "sr", "und-Cyrl"],
358 expected_region_chain: &["sr-RS", "und-RS"],
359 },
360 TestCase {
361 input: "sr-Latn-RS",
362 requires_data: true,
363 expected_language_chain: &["sr-Latn-RS", "sr-Latn"],
364 expected_script_chain: &["sr-Latn-RS", "sr-Latn", "und-Latn"],
365 expected_region_chain: &["sr-Latn-RS", "und-RS"],
366 },
367 TestCase {
368 input: "de-Latn-LI",
369 requires_data: true,
370 expected_language_chain: &["de-LI", "de"],
371 expected_script_chain: &["de-LI", "de", "und-Latn"],
372 expected_region_chain: &["de-LI", "und-LI"],
373 },
374 TestCase {
375 input: "ca-ES-valencia",
376 requires_data: true,
377 expected_language_chain: &["ca-ES-valencia", "ca-ES", "ca-valencia", "ca"],
378 expected_script_chain: &[
379 "ca-ES-valencia",
380 "ca-ES",
381 "ca-valencia",
382 "ca",
383 "und-Latn-valencia",
384 "und-Latn",
385 ],
386 expected_region_chain: &["ca-ES-valencia", "ca-ES", "und-ES-valencia", "und-ES"],
387 },
388 TestCase {
389 input: "es-AR",
390 requires_data: true,
391 expected_language_chain: &["es-AR", "es-419", "es"],
392 expected_script_chain: &["es-AR", "es-419", "es", "und-Latn"],
393 expected_region_chain: &["es-AR", "und-AR"],
394 },
395 TestCase {
396 input: "hi-IN",
397 requires_data: true,
398 expected_language_chain: &["hi-IN", "hi"],
399 expected_script_chain: &["hi-IN", "hi", "und-Deva"],
400 expected_region_chain: &["hi-IN", "und-IN"],
401 },
402 TestCase {
403 input: "hi-Latn-IN",
404 requires_data: true,
405 expected_language_chain: &["hi-Latn-IN", "hi-Latn", "en-IN", "en-001", "en"],
406 expected_script_chain: &["hi-Latn-IN", "hi-Latn", "en-IN", "en-001", "en", "und-Latn"],
407 expected_region_chain: &["hi-Latn-IN", "und-IN"],
408 },
409 TestCase {
410 input: "zh-CN",
411 requires_data: true,
412 expected_language_chain: &["zh-CN", "zh"],
416 expected_script_chain: &["zh-CN", "zh", "und-Hans", "und-Hani"],
417 expected_region_chain: &["zh-CN", "und-CN"],
418 },
419 TestCase {
420 input: "zh-TW",
421 requires_data: true,
422 expected_language_chain: &["zh-TW", "zh-Hant"],
423 expected_script_chain: &["zh-TW", "zh-Hant", "und-Hant", "und-Hani"],
424 expected_region_chain: &["zh-TW", "und-TW"],
425 },
426 TestCase {
427 input: "yue-HK",
428 requires_data: true,
429 expected_language_chain: &["yue-HK", "yue"],
430 expected_script_chain: &["yue-HK", "yue", "und-Hant", "und-Hani"],
431 expected_region_chain: &["yue-HK", "und-HK"],
432 },
433 TestCase {
434 input: "yue-HK",
435 requires_data: true,
436 expected_language_chain: &["yue-HK", "yue"],
437 expected_script_chain: &["yue-HK", "yue", "und-Hant", "und-Hani"],
438 expected_region_chain: &["yue-HK", "und-HK"],
439 },
440 TestCase {
441 input: "yue-CN",
442 requires_data: true,
443 expected_language_chain: &["yue-CN", "yue-Hans"],
444 expected_script_chain: &["yue-CN", "yue-Hans", "und-Hans", "und-Hani"],
445 expected_region_chain: &["yue-CN", "und-CN"],
446 },
447 TestCase {
448 input: "az-Arab-IR",
449 requires_data: true,
450 expected_language_chain: &["az-IR", "az-Arab"],
451 expected_script_chain: &["az-IR", "az-Arab", "und-Arab"],
452 expected_region_chain: &["az-IR", "und-IR"],
453 },
454 TestCase {
455 input: "az-IR",
456 requires_data: true,
457 expected_language_chain: &["az-IR", "az-Arab"],
458 expected_script_chain: &["az-IR", "az-Arab", "und-Arab"],
459 expected_region_chain: &["az-IR", "und-IR"],
460 },
461 TestCase {
462 input: "az-Arab",
463 requires_data: true,
464 expected_language_chain: &["az-Arab"],
465 expected_script_chain: &["az-Arab", "und-Arab"],
466 expected_region_chain: &["az-IR", "und-IR"],
467 },
468 ];
469
470 #[test]
471 fn test_fallback() {
472 let fallbacker_no_data = LocaleFallbacker::new_without_data();
473 let fallbacker_no_data = fallbacker_no_data.as_borrowed();
474 let fallbacker_with_data = LocaleFallbacker::new();
475 for cas in TEST_CASES {
476 for (priority, expected_chain) in [
477 (
478 LocaleFallbackPriority::Language,
479 cas.expected_language_chain,
480 ),
481 (LocaleFallbackPriority::Script, cas.expected_script_chain),
482 (LocaleFallbackPriority::Region, cas.expected_region_chain),
483 ] {
484 let mut config = LocaleFallbackConfig::default();
485 config.priority = priority;
486 let fallbacker = if cas.requires_data {
487 fallbacker_with_data
488 } else {
489 fallbacker_no_data
490 };
491 let mut it = fallbacker
492 .for_config(config)
493 .fallback_for(cas.input.parse().unwrap());
494 let mut actual_chain = Vec::new();
495 for i in 0..20 {
496 if i == 19 {
497 eprintln!("20 iterations reached!");
498 }
499 if it.get().is_unknown() {
500 break;
501 }
502 actual_chain.push(it.get().write_to_string().into_owned());
503 it.step();
504 }
505 assert_eq!(
506 expected_chain, &actual_chain,
507 "{:?} ({:?})",
508 cas.input, priority
509 );
510 }
511 }
512 }
513}