pub mod parser;
pub mod subtitle;
pub use parser::parse_languages;
pub use subtitle::{
SubtitleTitleInfo, parse_basic_subtitle, parse_language_tags, parse_subtitle_language,
parse_subtitle_language_information,
};
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum Language {
#[default]
Unknown = 0,
English = 1,
French = 2,
Spanish = 3,
German = 4,
Italian = 5,
Danish = 6,
Dutch = 7,
Japanese = 8,
Icelandic = 9,
Chinese = 10,
Russian = 11,
Polish = 12,
Vietnamese = 13,
Swedish = 14,
Norwegian = 15,
Finnish = 16,
Turkish = 17,
Portuguese = 18,
Flemish = 19,
Greek = 20,
Korean = 21,
Hungarian = 22,
Hebrew = 23,
Lithuanian = 24,
Czech = 25,
Arabic = 26,
Hindi = 27,
Bulgarian = 28,
Malayalam = 29,
Ukrainian = 30,
Slovak = 31,
Thai = 32,
PortugueseBrazil = 33,
SpanishLatino = 34,
Romanian = 35,
Latvian = 36,
Persian = 37,
Catalan = 38,
Croatian = 39,
Serbian = 40,
Bosnian = 41,
Estonian = 42,
Tamil = 43,
Indonesian = 44,
Macedonian = 45,
Slovenian = 46,
Original = -2,
}
impl From<Language> for i32 {
fn from(lang: Language) -> i32 {
lang as i32
}
}
impl Language {
pub fn name(self) -> &'static str {
match self {
Language::Unknown => "Unknown",
Language::English => "English",
Language::French => "French",
Language::Spanish => "Spanish",
Language::German => "German",
Language::Italian => "Italian",
Language::Danish => "Danish",
Language::Dutch => "Dutch",
Language::Japanese => "Japanese",
Language::Icelandic => "Icelandic",
Language::Chinese => "Chinese",
Language::Russian => "Russian",
Language::Polish => "Polish",
Language::Vietnamese => "Vietnamese",
Language::Swedish => "Swedish",
Language::Norwegian => "Norwegian",
Language::Finnish => "Finnish",
Language::Turkish => "Turkish",
Language::Portuguese => "Portuguese",
Language::Flemish => "Flemish",
Language::Greek => "Greek",
Language::Korean => "Korean",
Language::Hungarian => "Hungarian",
Language::Hebrew => "Hebrew",
Language::Lithuanian => "Lithuanian",
Language::Czech => "Czech",
Language::Arabic => "Arabic",
Language::Hindi => "Hindi",
Language::Bulgarian => "Bulgarian",
Language::Malayalam => "Malayalam",
Language::Ukrainian => "Ukrainian",
Language::Slovak => "Slovak",
Language::Thai => "Thai",
Language::PortugueseBrazil => "Portuguese (Brazil)",
Language::SpanishLatino => "Spanish (Latino)",
Language::Romanian => "Romanian",
Language::Latvian => "Latvian",
Language::Persian => "Persian",
Language::Catalan => "Catalan",
Language::Croatian => "Croatian",
Language::Serbian => "Serbian",
Language::Bosnian => "Bosnian",
Language::Estonian => "Estonian",
Language::Tamil => "Tamil",
Language::Indonesian => "Indonesian",
Language::Macedonian => "Macedonian",
Language::Slovenian => "Slovenian",
Language::Original => "Original",
}
}
}
pub mod iso_languages {
use super::Language;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct IsoLanguage {
pub two_letter_code: &'static str,
pub country_code: &'static str,
pub three_letter_code: &'static str,
pub language: Language,
}
const ALL: &[IsoLanguage] = &[
IsoLanguage {
two_letter_code: "en",
country_code: "",
three_letter_code: "eng",
language: Language::English,
},
IsoLanguage {
two_letter_code: "fr",
country_code: "fr",
three_letter_code: "fra",
language: Language::French,
},
IsoLanguage {
two_letter_code: "es",
country_code: "",
three_letter_code: "spa",
language: Language::Spanish,
},
IsoLanguage {
two_letter_code: "de",
country_code: "de",
three_letter_code: "deu",
language: Language::German,
},
IsoLanguage {
two_letter_code: "it",
country_code: "",
three_letter_code: "ita",
language: Language::Italian,
},
IsoLanguage {
two_letter_code: "da",
country_code: "",
three_letter_code: "dan",
language: Language::Danish,
},
IsoLanguage {
two_letter_code: "nl",
country_code: "",
three_letter_code: "nld",
language: Language::Dutch,
},
IsoLanguage {
two_letter_code: "ja",
country_code: "",
three_letter_code: "jpn",
language: Language::Japanese,
},
IsoLanguage {
two_letter_code: "is",
country_code: "",
three_letter_code: "isl",
language: Language::Icelandic,
},
IsoLanguage {
two_letter_code: "zh",
country_code: "cn",
three_letter_code: "zho",
language: Language::Chinese,
},
IsoLanguage {
two_letter_code: "ru",
country_code: "",
three_letter_code: "rus",
language: Language::Russian,
},
IsoLanguage {
two_letter_code: "pl",
country_code: "",
three_letter_code: "pol",
language: Language::Polish,
},
IsoLanguage {
two_letter_code: "vi",
country_code: "",
three_letter_code: "vie",
language: Language::Vietnamese,
},
IsoLanguage {
two_letter_code: "sv",
country_code: "",
three_letter_code: "swe",
language: Language::Swedish,
},
IsoLanguage {
two_letter_code: "no",
country_code: "",
three_letter_code: "nor",
language: Language::Norwegian,
},
IsoLanguage {
two_letter_code: "nb",
country_code: "",
three_letter_code: "nob",
language: Language::Norwegian,
},
IsoLanguage {
two_letter_code: "fi",
country_code: "",
three_letter_code: "fin",
language: Language::Finnish,
},
IsoLanguage {
two_letter_code: "tr",
country_code: "",
three_letter_code: "tur",
language: Language::Turkish,
},
IsoLanguage {
two_letter_code: "pt",
country_code: "pt",
three_letter_code: "por",
language: Language::Portuguese,
},
IsoLanguage {
two_letter_code: "nl",
country_code: "",
three_letter_code: "nld",
language: Language::Flemish,
},
IsoLanguage {
two_letter_code: "el",
country_code: "",
three_letter_code: "ell",
language: Language::Greek,
},
IsoLanguage {
two_letter_code: "ko",
country_code: "",
three_letter_code: "kor",
language: Language::Korean,
},
IsoLanguage {
two_letter_code: "hu",
country_code: "",
three_letter_code: "hun",
language: Language::Hungarian,
},
IsoLanguage {
two_letter_code: "he",
country_code: "",
three_letter_code: "heb",
language: Language::Hebrew,
},
IsoLanguage {
two_letter_code: "lt",
country_code: "",
three_letter_code: "lit",
language: Language::Lithuanian,
},
IsoLanguage {
two_letter_code: "cs",
country_code: "",
three_letter_code: "ces",
language: Language::Czech,
},
IsoLanguage {
two_letter_code: "ar",
country_code: "",
three_letter_code: "ara",
language: Language::Arabic,
},
IsoLanguage {
two_letter_code: "hi",
country_code: "",
three_letter_code: "hin",
language: Language::Hindi,
},
IsoLanguage {
two_letter_code: "bg",
country_code: "",
three_letter_code: "bul",
language: Language::Bulgarian,
},
IsoLanguage {
two_letter_code: "ml",
country_code: "",
three_letter_code: "mal",
language: Language::Malayalam,
},
IsoLanguage {
two_letter_code: "uk",
country_code: "",
three_letter_code: "ukr",
language: Language::Ukrainian,
},
IsoLanguage {
two_letter_code: "sk",
country_code: "",
three_letter_code: "slk",
language: Language::Slovak,
},
IsoLanguage {
two_letter_code: "th",
country_code: "th",
three_letter_code: "tha",
language: Language::Thai,
},
IsoLanguage {
two_letter_code: "pt",
country_code: "br",
three_letter_code: "por",
language: Language::PortugueseBrazil,
},
IsoLanguage {
two_letter_code: "es",
country_code: "mx",
three_letter_code: "spa",
language: Language::SpanishLatino,
},
IsoLanguage {
two_letter_code: "ro",
country_code: "",
three_letter_code: "ron",
language: Language::Romanian,
},
IsoLanguage {
two_letter_code: "lv",
country_code: "",
three_letter_code: "lav",
language: Language::Latvian,
},
IsoLanguage {
two_letter_code: "fa",
country_code: "",
three_letter_code: "fas",
language: Language::Persian,
},
IsoLanguage {
two_letter_code: "ca",
country_code: "",
three_letter_code: "cat",
language: Language::Catalan,
},
IsoLanguage {
two_letter_code: "hr",
country_code: "",
three_letter_code: "hrv",
language: Language::Croatian,
},
IsoLanguage {
two_letter_code: "sr",
country_code: "",
three_letter_code: "srp",
language: Language::Serbian,
},
IsoLanguage {
two_letter_code: "bs",
country_code: "",
three_letter_code: "bos",
language: Language::Bosnian,
},
IsoLanguage {
two_letter_code: "et",
country_code: "",
three_letter_code: "est",
language: Language::Estonian,
},
IsoLanguage {
two_letter_code: "ta",
country_code: "",
three_letter_code: "tam",
language: Language::Tamil,
},
IsoLanguage {
two_letter_code: "id",
country_code: "",
three_letter_code: "ind",
language: Language::Indonesian,
},
IsoLanguage {
two_letter_code: "mk",
country_code: "",
three_letter_code: "mkd",
language: Language::Macedonian,
},
IsoLanguage {
two_letter_code: "sl",
country_code: "",
three_letter_code: "slv",
language: Language::Slovenian,
},
];
const ALTERNATE: &[(&str, Language)] = &[
("cze", Language::Czech),
("dut", Language::Dutch),
("mac", Language::Macedonian),
("rum", Language::Romanian),
("yue", Language::Chinese),
("zhtw", Language::Chinese),
];
const B_TO_T_MAP: &[(&str, &str)] = &[
("alb", "sqi"),
("arm", "hye"),
("baq", "eus"),
("bur", "mya"),
("chi", "zho"),
("cze", "ces"),
("dut", "nld"),
("fre", "fra"),
("geo", "kat"),
("ger", "deu"),
("gre", "ell"),
("ice", "isl"),
("mac", "mkd"),
("mao", "mri"),
("may", "msa"),
("per", "fas"),
("rum", "ron"),
("slo", "slk"),
("tib", "bod"),
("wel", "cym"),
];
pub fn find(code: &str) -> Option<Language> {
find_iso(code).map(|iso| iso.language)
}
pub fn find_iso(code: &str) -> Option<IsoLanguage> {
let lowered = code.to_ascii_lowercase();
let (lang_code, country_code) = match lowered.split_once('-') {
Some((l, c)) => (l, Some(c)),
None => (lowered.as_str(), None),
};
match lang_code.len() {
2 => find_two_letter(lang_code, country_code),
3 => {
if let Some(lang) = alternate_lookup(&lowered) {
return canonical_row(lang);
}
find_three_letter(lang_code)
}
_ => alternate_lookup(&lowered).and_then(canonical_row),
}
}
fn find_two_letter(lang_code: &str, country_code: Option<&str>) -> Option<IsoLanguage> {
let matches: Vec<&IsoLanguage> = ALL
.iter()
.filter(|l| l.two_letter_code == lang_code)
.collect();
if matches.is_empty() {
return None;
}
match country_code {
None => matches.first().copied().copied(),
Some(country) => {
let any_country_match = matches.iter().any(|l| l.country_code == country);
if any_country_match {
matches
.iter()
.find(|l| l.country_code == country)
.copied()
.copied()
} else {
matches
.iter()
.find(|l| l.country_code.is_empty())
.copied()
.copied()
}
}
}
}
fn find_three_letter(lang_code: &str) -> Option<IsoLanguage> {
let mapped = B_TO_T_MAP
.iter()
.find_map(|(b, t)| (*b == lang_code).then_some(*t))
.unwrap_or(lang_code);
ALL.iter().find(|l| l.three_letter_code == mapped).copied()
}
fn alternate_lookup(lowered: &str) -> Option<Language> {
ALTERNATE
.iter()
.find_map(|(code, lang)| (*code == lowered).then_some(*lang))
}
fn canonical_row(language: Language) -> Option<IsoLanguage> {
ALL.iter().find(|l| l.language == language).copied()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn language_constants_have_expected_ids() {
assert_eq!(Language::Unknown as i32, 0);
assert_eq!(Language::English as i32, 1);
assert_eq!(Language::SpanishLatino as i32, 34);
assert_eq!(Language::Slovenian as i32, 46);
assert_eq!(Language::Original as i32, -2);
}
#[test]
fn iso_languages_finds_eng() {
assert_eq!(iso_languages::find("eng"), Some(Language::English));
}
#[test]
fn iso_languages_finds_fre() {
assert_eq!(iso_languages::find("fre"), Some(Language::French));
}
#[test]
fn iso_languages_unknown_for_garbage() {
assert_eq!(iso_languages::find("zzz"), None);
}
#[test]
fn english_resolves_via_two_letter_three_letter_and_locale() {
for code in ["en", "eng", "en-US", "en-GB"] {
assert_eq!(
iso_languages::find(code),
Some(Language::English),
"code = {code}"
);
}
}
#[test]
fn unknown_or_invalid_codes_return_none() {
for code in ["enus", "enusa", "wo", "fr-CA"] {
assert_eq!(iso_languages::find(code), None, "code = {code}");
}
}
#[test]
fn portuguese_resolves_via_two_letter_three_letter_and_locale() {
for code in ["pt", "por", "pt-PT"] {
assert_eq!(
iso_languages::find(code),
Some(Language::Portuguese),
"code = {code}"
);
}
}
#[test]
fn de_au_returns_none_because_german_requires_de_country_code() {
assert_eq!(iso_languages::find("de-AU"), None);
}
#[test]
fn cze_resolves_via_alternate_iso_code_mapping() {
assert_eq!(iso_languages::find("cze"), Some(Language::Czech));
}
#[test]
fn pt_br_resolves_to_portuguese_brazil() {
assert_eq!(
iso_languages::find("pt-br"),
Some(Language::PortugueseBrazil)
);
assert_eq!(
iso_languages::find("pt-BR"),
Some(Language::PortugueseBrazil)
);
}
#[test]
fn es_mx_resolves_to_spanish_latino_but_bare_es_to_spanish() {
assert_eq!(iso_languages::find("es"), Some(Language::Spanish));
assert_eq!(iso_languages::find("es-mx"), Some(Language::SpanishLatino));
}
#[test]
fn bare_nl_resolves_to_dutch_not_flemish() {
assert_eq!(iso_languages::find("nl"), Some(Language::Dutch));
assert_eq!(iso_languages::find("nld"), Some(Language::Dutch));
}
#[test]
fn b_to_t_map_resolves_old_ger_form_to_german() {
assert_eq!(iso_languages::find("ger"), Some(Language::German));
assert_eq!(iso_languages::find("deu"), Some(Language::German));
}
#[test]
fn alternate_zhtw_resolves_to_chinese() {
assert_eq!(iso_languages::find("zhtw"), Some(Language::Chinese));
}
#[test]
fn nb_two_letter_resolves_to_norwegian() {
assert_eq!(iso_languages::find("nb"), Some(Language::Norwegian));
assert_eq!(iso_languages::find("nob"), Some(Language::Norwegian));
}
#[test]
fn case_is_normalised() {
assert_eq!(iso_languages::find("ENG"), Some(Language::English));
assert_eq!(iso_languages::find("Fr"), Some(Language::French));
}
#[test]
fn empty_and_oversized_codes_return_none() {
assert_eq!(iso_languages::find(""), None);
assert_eq!(iso_languages::find("english"), None);
}
#[test]
fn language_default_is_unknown() {
assert_eq!(Language::default(), Language::Unknown);
}
#[test]
fn language_name_round_trips_for_brazil_and_latino() {
assert_eq!(Language::PortugueseBrazil.name(), "Portuguese (Brazil)");
assert_eq!(Language::SpanishLatino.name(), "Spanish (Latino)");
assert_eq!(Language::Original.name(), "Original");
}
#[test]
fn find_iso_returns_full_record_for_canonical_codes() {
let iso = iso_languages::find_iso("pt-br").expect("pt-br must resolve");
assert_eq!(iso.language, Language::PortugueseBrazil);
assert_eq!(iso.two_letter_code, "pt");
assert_eq!(iso.country_code, "br");
assert_eq!(iso.three_letter_code, "por");
}
#[test]
fn find_iso_alternate_hit_returns_canonical_row_not_synthetic() {
let cze = iso_languages::find_iso("cze").expect("alternate hit");
assert_eq!(cze.language, Language::Czech);
assert_eq!(cze.three_letter_code, "ces"); assert_eq!(cze.two_letter_code, "cs"); }
#[test]
fn from_language_to_i32_preserves_id() {
assert_eq!(i32::from(Language::English), 1);
assert_eq!(i32::from(Language::Original), -2);
assert_eq!(i32::from(Language::Slovenian), 46);
}
#[test]
fn enum_has_exactly_48_variants_with_contiguous_ids() {
let all: &[Language] = &[
Language::Unknown,
Language::English,
Language::French,
Language::Spanish,
Language::German,
Language::Italian,
Language::Danish,
Language::Dutch,
Language::Japanese,
Language::Icelandic,
Language::Chinese,
Language::Russian,
Language::Polish,
Language::Vietnamese,
Language::Swedish,
Language::Norwegian,
Language::Finnish,
Language::Turkish,
Language::Portuguese,
Language::Flemish,
Language::Greek,
Language::Korean,
Language::Hungarian,
Language::Hebrew,
Language::Lithuanian,
Language::Czech,
Language::Arabic,
Language::Hindi,
Language::Bulgarian,
Language::Malayalam,
Language::Ukrainian,
Language::Slovak,
Language::Thai,
Language::PortugueseBrazil,
Language::SpanishLatino,
Language::Romanian,
Language::Latvian,
Language::Persian,
Language::Catalan,
Language::Croatian,
Language::Serbian,
Language::Bosnian,
Language::Estonian,
Language::Tamil,
Language::Indonesian,
Language::Macedonian,
Language::Slovenian,
Language::Original,
];
assert_eq!(all.len(), 48);
for (idx, lang) in all.iter().enumerate() {
if idx < 47 {
assert_eq!(*lang as i32, idx as i32, "variant at index {idx}");
} else {
assert_eq!(*lang as i32, -2, "Original sentinel last");
}
}
}
}