"""Language tag data — ISO 639-1 to all known file/metadata variant mappings. Extracted from config.py for size management. config.py re-exports everything from this module for backwards compatibility. """ # Language tag mapping (ISO 639-1 -> all known file/metadata variants) _LANGUAGE_TAGS: dict[str, set[str]] = { # Western Europe "de": {"de", "deu", "ger", "german"}, "en": {"en", "eng", "english"}, "fr": {"fr", "fra", "fre", "french"}, "es": {"es", "spa", "spanish"}, "it": {"it", "ita", "italian"}, # "pob"/"pb" are the Bazarr/OpenSubtitles conventions for pt-BR. "pt": {"pt", "por", "portuguese", "pt-br", "pob", "pb"}, "nl": {"nl", "nld", "dut", "dutch"}, "sv": {"sv", "swe", "swedish"}, "da": {"da", "dan", "danish"}, # Bokmål (nb/nob) and Nynorsk (nn/nno) tags are common in real releases # and must map onto the generic Norwegian target. "no": {"no", "nor", "norwegian", "nb", "nob", "nn", "nno"}, "fi": {"fi", "fin", "finnish"}, "is": {"is", "isl", "ice", "icelandic"}, "eu": {"eu", "eus", "baq", "basque"}, "ca": {"ca", "cat", "catalan"}, "gl": {"gl", "glg", "galician"}, # Eastern Europe "pl": {"pl", "pol", "polish"}, "cs": {"cs", "ces", "cze", "czech"}, "sk": {"sk", "slk", "slo", "slovak"}, "hu": {"hu", "hun", "hungarian"}, "ro": {"ro", "ron", "rum", "romanian"}, "bg": {"bg", "bul", "bulgarian"}, "hr": {"hr", "hrv", "croatian"}, "sr": {"sr", "srp", "serbian"}, "sl": {"sl", "slv", "slovenian"}, "bs": {"bs", "bos", "bosnian"}, "mk": {"mk", "mkd", "macedonian"}, "sq": {"sq", "alb", "sqi", "albanian"}, "lt": {"lt", "lit", "lithuanian"}, "lv": {"lv", "lav", "latvian"}, "et": {"et", "est", "estonian"}, "uk": {"uk", "ukr", "ukrainian"}, "ru": {"ru", "rus", "russian"}, # Caucasus / Central Asia "hy": {"hy", "hye", "arm", "armenian"}, "ka": {"ka", "kat", "geo", "georgian"}, "az": {"az", "aze", "azerbaijani"}, "kk": {"kk", "kaz", "kazakh"}, "uz": {"uz", "uzb", "uzbek"}, # Middle East "ar": {"ar", "ara", "arabic"}, "he": {"he", "heb", "hebrew"}, "fa": {"fa", "per", "fas", "persian"}, "tr": {"tr", "tur", "turkish"}, # South / Southeast Asia "hi": {"hi", "hin", "hindi"}, "bn": {"bn", "ben", "bengali"}, "ur": {"ur", "urd", "urdu"}, "ta": {"ta", "tam", "tamil"}, "te": {"te", "tel", "telugu"}, "ml": {"ml", "mal", "malayalam"}, "kn": {"kn", "kan", "kannada"}, "si": {"si", "sin", "sinhala"}, "th": {"th", "tha", "thai"}, "vi": {"vi", "vie", "vietnamese"}, "id": {"id", "ind", "indonesian"}, "ms": {"ms", "msa", "may", "malay"}, "tl": {"tl", "fil", "tagalog", "filipino"}, # East Asia "ja": {"ja", "jpn", "japanese"}, "ko": {"ko", "kor", "korean"}, "zh": {"zh", "zho", "chi", "chinese"}, "zh-hans": {"zh-hans", "zhs", "chs", "chi-sim", "chinese simplified"}, "zh-hant": {"zh-hant", "zht", "cht", "chi-tra", "chinese traditional"}, "mn": {"mn", "mon", "mongolian"}, # Other "el": {"el", "ell", "gre", "greek"}, "af": {"af", "afr", "afrikaans"}, "sw": {"sw", "swa", "swahili"}, } # Ordered list of supported languages for the UI language picker _CORE_LANGUAGES: list[dict] = [ {"code": "af", "name": "Afrikaans"}, {"code": "sq", "name": "Albanian"}, {"code": "ar", "name": "Arabic"}, {"code": "hy", "name": "Armenian"}, {"code": "az", "name": "Azerbaijani"}, {"code": "eu", "name": "Basque"}, {"code": "bn", "name": "Bengali"}, {"code": "bs", "name": "Bosnian"}, {"code": "bg", "name": "Bulgarian"}, {"code": "ca", "name": "Catalan"}, {"code": "zh", "name": "Chinese"}, {"code": "zh-hans", "name": "Chinese (Simplified)"}, {"code": "zh-hant", "name": "Chinese (Traditional)"}, {"code": "hr", "name": "Croatian"}, {"code": "cs", "name": "Czech"}, {"code": "da", "name": "Danish"}, {"code": "nl", "name": "Dutch"}, {"code": "en", "name": "English"}, {"code": "et", "name": "Estonian"}, {"code": "tl", "name": "Filipino"}, {"code": "fi", "name": "Finnish"}, {"code": "fr", "name": "French"}, {"code": "gl", "name": "Galician"}, {"code": "ka", "name": "Georgian"}, {"code": "de", "name": "German"}, {"code": "el", "name": "Greek"}, {"code": "he", "name": "Hebrew"}, {"code": "hi", "name": "Hindi"}, {"code": "hu", "name": "Hungarian"}, {"code": "is", "name": "Icelandic"}, {"code": "id", "name": "Indonesian"}, {"code": "it", "name": "Italian"}, {"code": "ja", "name": "Japanese"}, {"code": "kn", "name": "Kannada"}, {"code": "kk", "name": "Kazakh"}, {"code": "ko", "name": "Korean"}, {"code": "lv", "name": "Latvian"}, {"code": "lt", "name": "Lithuanian"}, {"code": "mk", "name": "Macedonian"}, {"code": "ms", "name": "Malay"}, {"code": "ml", "name": "Malayalam"}, {"code": "mn", "name": "Mongolian"}, {"code": "no", "name": "Norwegian"}, {"code": "fa", "name": "Persian"}, {"code": "pl", "name": "Polish"}, {"code": "pt", "name": "Portuguese"}, {"code": "ro", "name": "Romanian"}, {"code": "ru", "name": "Russian"}, {"code": "sr", "name": "Serbian"}, {"code": "si", "name": "Sinhala"}, {"code": "sk", "name": "Slovak"}, {"code": "sl", "name": "Slovenian"}, {"code": "es", "name": "Spanish"}, {"code": "sw", "name": "Swahili"}, {"code": "sv", "name": "Swedish"}, {"code": "ta", "name": "Tamil"}, {"code": "te", "name": "Telugu"}, {"code": "th", "name": "Thai"}, {"code": "tr", "name": "Turkish"}, {"code": "uk", "name": "Ukrainian"}, {"code": "ur", "name": "Urdu"}, {"code": "uz", "name": "Uzbek"}, {"code": "vi", "name": "Vietnamese"}, ] def _with_extra_languages() -> list[dict]: """Core languages plus every other ISO 639-1 language, each with a German name. New languages get tags too (code, both three-letter codes, English name), so a track tagged ``bel`` resolves to ``be`` like ``ger`` resolves to ``de``. A tag another language already owns is never taken over. """ from config_language_names import EXTRA_LANGUAGES, GERMAN_NAMES # "lat" names Latin in ISO 639-2 but Latin-American Spanish in common # subtitle naming ("Movie.lat.srt"); claiming it for Latin would let the # sidecar cleanup trash those files. It stays unknown, as before. taken = {tag for tags in _LANGUAGE_TAGS.values() for tag in tags} | {"lat"} entries = [ {**entry, "name_de": GERMAN_NAMES.get(entry["code"], entry["name"])} for entry in _CORE_LANGUAGES ] for code, alpha3, alpha3b, name_en, name_de in EXTRA_LANGUAGES: if code in _LANGUAGE_TAGS: continue tags = {code} | ({alpha3, alpha3b, name_en.lower()} - taken) _LANGUAGE_TAGS[code] = tags taken |= tags entries.append({"code": code, "name": name_en, "name_de": name_de}) return sorted(entries, key=lambda entry: entry["name"]) SUPPORTED_LANGUAGES: list[dict] = _with_extra_languages() def _get_language_tags(lang_code: str) -> set[str]: """Get all known tags for a language code. The input is canonicalised first, so 3-letter/legacy codes ("ger", "eng") expand exactly like their 2-letter equivalents — previously a keep-list entry of "ger" produced the 1-element set {"ger"}, which failed to match "de"/"deu"-tagged tracks on destructive cleanup paths. Script-variant codes (zh-hans, zh-hant) additionally include their base language's tags: real containers tag Chinese tracks "chi"/"zho"/"zh", and a zh-hans target must never classify those as foreign/disposable. """ key = (lang_code or "").lower().strip() canonical = _REVERSE_LANGUAGE_TAGS.get(key, key) tags = set(_LANGUAGE_TAGS.get(canonical, {key})) if "-" in canonical: base = canonical.split("-", 1)[0] tags |= _LANGUAGE_TAGS.get(base, set()) return tags # Reverse lookup: any known tag -> canonical ISO 639-1 code. # Built lazily/deterministically at module import time so callers get O(1) lookup. _REVERSE_LANGUAGE_TAGS: dict[str, str] = { tag: code for code, tags in _LANGUAGE_TAGS.items() for tag in tags } def language_name(code: str) -> str: """English display name for an ISO 639-1 code; the code itself if unknown.""" normalized = normalize_language_code(code or "") for entry in SUPPORTED_LANGUAGES: if entry.get("code") == normalized: return entry.get("name", normalized) return code def normalize_language_code(raw: str) -> str: """Map any known language tag to its canonical ISO 639-1 code. Examples: 'ger', 'deu', 'german', 'DE' -> 'de' 'eng', 'english' -> 'en' 'jpn', 'japanese' -> 'ja' Unknown tags (including empty strings and 'und') are returned in lower-case/stripped form unchanged — callers decide whether to treat an unknown tag as "keep" (safer) or "discard" (stricter). """ if not raw: return "" key = raw.lower().strip() return _REVERSE_LANGUAGE_TAGS.get(key, key)