use mf2::{Dir, LanguageMatching};
fn cldr() -> &'static LanguageMatching {
LanguageMatching::cldr()
}
fn best(desired: &[&str], supported: &[&'static str]) -> Option<&'static str> {
let table: Vec<(&str, Dir)> = supported.iter().map(|t| (*t, Dir::Ltr)).collect();
let at = cldr().best_match(desired.iter().copied(), &table)?;
supported.get(at).copied()
}
fn distance(desired: &str, supported: &str) -> u32 {
cldr()
.distance_of(desired, supported)
.unwrap_or_else(|| panic!("{desired} and {supported} are tags"))
}
#[test]
fn traditional_chinese_finds_taiwan_through_likely_subtags() {
assert_eq!(distance("zh-Hant-TW", "zh-TW"), 0);
assert_eq!(best(&["zh-Hant-TW"], &["zh", "zh-TW"]), Some("zh-TW"));
assert_eq!(best(&["zh-Hant"], &["zh", "zh-TW"]), Some("zh-TW"));
assert_eq!(distance("zh-HK", "zh-TW"), 5);
assert_eq!(best(&["zh-HK"], &["zh", "zh-TW"]), Some("zh-TW"));
assert_eq!(best(&["zh-HK"], &["zh-TW", "zh-MO"]), Some("zh-MO"));
}
#[test]
fn traditional_and_simplified_are_not_served_for_each_other() {
assert_eq!(distance("zh-Hant", "zh-Hans"), 54);
assert_eq!(distance("zh-Hans", "zh-Hant"), 54);
assert_eq!(best(&["zh-Hant"], &["zh-Hans"]), None);
assert_eq!(best(&["zh-Hans"], &["zh-Hant"]), None);
assert_eq!(best(&["zh-TW"], &["zh-CN"]), None);
assert_eq!(best(&["zh"], &["zh-TW"]), None);
assert_eq!(best(&["zh-TW"], &["zh"]), None);
assert_eq!(best(&["zh-Hant", "en"], &["zh", "en"]), Some("en"));
assert_eq!(best(&["zh-Hant"], &["zh", "en"]), None);
assert_eq!(best(&["zh-TW", "zh"], &["zh", "en"]), Some("zh"));
}
#[test]
fn serbian_scripts_are_served_for_each_other_and_punjabi_scripts_are_not() {
assert_eq!(distance("sr-Latn", "sr-Cyrl"), 5);
assert_eq!(distance("sr-Cyrl", "sr-Latn"), 5);
assert_eq!(best(&["sr-Latn"], &["sr", "en"]), Some("sr"));
assert_eq!(best(&["sr"], &["sr-Latn", "en"]), Some("sr-Latn"));
assert_eq!(distance("sr-ME", "sr"), 9);
assert_eq!(distance("pa-Arab", "pa-Guru"), 54);
assert_eq!(distance("pa-Guru", "pa-Arab"), 54);
assert_eq!(best(&["pa-Arab"], &["pa-Guru"]), None);
assert_eq!(best(&["pa-Arab", "en"], &["pa-Guru", "en"]), Some("en"));
}
#[test]
fn spanish_regions_fall_back_as_the_owner_described() {
assert_eq!(distance("es-MX", "es"), 5);
assert_eq!(best(&["es-MX"], &["es", "en"]), Some("es"));
assert_eq!(distance("es-MX", "es-419"), 4);
assert_eq!(best(&["es-MX"], &["es", "es-419"]), Some("es-419"));
assert_eq!(distance("es", "es-MX"), 5);
assert_eq!(best(&["es"], &["es-MX", "en"]), Some("es-MX"));
assert_eq!(best(&["es-MX"], &["es", "es-MX"]), Some("es-MX"));
}
#[test]
fn other_languages_where_cldr_accepts_them() {
assert_eq!(distance("ca", "es"), 20);
assert_eq!(best(&["ca"], &["es"]), Some("es"));
assert_eq!(best(&["es"], &["ca"]), None);
assert_eq!(distance("yue", "zh-TW"), 14);
assert_eq!(best(&["yue"], &["zh"]), None);
assert_eq!(distance("pa", "en"), 44);
assert_eq!(distance("pa-PK", "en"), 84);
assert_eq!(best(&["nb"], &["no"]), Some("no"));
}
#[test]
fn the_threshold_is_below_fifty() {
assert_eq!(best(&["pa"], &["en"]), Some("en"));
assert_eq!(best(&["zh-Hant"], &["zh-Hans"]), None);
assert_eq!(best(&["pa-PK"], &["en"]), None);
}
#[test]
fn demotion_is_unbounded_so_an_eleventh_exact_match_is_refused() {
let others = ["fr", "de", "it", "es", "pt", "nl", "sv", "da", "nb", "pl"];
let supported = ["zh-Hant", "en"];
let mut list: Vec<&str> = others.iter().take(9).copied().collect();
list.push("zh-Hant");
assert_eq!(list.len(), 10);
assert_eq!(best(&list, &supported), Some("zh-Hant"));
let mut list: Vec<&str> = others.to_vec();
list.push("zh-Hant");
assert_eq!(list.len(), 11);
assert_eq!(best(&list, &supported), None);
let mut list: Vec<&str> = vec!["*", "C"];
list.extend(others.iter().take(9));
list.push("zh-Hant");
assert_eq!(best(&list, &supported), Some("zh-Hant"));
}
#[test]
fn a_tie_goes_to_the_earlier_pair_and_a_paradigm_wins_within_an_entry() {
assert_eq!(best(&["fr-BE"], &["fr-FR", "fr-CA"]), Some("fr-FR"));
assert_eq!(best(&["fr-BE"], &["fr-CA", "fr-FR"]), Some("fr-CA"));
assert_eq!(
best(&["en-US", "en-GB"], &["en-GB", "en-AU"]),
Some("en-GB")
);
assert_eq!(best(&["en-AU", "fr"], &["en-US", "fr"]), Some("en-US"));
assert_eq!(best(&["es-MX"], &["es-CR", "es-419"]), Some("es-419"));
assert_eq!(best(&["es-MX"], &["es-419", "es-CR"]), Some("es-419"));
}
#[test]
fn a_macroregion_is_inside_a_variable_when_all_its_contents_are() {
assert_eq!(distance("es-419", "es-MX"), 4);
assert_eq!(distance("es-419", "es"), 5);
assert_eq!(distance("en-001", "en-GB"), 3);
assert_eq!(distance("en-001", "en-US"), 5);
assert_eq!(best(&["en-001"], &["en", "en-GB"]), Some("en-GB"));
assert_eq!(distance("en-150", "en-GB"), 3);
}
#[test]
fn a_readers_und_is_not_maximized() {
assert_eq!(best(&["und"], &["en"]), None);
assert_ne!(distance("und", "en"), 0);
assert_eq!(best(&["und", "it"], &["en", "it"]), Some("it"));
assert_eq!(best(&["und-US"], &["en-US"]), None);
}
#[test]
fn a_tag_the_data_cannot_fill_keeps_its_empty_fields() {
assert_eq!(best(&["qaa"], &["en", "qaa"]), Some("qaa"));
assert_eq!(distance("qaa", "qaa-Latn"), 50);
assert_eq!(best(&["qaa-Latn"], &["qaa"]), None);
assert_eq!(best(&["abcdef"], &["abcdef", "en"]), Some("abcdef"));
}
#[test]
fn posix_names_are_read_as_tags() {
let locales = ["en", "fr-CA", "ar"];
assert_eq!(best(&["fr_CA.UTF-8"], &locales), Some("fr-CA"));
assert_eq!(best(&["FR_ca"], &locales), Some("fr-CA"));
assert_eq!(best(&["ar_EG.UTF-8@latin"], &locales), Some("ar"));
assert_eq!(best(&["en_US.ISO-8859-1"], &locales), Some("en"));
for tag in ["", "*", "C", "POSIX", "C.UTF-8", "posix"] {
assert_eq!(best(&[tag], &locales), None, "{tag:?}");
}
}
#[test]
fn what_follows_the_region_is_ignored() {
let locales = ["en", "fr-CA", "de"];
assert_eq!(best(&["fr-CA-x-private"], &locales), Some("fr-CA"));
assert_eq!(best(&["en-US-u-ca-gregory"], &locales), Some("en"));
assert_eq!(best(&["de-DE-1996"], &locales), Some("de"));
assert_eq!(best(&["zh-yue-HK"], &["zh-TW", "en"]), Some("zh-TW"));
}
#[test]
fn section_4_4_a_reader_of_several_languages() {
assert_eq!(
best(
&["en-US", "de", "fr", "gsw", "it"],
&["ja-JP", "de", "zh-TW"]
),
Some("de")
);
}
#[test]
fn section_4_4_the_opening_illustration_is_not_reproduced() {
assert_eq!(distance("zh", "zh-TW"), 54);
assert_eq!(best(&["zh"], &["ja-JP", "de", "zh-TW"]), None);
}
#[test]
fn section_4_4_demotion_of_a_later_entry() {
assert_eq!(best(&["de-AT", "fr"], &["de", "fr", "ja"]), Some("de"));
}
#[test]
fn section_4_4_und_is_not_filled_in() {
assert_eq!(best(&["und", "it"], &["en", "it"]), Some("it"));
}
#[test]
fn section_4_4_1_paradigm_and_cluster() {
assert_eq!(
best(&["en-SA"], &["en-GU", "en", "en-IN", "en-GB"]),
Some("en-GB")
);
assert_eq!(distance("en-SA", "en-GB"), 3);
assert_eq!(distance("en-SA", "en-IN"), 4);
}
#[test]
fn section_4_4_1_macroregions() {
assert_eq!(best(&["es-419"], &["es-MX", "es"]), Some("es-MX"));
assert_eq!(best(&["es-MX"], &["es-419", "es"]), Some("es-419"));
assert_eq!(best(&["es-MX"], &["es-CR", "es-419"]), Some("es-419"));
}
#[test]
fn section_4_2_a_fallback_from_a_list() {
assert_eq!(best(&["en", "fr"], &["fr-CA", "ru"]), Some("fr-CA"));
}
const NATIVE_1X: [&str; 6] = ["en", "fr-FR", "fr-CA", "ar", "zh-CN", "sr-Latn"];
#[test]
fn native_1x_cases_still_pass() {
assert_eq!(best(&["FR_ca"], &NATIVE_1X), Some("fr-CA"));
assert_eq!(best(&["fr_CA.UTF-8"], &NATIVE_1X), Some("fr-CA"));
assert_eq!(best(&["ar_EG.UTF-8@latin"], &NATIVE_1X), Some("ar"));
assert_eq!(best(&["fr-CA-x-private"], &NATIVE_1X), Some("fr-CA"));
assert_eq!(best(&["en-GB"], &NATIVE_1X), Some("en"));
assert_eq!(best(&["fr-BE"], &NATIVE_1X), Some("fr-FR"));
assert_eq!(best(&["zh-TW"], &NATIVE_1X), None);
assert_eq!(best(&["zh-CN"], &NATIVE_1X), Some("zh-CN"));
assert_eq!(best(&["sr-Latn-RS"], &NATIVE_1X), Some("sr-Latn"));
for tag in ["", "*", "C", "POSIX", "C.UTF-8"] {
assert_eq!(best(&[tag], &NATIVE_1X), None, "{tag}");
}
assert_eq!(best(&["de-DE", "fr-CA", "en"], &NATIVE_1X), Some("fr-CA"));
assert_eq!(best(&["de", "it"], &NATIVE_1X), None);
}
#[test]
fn web_1x_cases_still_pass() {
let locales = ["en", "fr-CA", "ar"];
assert_eq!(best(&["en"], &locales), Some("en"));
assert_eq!(best(&["EN-GB"], &locales), Some("en"));
assert_eq!(best(&["fr"], &locales), Some("fr-CA"));
assert_eq!(best(&["de"], &locales), None);
assert_eq!(best(&["*"], &locales), None);
let demo = ["ar", "en", "fr"];
assert_eq!(best(&["fr-CA"], &demo), Some("fr"));
assert_eq!(best(&["ar-EG"], &demo), Some("ar"));
assert_eq!(best(&["de-DE"], &demo), None);
}
#[test]
fn what_questions_11_and_15_change_from_1x() {
assert_eq!(best(&["sr-Cyrl"], &NATIVE_1X), Some("sr-Latn"));
assert_eq!(best(&["sr-RS"], &["sr-Latn"]), Some("sr-Latn"));
for tag in ["zh-Hant-TW", "zh-Hant", "zh-TW"] {
assert_eq!(best(&[tag], &["zh", "en"]), None, "{tag}");
}
assert_eq!(best(&["zh-TW"], &["zh-CN"]), None);
assert_eq!(best(&["pa-PK"], &["pa"]), None);
assert_eq!(best(&["uz-AF"], &["uz"]), None);
assert_eq!(best(&["en-AU"], &["en-US", "en-GB"]), Some("en-GB"));
}
fn cut(locales: &[&str]) -> &'static LanguageMatching {
let e = mf2_locale_data::matching::Matching::shipped()
.expect("the shipped table")
.cut(locales)
.encode()
.expect("encodes");
let variables: Vec<&'static [u16]> = e.variables.into_iter().map(|v| &*v.leak()).collect();
Box::leak(Box::new(LanguageMatching::new(
e.scripts.leak(),
e.regions.leak(),
e.languages.leak(),
e.language_values.leak(),
e.tags.leak(),
e.tag_values.leak(),
e.language_pairs.leak(),
e.language_distances.leak(),
e.script_pairs.leak(),
e.region_rules.leak(),
variables.leak(),
e.paradigms.leak(),
e.defaults,
)))
}
fn readers() -> Vec<String> {
let table = mf2_locale_data::matching::Matching::shipped().expect("the shipped table");
let mut out: Vec<String> = table.likely.keys().cloned().collect();
for tag in [
"en-001",
"en-150",
"en-SA",
"en-GU",
"es-419",
"es-CR",
"fr-BE",
"zh-Hant",
"zh-Hans",
"zh-HK",
"sr-Latn",
"sr-Cyrl",
"pa-Arab",
"und",
"und-US",
"qaa",
"de-AT",
"gsw",
"ar-Latn",
"hi-Latn",
"ja-Hira",
"ko-Hang",
"fr_CA.UTF-8",
"C",
"*",
"",
] {
out.push(tag.to_owned());
}
for language in [
"en", "es", "pt", "fr", "ar", "zh", "sr", "de", "pl", "cy", "ja",
] {
for region in [
"US", "GB", "MX", "419", "BR", "PT", "FR", "CA", "EG", "MA", "TW", "CN",
] {
out.push(format!("{language}-{region}"));
}
for script in ["Latn", "Cyrl", "Arab", "Hant", "Hans"] {
out.push(format!("{language}-{script}"));
}
}
out
}
#[test]
fn a_corpus_cut_gives_every_reader_what_the_whole_table_gives() {
let corpora: [&[&str]; 8] = [
&["ar", "en", "fr"],
&["de", "en", "es", "fr"],
&[
"en", "es", "de", "fr", "ar", "he", "ja", "hi", "ru", "pl", "cy",
],
&["en", "pl", "en-XA", "ar-XB"],
&["en", "zh", "zh-TW", "zh-HK", "yue"],
&["sr", "sr-Latn", "hr", "bs"],
&["en", "en-GB", "es", "es-419", "es-MX", "pt-BR", "pt-PT"],
&["pa", "pa-Arab", "ur", "hi"],
];
let readers = readers();
let whole = cldr();
for corpus in corpora {
let part = cut(corpus);
let table: Vec<(&str, Dir)> = corpus.iter().map(|t| (*t, Dir::Ltr)).collect();
for reader in &readers {
assert_eq!(
whole.best_match([reader.as_str()], &table),
part.best_match([reader.as_str()], &table),
"{reader} against {corpus:?}"
);
for own in corpus {
let (w, p) = (
whole.distance_of(reader, own),
part.distance_of(reader, own),
);
assert!(
w == p || w.zip(p).is_some_and(|(w, p)| w >= 50 && p >= 50),
"{reader} → {own} in {corpus:?}: {w:?} whole, {p:?} cut"
);
}
}
for window in readers.windows(4).step_by(7) {
let list = window.iter().map(String::as_str);
assert_eq!(
whole.best_match(list.clone(), &table),
part.best_match(list, &table),
"{window:?} against {corpus:?}"
);
}
}
}
#[test]
fn a_cut_is_small() {
let e = mf2_locale_data::matching::Matching::shipped()
.expect("the shipped table")
.cut(&["ar", "en", "fr"])
.encode()
.expect("encodes");
assert!(
e.languages.len() + e.tags.len() < 150,
"{}",
e.languages.len()
);
assert!(e.language_pairs.len() < 150, "{}", e.language_pairs.len());
}