mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-04 08:56:59 +01:00
The crawl needs it to read a subtitle file's language from its name (en, eng, English), and pstr-player re-exports it unchanged.
127 lines
4.7 KiB
Rust
127 lines
4.7 KiB
Rust
//! Language tags: what a container or a file name calls a language, and what a
|
|
//! viewer calls it.
|
|
|
|
/// The languages offered by name where a preference is chosen ahead of any
|
|
/// file, as the three-letter tag a Matroska file carries. The same list
|
|
/// [`language_name`] knows, and the phones' `LANGUAGES`; mpv treats the two-
|
|
/// and three-letter forms as one.
|
|
pub const LANGUAGES: [&str; 29] = [
|
|
"jpn", "eng", "ger", "fre", "spa", "ita", "por", "dut", "rus", "pol", "swe", "dan", "nor",
|
|
"fin", "cze", "hun", "tur", "ara", "heb", "hin", "kor", "chi", "tha", "vie", "ukr", "rum",
|
|
"gre", "ind", "may",
|
|
];
|
|
|
|
/// Each language's name and the tags it goes by, the [`LANGUAGES`] form first.
|
|
///
|
|
/// Deliberately a short list rather than a full ISO 639 table: it covers what
|
|
/// the shares this app is pointed at actually contain, and an unrecognised tag
|
|
/// still shows as something a viewer can pick between — `"por-BR"` reads fine
|
|
/// on its own.
|
|
const TABLE: [(&str, &[&str]); 29] = [
|
|
("English", &["eng", "en"]),
|
|
("Japanese", &["jpn", "ja"]),
|
|
("German", &["ger", "de", "deu"]),
|
|
("French", &["fre", "fr", "fra"]),
|
|
("Spanish", &["spa", "es"]),
|
|
("Italian", &["ita", "it"]),
|
|
("Portuguese", &["por", "pt"]),
|
|
("Dutch", &["dut", "nl", "nld"]),
|
|
("Russian", &["rus", "ru"]),
|
|
("Polish", &["pol", "pl"]),
|
|
("Swedish", &["swe", "sv"]),
|
|
("Danish", &["dan", "da"]),
|
|
("Norwegian", &["nor", "no"]),
|
|
("Finnish", &["fin", "fi"]),
|
|
("Czech", &["cze", "cs", "ces"]),
|
|
("Hungarian", &["hun", "hu"]),
|
|
("Turkish", &["tur", "tr"]),
|
|
("Arabic", &["ara", "ar"]),
|
|
("Hebrew", &["heb", "he"]),
|
|
("Hindi", &["hin", "hi"]),
|
|
("Korean", &["kor", "ko"]),
|
|
("Chinese", &["chi", "zh", "zho"]),
|
|
("Thai", &["tha", "th"]),
|
|
("Vietnamese", &["vie", "vi"]),
|
|
("Ukrainian", &["ukr", "uk"]),
|
|
("Romanian", &["rum", "ro", "ron"]),
|
|
("Greek", &["gre", "el", "ell"]),
|
|
("Indonesian", &["ind", "id"]),
|
|
("Malay", &["may", "ms", "msa"]),
|
|
];
|
|
|
|
/// A readable name for a language tag, falling back to the tag itself.
|
|
pub fn language_name(tag: &str) -> String {
|
|
// Container tags come in both the two- and three-letter forms, and often
|
|
// with a region suffix: "en", "eng", "en-US", "pt-BR".
|
|
let base = tag.split(['-', '_']).next().unwrap_or(tag);
|
|
let Some((name, _)) = TABLE
|
|
.iter()
|
|
.find(|(_, tags)| tags.iter().any(|known| known.eq_ignore_ascii_case(base)))
|
|
else {
|
|
return tag.to_string();
|
|
};
|
|
|
|
// The region matters when there is one — Brazilian Portuguese is not
|
|
// Portuguese to anyone choosing between the two.
|
|
match tag.split_once(['-', '_']) {
|
|
Some((_, region)) if !region.is_empty() => {
|
|
format!("{name} ({})", region.to_ascii_uppercase())
|
|
}
|
|
_ => (*name).to_string(),
|
|
}
|
|
}
|
|
|
|
/// Whether two container tags name the same language: "en" and "eng" do.
|
|
pub fn same_language(a: &str, b: &str) -> bool {
|
|
a.eq_ignore_ascii_case(b) || language_name(a) == language_name(b)
|
|
}
|
|
|
|
/// The [`LANGUAGES`] tag for one word of a file name, if it names a language:
|
|
/// "en", "eng" and "English" are all `"eng"`.
|
|
///
|
|
/// For the words around a subtitle file's name — `Episode 01.en.srt`,
|
|
/// `2_English.srt` — where a two-letter word is far more often a language than
|
|
/// anything else.
|
|
pub fn language_tag(word: &str) -> Option<&'static str> {
|
|
TABLE
|
|
.iter()
|
|
.find(|(name, tags)| {
|
|
name.eq_ignore_ascii_case(word) || tags.iter().any(|tag| tag.eq_ignore_ascii_case(word))
|
|
})
|
|
.map(|(_, tags)| tags[0])
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn language_tags_resolve_in_both_iso_forms_and_keep_their_region() {
|
|
assert_eq!(language_name("jpn"), "Japanese");
|
|
assert_eq!(language_name("ja"), "Japanese");
|
|
assert_eq!(language_name("pt-BR"), "Portuguese (BR)");
|
|
// An unknown tag is shown as it stands rather than as "Unknown", which
|
|
// would make two of them indistinguishable.
|
|
assert_eq!(language_name("mis"), "mis");
|
|
}
|
|
|
|
#[test]
|
|
fn a_language_named_by_either_tag_or_by_its_name_has_the_tag_a_preference_uses() {
|
|
assert_eq!(language_tag("en"), Some("eng"));
|
|
assert_eq!(language_tag("ENG"), Some("eng"));
|
|
assert_eq!(language_tag("German"), Some("ger"));
|
|
assert_eq!(language_tag("deu"), Some("ger"));
|
|
assert_eq!(language_tag("forced"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn every_language_offered_by_name_is_one_the_table_knows() {
|
|
let firsts: Vec<&str> = TABLE.iter().map(|(_, tags)| tags[0]).collect();
|
|
let mut offered = LANGUAGES.to_vec();
|
|
let mut known = firsts.clone();
|
|
offered.sort_unstable();
|
|
known.sort_unstable();
|
|
assert_eq!(offered, known);
|
|
}
|
|
}
|