Skip to main content

shuyi_assets/lang/
matching.rs

1//! The game's language that fits the system's languages ([`best_match`]).
2use crate::prelude::*;
3
4use super::tag::{self, LangCode};
5
6/// The language subtag of Chinese.
7const CHINESE: &str = "zh";
8
9/// The script a Chinese tag without one takes from its region: `Hans` for
10/// `CN` and `SG`, `Hant` for `TW`, `HK` and `MO`. `None` for another
11/// language, a tag with a script, or another region.
12fn chinese_script(code: &LangCode) -> Option<&'static str> {
13    if code.language() != CHINESE || code.script().is_some() {
14        return None;
15    }
16    match code.region()? {
17        "CN" | "SG" => Some("Hans"),
18        "TW" | "HK" | "MO" => Some("Hant"),
19        _ => None,
20    }
21}
22
23/// The first of `tag`, then each of its parents, that is offered.
24fn offered_along<'a>(tag: &str, offered: &'a [LangCode]) -> Option<&'a LangCode> {
25    tag::ancestry(tag).find_map(|step| offered.iter().find(|code| code.as_str() == step))
26}
27
28/// The offered tag `wanted` matches, by the three steps of [`best_match`].
29fn match_one(wanted: &LangCode, offered: &[LangCode]) -> Option<LangCode> {
30    if let Some(found) = offered_along(wanted.as_str(), offered) {
31        return Some(found.clone());
32    }
33    let language = wanted.language();
34    let region_script = chinese_script(wanted);
35    if let Some(script) = region_script {
36        let rest = &wanted.as_str()[language.len()..];
37        if let Some(found) = offered_along(&format!("{language}-{script}{rest}"), offered) {
38            return Some(found.clone());
39        }
40    }
41    let mut same = offered.iter().filter(|code| code.language() == language);
42    let only = same.next()?;
43    if same.next().is_some() {
44        return None;
45    }
46    let script = wanted.script().or(region_script);
47    match (script, only.script().or_else(|| chinese_script(only))) {
48        (Some(want), Some(have)) if want != have => None,
49        _ => Some(only.clone()),
50    }
51}
52
53/// The offered language that fits the system's: `preferred` is the
54/// system's languages, most preferred first, and `offered` the source's tag
55/// and the packs'. For each preferred tag in order:
56///
57/// 1. the tag, then each of its [`parent`](LangCode::parent)s, if offered
58///    (`en-GB` matches an `en` source);
59/// 2. Chinese without a script takes one from its region (`CN` and `SG`
60///    give `Hans`; `TW`, `HK` and `MO` give `Hant`), then step 1 again with
61///    the script written after the language (`zh-CN` matches `zh-Hans`,
62///    through `zh-Hans-CN`);
63/// 3. the only offered tag with the same language subtag, unless both have
64///    a script (written, or from step 2) and they differ (`pt-PT` matches a
65///    lone `pt-BR`; `zh-TW` doesn't match `zh-Hans`).
66///
67/// The first preferred tag that matches wins. `None` if none does: the
68/// caller then uses the source. No CLDR data: no likely subtags beyond
69/// step 2's five regions, no aliases, and no parent but the shorter tag.
70pub fn best_match(preferred: &[LangCode], offered: &[LangCode]) -> Option<LangCode> {
71    preferred
72        .iter()
73        .find_map(|wanted| match_one(wanted, offered))
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    fn tags(texts: &[&str]) -> Vec<LangCode> {
81        texts
82            .iter()
83            .map(|text| LangCode::new(text).unwrap_or_else(|| panic!("{text}")))
84            .collect()
85    }
86
87    fn best(preferred: &[&str], offered: &[&str]) -> Option<String> {
88        best_match(&tags(preferred), &tags(offered)).map(|code| code.as_str().to_owned())
89    }
90
91    #[test]
92    fn a_tag_matches_itself_or_a_parent() {
93        assert_eq!(best(&["pt-BR"], &["en", "pt"]), Some("pt".into()));
94        assert_eq!(best(&["en-GB"], &["en"]), Some("en".into()));
95        assert_eq!(best(&["pt-BR"], &["pt", "pt-BR"]), Some("pt-BR".into()));
96        assert_eq!(best(&["ja"], &["en", "ja"]), Some("ja".into()));
97    }
98
99    #[test]
100    fn chinese_takes_its_script_from_its_region() {
101        assert_eq!(best(&["zh-CN"], &["en", "zh-Hans"]), Some("zh-Hans".into()));
102        assert_eq!(best(&["zh-TW"], &["en", "zh-Hans"]), None);
103        assert_eq!(
104            best(&["zh-CN"], &["zh-Hant", "zh-Hans-CN"]),
105            Some("zh-Hans-CN".into())
106        );
107        for (region, script) in [
108            ("CN", "Hans"),
109            ("SG", "Hans"),
110            ("TW", "Hant"),
111            ("HK", "Hant"),
112            ("MO", "Hant"),
113        ] {
114            let offered = ["en", "zh-Hans", "zh-Hant"];
115            let wanted = format!("zh-{region}");
116            assert_eq!(
117                best(&[&wanted], &offered),
118                Some(format!("zh-{script}")),
119                "{wanted}"
120            );
121        }
122        assert_eq!(best(&["zh-MY"], &["zh-Hans", "zh-Hant"]), None);
123    }
124
125    #[test]
126    fn only_chinese_without_a_script_takes_one_from_its_region() {
127        let script = |text: &str| chinese_script(&tags(&[text])[0]);
128        assert_eq!(script("zh-SG"), Some("Hans"));
129        assert_eq!(script("ms-SG"), None);
130        assert_eq!(script("zh-Hant-CN"), None);
131        assert_eq!(best(&["ms-SG"], &["en", "ms-Arab"]), Some("ms-Arab".into()));
132    }
133
134    #[test]
135    fn a_lone_tag_of_the_language_matches_unless_the_scripts_differ() {
136        assert_eq!(best(&["pt-PT"], &["en", "pt-BR"]), Some("pt-BR".into()));
137        assert_eq!(best(&["pt-PT"], &["en", "pt-BR", "pt-AO"]), None);
138        assert_eq!(best(&["sr-Latn"], &["en", "sr-Cyrl"]), None);
139        assert_eq!(best(&["sr-Latn"], &["en", "sr-RS"]), Some("sr-RS".into()));
140        assert_eq!(best(&["zh-HK"], &["en", "zh-CN"]), None);
141        assert_eq!(best(&["zh-HK"], &["en", "zh-TW"]), Some("zh-TW".into()));
142        assert_eq!(best(&["zh"], &["en", "zh-Hant"]), Some("zh-Hant".into()));
143    }
144
145    #[test]
146    fn the_first_preferred_tag_that_matches_wins() {
147        assert_eq!(best(&["de", "fr-CA"], &["en", "fr"]), Some("fr".into()));
148        assert_eq!(best(&["fr-CA", "en"], &["en", "fr"]), Some("fr".into()));
149        assert_eq!(best(&["de", "it"], &["en", "fr"]), None);
150        assert_eq!(best(&[], &["en", "fr"]), None);
151        assert_eq!(best(&["en"], &[]), None);
152    }
153}