From 037388886e14b1f51c8832eac785e6a086051c0c Mon Sep 17 00:00:00 2001 From: Javanaut Date: Mon, 13 Apr 2026 20:04:25 +0200 Subject: [PATCH] prep 0.2.6 --- README.md | 9 + SCRATCHPAD.md | 2 +- assets/ffx.json.j2 | 1 + assets/i18n/de.json | 361 ++++++++++++++++++ assets/i18n/en.json | 360 +++++++++++++++++ assets/i18n/eo.json | 361 ++++++++++++++++++ assets/i18n/es.json | 361 ++++++++++++++++++ assets/i18n/fr.json | 361 ++++++++++++++++++ assets/i18n/ja.json | 361 ++++++++++++++++++ assets/i18n/nb.json | 361 ++++++++++++++++++ assets/i18n/pt.json | 361 ++++++++++++++++++ assets/i18n/ta.json | 361 ++++++++++++++++++ pyproject.toml | 2 +- requirements/project.md | 2 +- requirements/shifted_seasons_handling.md | 2 +- src/ffx/_iso_language.py | 220 +++++++++++ src/ffx/cli.py | 57 ++- src/ffx/configuration_controller.py | 4 + src/ffx/confirm_screen.py | 22 +- src/ffx/constants.py | 2 +- src/ffx/ffx_app.py | 6 +- src/ffx/help_screen.py | 12 +- src/ffx/i18n.py | 158 ++++++++ src/ffx/inspect_details_screen.py | 267 +++++++++++-- src/ffx/iso_language.py | 14 +- src/ffx/media_descriptor_change_set.py | 60 ++- src/ffx/media_edit_screen.py | 267 ++++++++++--- src/ffx/media_workflow_screen_base.py | 219 +++++------ src/ffx/pattern_delete_screen.py | 31 +- src/ffx/pattern_details_screen.py | 146 +++---- src/ffx/screen_support.py | 53 +++ src/ffx/settings_screen.py | 13 +- src/ffx/shifted_season_delete_screen.py | 48 ++- src/ffx/shifted_season_details_screen.py | 54 ++- src/ffx/show_delete_screen.py | 27 +- src/ffx/show_details_screen.py | 134 ++++--- src/ffx/shows_screen.py | 121 +++++- src/ffx/tag_delete_screen.py | 34 +- src/ffx/tag_details_screen.py | 34 +- src/ffx/track_delete_screen.py | 53 ++- src/ffx/track_details_screen.py | 155 ++++++-- .../unit/test_configure_workstation_script.py | 22 ++ tests/unit/test_i18n.py | 89 +++++ .../unit/test_media_descriptor_change_set.py | 135 +++++++ tests/unit/test_metadata_editor.py | 3 + tests/unit/test_screen_support.py | 46 +++ tests/unit/test_tag_table_screen_state.py | 119 +++++- tools/configure_workstation.sh | 2 + tools/generate_i18n_catalogs.py | 299 +++++++++++++++ 49 files changed, 5702 insertions(+), 490 deletions(-) create mode 100644 assets/i18n/de.json create mode 100644 assets/i18n/en.json create mode 100644 assets/i18n/eo.json create mode 100644 assets/i18n/es.json create mode 100644 assets/i18n/fr.json create mode 100644 assets/i18n/ja.json create mode 100644 assets/i18n/nb.json create mode 100644 assets/i18n/pt.json create mode 100644 assets/i18n/ta.json create mode 100644 src/ffx/_iso_language.py create mode 100644 src/ffx/i18n.py create mode 100644 tests/unit/test_i18n.py create mode 100644 tools/generate_i18n_catalogs.py diff --git a/README.md b/README.md index 08e4f6a..b10c689 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,15 @@ TMDB-backed metadata enrichment requires `TMDB_API_KEY` to be set in the environ ## Version History +### 0.2.6 + +- DB-free `ffx edit` workflow for in-place metadata editing via temporary-file rewrite +- inspect and edit workflows split into dedicated Textual screens with shared media-workflow support +- Textual tables and row actions now separate raw data from rendered labels to avoid markup leaking into stored metadata +- responsive screen layout pass, `Esc` back handling, sortable show/inspect tables, and improved edit-screen notifications/toggles +- application-wide UTF-8 i18n catalogs with language precedence from CLI over config over system over German default +- metadata normalization extended for localized subtitle titles, ISO language cleanup, and smarter track editor language/title helpers + ### 0.2.5 - show-level quality and notes fields diff --git a/SCRATCHPAD.md b/SCRATCHPAD.md index e2c8e57..89eaddd 100644 --- a/SCRATCHPAD.md +++ b/SCRATCHPAD.md @@ -47,7 +47,7 @@ ## Open -- Durable shipped items have been moved into [`README.md`](/home/osgw/.local/src/codex/ffx/README.md) version history through `0.2.5`. +- Durable shipped items have been moved into [`README.md`](/home/osgw/.local/src/codex/ffx/README.md) version history through `0.2.6`. - Should optimization work focus first on operator-perceived latency, internal maintainability, or correctness-risk cleanup that also has performance upside? - Is the long-term supported model still “local Linux workstation plus Textual UI,” or should optimization decisions bias toward a more scriptable/headless CLI? diff --git a/assets/ffx.json.j2 b/assets/ffx.json.j2 index 666e603..73b8b53 100644 --- a/assets/ffx.json.j2 +++ b/assets/ffx.json.j2 @@ -1,4 +1,5 @@ { + "language": {{ language_json }}, "databasePath": {{ database_path_json }}, "logDirectory": {{ log_directory_json }}, "subtitlesDirectory": {{ subtitles_directory_json }}, diff --git a/assets/i18n/de.json b/assets/i18n/de.json new file mode 100644 index 0000000..d7dd31e --- /dev/null +++ b/assets/i18n/de.json @@ -0,0 +1,361 @@ +{ + "iso_languages": { + "ABKHAZIAN": "Abchasisch", + "AFAR": "Afar", + "AFRIKAANS": "Afrikaans", + "AKAN": "Akan", + "ALBANIAN": "Albanisch", + "AMHARIC": "Amharisch", + "ARABIC": "Arabisch", + "ARAGONESE": "Aragonesisch", + "ARMENIAN": "Armenisch", + "ASSAMESE": "Assamesisch", + "AVARIC": "Awarisch", + "AVESTAN": "Avestisch", + "AYMARA": "Aymara", + "AZERBAIJANI": "Aserbaidschanisch", + "BAMBARA": "Bambara", + "BASHKIR": "Baschkirisch", + "BASQUE": "Baskisch", + "BELARUSIAN": "Weißrussisch", + "BENGALI": "Bengalisch", + "BISLAMA": "Bislama", + "BOKMAL": "Bokmål", + "BOSNIAN": "Bosnisch", + "BRETON": "Bretonisch", + "BULGARIAN": "Bulgarisch", + "BURMESE": "Burmesisch", + "CATALAN": "Catalan", + "CHAMORRO": "Chamorro", + "CHECHEN": "Tschetschenisch", + "CHICHEWA": "Chichewa", + "CHINESE": "Chinesisch", + "CHURCH_SLAVIC": "Church Slavic", + "CHUVASH": "Tschuwaschisch", + "CORNISH": "Kornisch", + "CORSICAN": "Korsisch", + "CREE": "Cree", + "CROATIAN": "Kroatisch", + "CZECH": "Tschechisch", + "DANISH": "Dänisch", + "DIVEHI": "Divehi", + "DUTCH": "Dutch", + "DZONGKHA": "Dzongkha", + "ENGLISH": "Englisch", + "ESPERANTO": "Esperanto", + "ESTONIAN": "Estnisch", + "EWE": "Ewe-Sprache", + "FAROESE": "Färöisch", + "FIJIAN": "Fidschianisch", + "FILIPINO": "Filipino", + "FINNISH": "Finnisch", + "FRENCH": "Französisch", + "FULAH": "Ful", + "GALICIAN": "Galizisch", + "GANDA": "Ganda", + "GEORGIAN": "Georgisch", + "GERMAN": "Deutsch", + "GREEK": "Greek", + "GUARANI": "Guaraní", + "GUJARATI": "Gujarati", + "HAITIAN": "Haitian", + "HAUSA": "Haussa", + "HEBREW": "Hebräisch", + "HERERO": "Herero", + "HINDI": "Hindi", + "HIRI_MOTU": "Hiri-Motu", + "HUNGARIAN": "Ungarisch", + "ICELANDIC": "Isländisch", + "IDO": "Ido", + "IGBO": "Ibo", + "INDONESIAN": "Indonesisch", + "INTERLINGUA": "Interlingua", + "INTERLINGUE": "Interlingue", + "INUKTITUT": "Inuktitut", + "INUPIAQ": "Inupiaq", + "IRISH": "Irisch", + "ITALIAN": "Italienisch", + "JAPANESE": "Japanisch", + "JAVANESE": "Javanisch", + "KALAALLISUT": "Kalaallisut", + "KANNADA": "Kannada", + "KANURI": "Kanuri", + "KASHMIRI": "Kaschmirisch", + "KAZAKH": "Kasachisch", + "KHMER": "Khmer", + "KIKUYU": "Kikuyu", + "KINYARWANDA": "Kinyarwanda", + "KIRGHIZ": "Kirghiz", + "KOMI": "Komi", + "KONGO": "Kongo", + "KOREAN": "Koreanisch", + "KUANYAMA": "Kuanyama", + "KURDISH": "Kurdisch", + "LAO": "Laotisch", + "LATIN": "Lateinisch", + "LATVIAN": "Lettisch", + "LIMBURGAN": "Limburgan", + "LINGALA": "Lingala", + "LITHUANIAN": "Litauisch", + "LUBA_KATANGA": "Luba-Katanga", + "LUXEMBOURGISH": "Luxembourgish", + "MACEDONIAN": "Makedonisch", + "MALAGASY": "Malagasi", + "MALAY": "Malaiisch", + "MALAYALAM": "Malayalam", + "MALTESE": "Maltesisch", + "MANX": "Manx", + "MAORI": "Maori", + "MARATHI": "Marathi", + "MARSHALLESE": "Marschallesisch", + "MONGOLIAN": "Mongolisch", + "NAURU": "Nauru", + "NAVAJO": "Navajo", + "NDONGA": "Ndonga", + "NEPALI": "Nepali", + "NORTHERN_SAMI": "Nord-Samisch", + "NORTH_NDEBELE": "North Ndebele", + "NORWEGIAN": "Norwegisch", + "NORWEGIAN_NYNORSK": "Nynorsk", + "OCCITAN": "Occitan", + "OJIBWA": "Ojibwa", + "ORIYA": "Oriya", + "OROMO": "Oromo", + "OSSETIAN": "Ossetian", + "PALI": "Pali", + "PANJABI": "Panjabi", + "PERSIAN": "Persisch", + "POLISH": "Polnisch", + "PORTUGUESE": "Portugiesisch", + "PUSHTO": "Pushto", + "QUECHUA": "Quechua", + "ROMANIAN": "Romanian", + "ROMANSH": "Bündnerromanisch", + "RUNDI": "Kirundi", + "RUSSIAN": "Russisch", + "SAMOAN": "Samoanisch", + "SANGO": "Sango", + "SANSKRIT": "Sanskrit", + "SARDINIAN": "Sardisch", + "SCOTTISH_GAELIC": "Scottish Gaelic", + "SERBIAN": "Serbisch", + "SHONA": "Schona", + "SICHUAN_YI": "Sichuan Yi", + "SINDHI": "Sindhi", + "SINHALA": "Sinhala", + "SLOVAK": "Slowakisch", + "SLOVENIAN": "Slowenisch", + "SOMALI": "Somali", + "SOUTHERN_SOTHO": "Southern Sotho", + "SOUTH_NDEBELE": "South Ndebele", + "SPANISH": "Spanish", + "SUNDANESE": "Sundanesisch", + "SWAHILI": "Suaheli; Swaheli", + "SWATI": "Swazi", + "SWEDISH": "Schwedisch", + "TAGALOG": "Tagalog", + "TAHITIAN": "Tahitisch", + "TAJIK": "Tadschikisch", + "TAMIL": "Tamilisch", + "TATAR": "Tatarisch", + "TELUGU": "Telugu", + "THAI": "Thai", + "TIBETAN": "Tibetisch", + "TIGRINYA": "Tigrinja", + "TONGA": "Tonga", + "TSONGA": "Tsonga", + "TSWANA": "Tswana", + "TURKISH": "Türkisch", + "TURKMEN": "Turkmenisch", + "TWI": "Twi", + "UIGHUR": "Uighur", + "UKRAINIAN": "Ukrainisch", + "UNDEFINED": "undefined", + "URDU": "Urdu", + "UZBEK": "Usbekisch", + "VENDA": "Venda", + "VIETNAMESE": "Vietnamesisch", + "VOLAPUK": "Volapük", + "WALLOON": "Wallonisch", + "WELSH": "Walisisch", + "WESTERN_FRISIAN": "Westfriesisch", + "WOLOF": "Wolof", + "XHOSA": "Xhosa", + "YIDDISH": "Jiddisch", + "YORUBA": "Joruba", + "ZHUANG": "Zhuang", + "ZULU": "Zulu" + }, + "phrases": { + "5.0(side)": "5.0(side)", + "5.1(side)": "5.1(side)", + "6.1": "6.1", + "6ch": "6ch", + "7.1": "7.1", + "": "", + "Add": "Hinzufügen", + "Add Pattern": "Muster hinzufügen", + "Apply": "Anwenden", + "Apply failed: {error}": "Anwenden fehlgeschlagen: {error}", + "Are you sure to delete the following filename pattern?": "Möchtest du das folgende Dateinamensmuster wirklich löschen?", + "Are you sure to delete the following shifted season?": "Möchtest du die folgende verschobene Staffel wirklich löschen?", + "Are you sure to delete the following show?": "Möchtest du die folgende Serie wirklich löschen?", + "Are you sure to delete the following {track_type} track?": "Möchtest du den folgenden {track_type}-Stream wirklich löschen?", + "Are you sure to delete this tag?": "Möchtest du dieses Tag wirklich löschen?", + "Audio Layout": "Audiolayout", + "Back": "Zurück", + "Cancel": "Abbrechen", + "Cannot add another stream with disposition flag 'default' or 'forced' set": "Es kann kein weiterer Stream mit gesetztem Dispositions-Flag 'default' oder 'forced' hinzugefügt werden", + "Changes applied and file reloaded.": "Änderungen angewendet und Datei neu geladen.", + "Cleanup": "Bereinigen", + "Cleanup disabled.": "Bereinigung deaktiviert.", + "Cleanup enabled.": "Bereinigung aktiviert.", + "Codec": "Codec", + "Continuing edit session.": "Bearbeitung wird fortgesetzt.", + "Default": "Standard", + "Delete": "Löschen", + "Delete Show": "Serie löschen", + "Deleted media tag {tag!r}.": "Medien-Tag {tag!r} gelöscht.", + "Differences": "Unterschiede", + "Differences (file->db/output)": "Unterschiede (Datei->DB/Ausgabe)", + "Discard": "Verwerfen", + "Discard pending metadata changes and quit?": "Ausstehende Metadatenänderungen verwerfen und beenden?", + "Discard pending metadata changes and reload the file state?": "Ausstehende Metadatenänderungen verwerfen und Dateistand neu laden?", + "Down": "Runter", + "Dry-run: would rewrite via temporary file {target_path}": "Trockenlauf: würde über temporäre Datei {target_path} neu schreiben", + "Edit": "Bearbeiten", + "Edit Pattern": "Muster bearbeiten", + "Edit Show": "Serie bearbeiten", + "Edit filename pattern": "Dateinamensmuster bearbeiten", + "Edit shifted season": "Verschobene Staffel bearbeiten", + "Edit stream": "Stream bearbeiten", + "Episode Offset": "Episodenoffset", + "Episode offset": "Episodenoffset", + "File": "Datei", + "File patterns": "Datei-Namensmuster", + "First Episode": "Erste Episode", + "First episode": "Erste Episode", + "Forced": "Erzwungen", + "Help": "Hilfe", + "Help Screen": "Hilfe-Bildschirm", + "ID": "ID", + "Identify": "Identifizieren", + "Index": "Index", + "Index / Subindex": "Index / Unterindex", + "Index Episode Digits": "Ep. Index Stellen", + "Index Season Digits": "Sta. Index Stellen", + "Indicator Edisode Digits": "Ep. Indikator Stellen", + "Indicator Season Digits": "Sta. Indikator Stellen", + "Keep Editing": "Weiter bearbeiten", + "Keeping pending changes.": "Ausstehende Änderungen bleiben erhalten.", + "Key": "Schlüssel", + "Language": "Sprache", + "Last Episode": "Letzte Episode", + "Last episode": "Letzte Episode", + "Layout": "Layout", + "Media Tags": "Medien-Tags", + "More than one default audio stream detected and no prompt set": "Mehr als ein Standard-Audiostream erkannt und keine Abfrage aktiviert", + "More than one default audio stream detected! Please select stream": "Mehr als ein Standard-Audiostream erkannt! Bitte Stream auswählen", + "More than one default subtitle stream detected and no prompt set": "Mehr als ein Standard-Untertitelstream erkannt und keine Abfrage aktiviert", + "More than one default subtitle stream detected! Please select stream": "Mehr als ein Standard-Untertitelstream erkannt! Bitte Stream auswählen", + "More than one default video stream detected and no prompt set": "Mehr als ein Standard-Videostream erkannt und keine Abfrage aktiviert", + "More than one default video stream detected! Please select stream": "Mehr als ein Standard-Videostream erkannt! Bitte Stream auswählen", + "More than one forced audio stream detected and no prompt set": "Mehr als ein erzwungener Audiostream erkannt und keine Abfrage aktiviert", + "More than one forced audio stream detected! Please select stream": "Mehr als ein erzwungener Audiostream erkannt! Bitte Stream auswählen", + "More than one forced subtitle stream detected and no prompt set": "Mehr als ein erzwungener Untertitelstream erkannt und keine Abfrage aktiviert", + "More than one forced subtitle stream detected! Please select stream": "Mehr als ein erzwungener Untertitelstream erkannt! Bitte Stream auswählen", + "More than one forced video stream detected and no prompt set": "Mehr als ein erzwungener Videostream erkannt und keine Abfrage aktiviert", + "More than one forced video stream detected! Please select stream": "Mehr als ein erzwungener Videostream erkannt! Bitte Stream auswählen", + "Name": "Name", + "New Pattern": "Neues Muster", + "New Show": "Neue Serie", + "New filename pattern": "Neues Dateinamensmuster", + "New shifted season": "Neue verschobene Staffel", + "New stream": "Neuer Stream", + "No": "Nein", + "No changes to apply.": "Keine Änderungen zum Anwenden.", + "No changes to revert.": "Keine Änderungen zum Zurücksetzen.", + "Normalization disabled.": "Normalisierung deaktiviert.", + "Normalization enabled.": "Normalisierung aktiviert.", + "Normalize": "Normalisieren", + "Notes": "Notizen", + "Pattern": "Muster", + "Planned Changes (file->edited output)": "Geplante Änderungen (Datei->bearbeitete Ausgabe)", + "Quality": "Qualität", + "Quit": "Beenden", + "Remove Pattern": "Muster entfernen", + "Revert": "Zurücksetzen", + "Reverted pending changes.": "Ausstehende Änderungen verworfen.", + "Save": "Speichern", + "Season Offset": "Staffeloffset", + "Select a stream first.": "Bitte zuerst einen Stream auswählen.", + "Set Default": "Als Standard setzen", + "Set Forced": "Als erzwungen setzen", + "Settings Screen": "Einstellungsbildschirm", + "Numbering Mapping": "Abbildung Nummerierung", + "Show": "Serie", + "Shows": "Serien", + "Source Season": "Quellstaffel", + "SrcIndex": "QuellIndex", + "Status": "Status", + "Stay": "Bleiben", + "Stream dispositions": "Stream-Dispositionen", + "Stream tags": "Stream-Tags", + "Streams": "Streams", + "SubIndex": "Unterindex", + "Substitute": "Ersetzen", + "Substitute pattern": "Muster ersetzen", + "Title": "Titel", + "Type": "Typ", + "Unable to update selected stream.": "Ausgewählten Stream konnte nicht aktualisiert werden.", + "Up": "Hoch", + "Update Pattern": "Muster aktualisieren", + "Updated media tag {tag!r}.": "Medien-Tag {tag!r} aktualisiert.", + "Updated stream #{index} ({track_type}).": "Stream #{index} ({track_type}) aktualisiert.", + "Value": "Wert", + "Year": "Jahr", + "Yes": "Ja", + "add media tag: key='{key}' value='{value}'": "Medien-Tag hinzufügen: Schlüssel='{key}' Wert='{value}'", + "add {track_type} track: index={index} lang={language}": "{track_type}-Stream hinzufügen: Index={index} Sprache={language}", + "attached_pic": "attached_pic", + "attachment": "Anhang", + "audio": "Audio", + "captions": "Untertitel", + "change media tag: key='{key}' value='{value}'": "Medien-Tag ändern: Schlüssel='{key}' Wert='{value}'", + "change stream #{index} ({track_type}:{sub_index}) add disposition={disposition}": "Stream #{index} ({track_type}:{sub_index}) Disposition hinzufügen={disposition}", + "change stream #{index} ({track_type}:{sub_index}) add key={key} value={value}": "Stream #{index} ({track_type}:{sub_index}) Schlüssel hinzufügen={key} Wert={value}", + "change stream #{index} ({track_type}:{sub_index}) change key={key} value={value}": "Stream #{index} ({track_type}:{sub_index}) Schlüssel ändern={key} Wert={value}", + "change stream #{index} ({track_type}:{sub_index}) remove disposition={disposition}": "Stream #{index} ({track_type}:{sub_index}) Disposition entfernen={disposition}", + "change stream #{index} ({track_type}:{sub_index}) remove key={key} value={value}": "Stream #{index} ({track_type}:{sub_index}) Schlüssel entfernen={key} Wert={value}", + "clean_effects": "Nur Effekte", + "comment": "Kommentar", + "default": "Standard", + "dependent": "abhängig", + "descriptions": "Beschreibungen", + "dub": "Synchronisiert", + "for pattern": "für Muster", + "forced": "erzwungen", + "from": "von", + "from pattern": "aus Muster", + "from show": "aus Serie", + "hearing_impaired": "hörgeschädigt", + "karaoke": "Karaoke", + "lyrics": "Liedtext", + "metadata": "Metadaten", + "non_diegetic": "nicht-diegetisch", + "original": "Original", + "pattern #{id}": "Muster #{id}", + "remove media tag: key='{key}' value='{value}'": "Medien-Tag entfernen: Schlüssel='{key}' Wert='{value}'", + "remove stream #{index}": "Stream #{index} entfernen", + "show #{id}": "Serie #{id}", + "stereo": "Stereo", + "still_image": "Standbild", + "sub index": "Unterindex", + "subtitle": "Untertitel", + "timed_thumbnails": "zeitgesteuerte Vorschaubilder", + "undefined": "undefiniert", + "unknown": "unbekannt", + "video": "Video", + "visual_impaired": "sehgeschädigt" + } +} diff --git a/assets/i18n/en.json b/assets/i18n/en.json new file mode 100644 index 0000000..0b8ce63 --- /dev/null +++ b/assets/i18n/en.json @@ -0,0 +1,360 @@ +{ + "iso_languages": { + "ABKHAZIAN": "Abkhazian", + "AFAR": "Afar", + "AFRIKAANS": "Afrikaans", + "AKAN": "Akan", + "ALBANIAN": "Albanian", + "AMHARIC": "Amharic", + "ARABIC": "Arabic", + "ARAGONESE": "Aragonese", + "ARMENIAN": "Armenian", + "ASSAMESE": "Assamese", + "AVARIC": "Avaric", + "AVESTAN": "Avestan", + "AYMARA": "Aymara", + "AZERBAIJANI": "Azerbaijani", + "BAMBARA": "Bambara", + "BASHKIR": "Bashkir", + "BASQUE": "Basque", + "BELARUSIAN": "Belarusian", + "BENGALI": "Bengali", + "BISLAMA": "Bislama", + "BOKMAL": "Bokmål", + "BOSNIAN": "Bosnian", + "BRETON": "Breton", + "BULGARIAN": "Bulgarian", + "BURMESE": "Burmese", + "CATALAN": "Catalan", + "CHAMORRO": "Chamorro", + "CHECHEN": "Chechen", + "CHICHEWA": "Chichewa", + "CHINESE": "Chinese", + "CHURCH_SLAVIC": "Church Slavic", + "CHUVASH": "Chuvash", + "CORNISH": "Cornish", + "CORSICAN": "Corsican", + "CREE": "Cree", + "CROATIAN": "Croatian", + "CZECH": "Czech", + "DANISH": "Danish", + "DIVEHI": "Divehi", + "DUTCH": "Dutch", + "DZONGKHA": "Dzongkha", + "ENGLISH": "English", + "ESPERANTO": "Esperanto", + "ESTONIAN": "Estonian", + "EWE": "Ewe", + "FAROESE": "Faroese", + "FIJIAN": "Fijian", + "FILIPINO": "Filipino", + "FINNISH": "Finnish", + "FRENCH": "French", + "FULAH": "Fulah", + "GALICIAN": "Galician", + "GANDA": "Ganda", + "GEORGIAN": "Georgian", + "GERMAN": "German", + "GREEK": "Greek", + "GUARANI": "Guarani", + "GUJARATI": "Gujarati", + "HAITIAN": "Haitian", + "HAUSA": "Hausa", + "HEBREW": "Hebrew", + "HERERO": "Herero", + "HINDI": "Hindi", + "HIRI_MOTU": "Hiri Motu", + "HUNGARIAN": "Hungarian", + "ICELANDIC": "Icelandic", + "IDO": "Ido", + "IGBO": "Igbo", + "INDONESIAN": "Indonesian", + "INTERLINGUA": "Interlingua", + "INTERLINGUE": "Interlingue", + "INUKTITUT": "Inuktitut", + "INUPIAQ": "Inupiaq", + "IRISH": "Irish", + "ITALIAN": "Italian", + "JAPANESE": "Japanese", + "JAVANESE": "Javanese", + "KALAALLISUT": "Kalaallisut", + "KANNADA": "Kannada", + "KANURI": "Kanuri", + "KASHMIRI": "Kashmiri", + "KAZAKH": "Kazakh", + "KHMER": "Khmer", + "KIKUYU": "Kikuyu", + "KINYARWANDA": "Kinyarwanda", + "KIRGHIZ": "Kirghiz", + "KOMI": "Komi", + "KONGO": "Kongo", + "KOREAN": "Korean", + "KUANYAMA": "Kuanyama", + "KURDISH": "Kurdish", + "LAO": "Lao", + "LATIN": "Latin", + "LATVIAN": "Latvian", + "LIMBURGAN": "Limburgan", + "LINGALA": "Lingala", + "LITHUANIAN": "Lithuanian", + "LUBA_KATANGA": "Luba-Katanga", + "LUXEMBOURGISH": "Luxembourgish", + "MACEDONIAN": "Macedonian", + "MALAGASY": "Malagasy", + "MALAY": "Malay", + "MALAYALAM": "Malayalam", + "MALTESE": "Maltese", + "MANX": "Manx", + "MAORI": "Maori", + "MARATHI": "Marathi", + "MARSHALLESE": "Marshallese", + "MONGOLIAN": "Mongolian", + "NAURU": "Nauru", + "NAVAJO": "Navajo", + "NDONGA": "Ndonga", + "NEPALI": "Nepali", + "NORTHERN_SAMI": "Northern Sami", + "NORTH_NDEBELE": "North Ndebele", + "NORWEGIAN": "Norwegian", + "NORWEGIAN_NYNORSK": "Nynorsk", + "OCCITAN": "Occitan", + "OJIBWA": "Ojibwa", + "ORIYA": "Oriya", + "OROMO": "Oromo", + "OSSETIAN": "Ossetian", + "PALI": "Pali", + "PANJABI": "Panjabi", + "PERSIAN": "Persian", + "POLISH": "Polish", + "PORTUGUESE": "Portuguese", + "PUSHTO": "Pushto", + "QUECHUA": "Quechua", + "ROMANIAN": "Romanian", + "ROMANSH": "Romansh", + "RUNDI": "Rundi", + "RUSSIAN": "Russian", + "SAMOAN": "Samoan", + "SANGO": "Sango", + "SANSKRIT": "Sanskrit", + "SARDINIAN": "Sardinian", + "SCOTTISH_GAELIC": "Scottish Gaelic", + "SERBIAN": "Serbian", + "SHONA": "Shona", + "SICHUAN_YI": "Sichuan Yi", + "SINDHI": "Sindhi", + "SINHALA": "Sinhala", + "SLOVAK": "Slovak", + "SLOVENIAN": "Slovenian", + "SOMALI": "Somali", + "SOUTHERN_SOTHO": "Southern Sotho", + "SOUTH_NDEBELE": "South Ndebele", + "SPANISH": "Spanish", + "SUNDANESE": "Sundanese", + "SWAHILI": "Swahili", + "SWATI": "Swati", + "SWEDISH": "Swedish", + "TAGALOG": "Tagalog", + "TAHITIAN": "Tahitian", + "TAJIK": "Tajik", + "TAMIL": "Tamil", + "TATAR": "Tatar", + "TELUGU": "Telugu", + "THAI": "Thai", + "TIBETAN": "Tibetan", + "TIGRINYA": "Tigrinya", + "TONGA": "Tonga", + "TSONGA": "Tsonga", + "TSWANA": "Tswana", + "TURKISH": "Turkish", + "TURKMEN": "Turkmen", + "TWI": "Twi", + "UIGHUR": "Uighur", + "UKRAINIAN": "Ukrainian", + "UNDEFINED": "undefined", + "URDU": "Urdu", + "UZBEK": "Uzbek", + "VENDA": "Venda", + "VIETNAMESE": "Vietnamese", + "VOLAPUK": "Volapük", + "WALLOON": "Walloon", + "WELSH": "Welsh", + "WESTERN_FRISIAN": "Western Frisian", + "WOLOF": "Wolof", + "XHOSA": "Xhosa", + "YIDDISH": "Yiddish", + "YORUBA": "Yoruba", + "ZHUANG": "Zhuang", + "ZULU": "Zulu" + }, + "phrases": { + "5.0(side)": "5.0(side)", + "5.1(side)": "5.1(side)", + "6.1": "6.1", + "6ch": "6ch", + "7.1": "7.1", + "": "", + "Add": "Add", + "Add Pattern": "Add Pattern", + "Apply": "Apply", + "Apply failed: {error}": "Apply failed: {error}", + "Are you sure to delete the following filename pattern?": "Are you sure to delete the following filename pattern?", + "Are you sure to delete the following shifted season?": "Are you sure to delete the following shifted season?", + "Are you sure to delete the following show?": "Are you sure to delete the following show?", + "Are you sure to delete the following {track_type} track?": "Are you sure to delete the following {track_type} track?", + "Are you sure to delete this tag?": "Are you sure to delete this tag?", + "Audio Layout": "Audio Layout", + "Back": "Back", + "Cancel": "Cancel", + "Cannot add another stream with disposition flag 'default' or 'forced' set": "Cannot add another stream with disposition flag 'default' or 'forced' set", + "Changes applied and file reloaded.": "Changes applied and file reloaded.", + "Cleanup": "Cleanup", + "Cleanup disabled.": "Cleanup disabled.", + "Cleanup enabled.": "Cleanup enabled.", + "Codec": "Codec", + "Continuing edit session.": "Continuing edit session.", + "Default": "Default", + "Delete": "Delete", + "Delete Show": "Delete Show", + "Deleted media tag {tag!r}.": "Deleted media tag {tag!r}.", + "Differences": "Differences", + "Differences (file->db/output)": "Differences (file->db/output)", + "Discard": "Discard", + "Discard pending metadata changes and quit?": "Discard pending metadata changes and quit?", + "Discard pending metadata changes and reload the file state?": "Discard pending metadata changes and reload the file state?", + "Down": "Down", + "Dry-run: would rewrite via temporary file {target_path}": "Dry-run: would rewrite via temporary file {target_path}", + "Edit": "Edit", + "Edit Pattern": "Edit Pattern", + "Edit Show": "Edit Show", + "Edit filename pattern": "Edit filename pattern", + "Edit shifted season": "Edit shifted season", + "Edit stream": "Edit stream", + "Episode Offset": "Episode Offset", + "Episode offset": "Episode offset", + "File": "File", + "File patterns": "File patterns", + "First Episode": "First Episode", + "First episode": "First episode", + "Forced": "Forced", + "Help": "Help", + "Help Screen": "Help Screen", + "ID": "ID", + "Identify": "Identify", + "Index": "Index", + "Index / Subindex": "Index / Subindex", + "Index Episode Digits": "Index Episode Digits", + "Index Season Digits": "Index Season Digits", + "Indicator Edisode Digits": "Indicator Edisode Digits", + "Indicator Season Digits": "Indicator Season Digits", + "Keep Editing": "Keep Editing", + "Keeping pending changes.": "Keeping pending changes.", + "Key": "Key", + "Language": "Language", + "Last Episode": "Last Episode", + "Last episode": "Last episode", + "Layout": "Layout", + "Media Tags": "Media Tags", + "More than one default audio stream detected and no prompt set": "More than one default audio stream detected and no prompt set", + "More than one default audio stream detected! Please select stream": "More than one default audio stream detected! Please select stream", + "More than one default subtitle stream detected and no prompt set": "More than one default subtitle stream detected and no prompt set", + "More than one default subtitle stream detected! Please select stream": "More than one default subtitle stream detected! Please select stream", + "More than one default video stream detected and no prompt set": "More than one default video stream detected and no prompt set", + "More than one default video stream detected! Please select stream": "More than one default video stream detected! Please select stream", + "More than one forced audio stream detected and no prompt set": "More than one forced audio stream detected and no prompt set", + "More than one forced audio stream detected! Please select stream": "More than one forced audio stream detected! Please select stream", + "More than one forced subtitle stream detected and no prompt set": "More than one forced subtitle stream detected and no prompt set", + "More than one forced subtitle stream detected! Please select stream": "More than one forced subtitle stream detected! Please select stream", + "More than one forced video stream detected and no prompt set": "More than one forced video stream detected and no prompt set", + "More than one forced video stream detected! Please select stream": "More than one forced video stream detected! Please select stream", + "Name": "Name", + "New Pattern": "New Pattern", + "New Show": "New Show", + "New filename pattern": "New filename pattern", + "New shifted season": "New shifted season", + "New stream": "New stream", + "No": "No", + "No changes to apply.": "No changes to apply.", + "No changes to revert.": "No changes to revert.", + "Normalization disabled.": "Normalization disabled.", + "Normalization enabled.": "Normalization enabled.", + "Normalize": "Normalize", + "Notes": "Notes", + "Pattern": "Pattern", + "Planned Changes (file->edited output)": "Planned Changes (file->edited output)", + "Quality": "Quality", + "Quit": "Quit", + "Remove Pattern": "Remove Pattern", + "Revert": "Revert", + "Reverted pending changes.": "Reverted pending changes.", + "Save": "Save", + "Season Offset": "Season Offset", + "Select a stream first.": "Select a stream first.", + "Set Default": "Set Default", + "Set Forced": "Set Forced", + "Settings Screen": "Settings Screen", + "Numbering Mapping": "Numbering Mapping", + "Show": "Show", + "Shows": "Shows", + "SrcIndex": "SrcIndex", + "Status": "Status", + "Stay": "Stay", + "Stream dispositions": "Stream dispositions", + "Stream tags": "Stream tags", + "Streams": "Streams", + "SubIndex": "SubIndex", + "Substitute": "Substitute", + "Substitute pattern": "Substitute pattern", + "Title": "Title", + "Type": "Type", + "Unable to update selected stream.": "Unable to update selected stream.", + "Up": "Up", + "Update Pattern": "Update Pattern", + "Updated media tag {tag!r}.": "Updated media tag {tag!r}.", + "Updated stream #{index} ({track_type}).": "Updated stream #{index} ({track_type}).", + "Value": "Value", + "Year": "Year", + "Yes": "Yes", + "add media tag: key='{key}' value='{value}'": "add media tag: key='{key}' value='{value}'", + "add {track_type} track: index={index} lang={language}": "add {track_type} track: index={index} lang={language}", + "attached_pic": "attached_pic", + "attachment": "attachment", + "audio": "audio", + "captions": "captions", + "change media tag: key='{key}' value='{value}'": "change media tag: key='{key}' value='{value}'", + "change stream #{index} ({track_type}:{sub_index}) add disposition={disposition}": "change stream #{index} ({track_type}:{sub_index}) add disposition={disposition}", + "change stream #{index} ({track_type}:{sub_index}) add key={key} value={value}": "change stream #{index} ({track_type}:{sub_index}) add key={key} value={value}", + "change stream #{index} ({track_type}:{sub_index}) change key={key} value={value}": "change stream #{index} ({track_type}:{sub_index}) change key={key} value={value}", + "change stream #{index} ({track_type}:{sub_index}) remove disposition={disposition}": "change stream #{index} ({track_type}:{sub_index}) remove disposition={disposition}", + "change stream #{index} ({track_type}:{sub_index}) remove key={key} value={value}": "change stream #{index} ({track_type}:{sub_index}) remove key={key} value={value}", + "clean_effects": "clean_effects", + "comment": "comment", + "default": "default", + "dependent": "dependent", + "descriptions": "descriptions", + "dub": "dub", + "for pattern": "for pattern", + "forced": "forced", + "from": "from", + "from pattern": "from pattern", + "from show": "from show", + "hearing_impaired": "hearing_impaired", + "karaoke": "karaoke", + "lyrics": "lyrics", + "metadata": "metadata", + "non_diegetic": "non_diegetic", + "original": "original", + "pattern #{id}": "pattern #{id}", + "remove media tag: key='{key}' value='{value}'": "remove media tag: key='{key}' value='{value}'", + "remove stream #{index}": "remove stream #{index}", + "show #{id}": "show #{id}", + "stereo": "stereo", + "still_image": "still_image", + "sub index": "sub index", + "subtitle": "subtitle", + "timed_thumbnails": "timed_thumbnails", + "undefined": "undefined", + "unknown": "unknown", + "video": "video", + "visual_impaired": "visual_impaired" + } +} diff --git a/assets/i18n/eo.json b/assets/i18n/eo.json new file mode 100644 index 0000000..a8d0484 --- /dev/null +++ b/assets/i18n/eo.json @@ -0,0 +1,361 @@ +{ + "iso_languages": { + "ABKHAZIAN": "Abĥaza", + "AFAR": "Afara", + "AFRIKAANS": "Afrikansa", + "AKAN": "Akana", + "ALBANIAN": "Albana", + "AMHARIC": "Amhara", + "ARABIC": "Araba", + "ARAGONESE": "Aragona", + "ARMENIAN": "Armena", + "ASSAMESE": "Asama", + "AVARIC": "Avara", + "AVESTAN": "Avesta", + "AYMARA": "Ajmara", + "AZERBAIJANI": "Azerbajĝana", + "BAMBARA": "Bambara", + "BASHKIR": "Baŝkira", + "BASQUE": "Eŭska", + "BELARUSIAN": "Belorusa", + "BENGALI": "Bengala", + "BISLAMA": "Bislamo", + "BOKMAL": "Bokmål", + "BOSNIAN": "Bosna", + "BRETON": "Bretona", + "BULGARIAN": "Bulgara", + "BURMESE": "Birma", + "CATALAN": "Catalan", + "CHAMORRO": "Ĉamora", + "CHECHEN": "Ĉeĉena", + "CHICHEWA": "Chichewa", + "CHINESE": "Ĉina", + "CHURCH_SLAVIC": "Church Slavic", + "CHUVASH": "Ĉuvaŝa", + "CORNISH": "Kornvala", + "CORSICAN": "Korsika", + "CREE": "Kria", + "CROATIAN": "Kroata", + "CZECH": "Ĉeĥa", + "DANISH": "Dana", + "DIVEHI": "Divehi", + "DUTCH": "Dutch", + "DZONGKHA": "Dzonka", + "ENGLISH": "Angla", + "ESPERANTO": "Esperanto", + "ESTONIAN": "Estona", + "EWE": "Evea", + "FAROESE": "Feroa", + "FIJIAN": "Fiĝia", + "FILIPINO": "Filipino", + "FINNISH": "Finna", + "FRENCH": "Franca", + "FULAH": "Fula", + "GALICIAN": "Galega", + "GANDA": "Ganda", + "GEORGIAN": "Kartvela", + "GERMAN": "Germana", + "GREEK": "Greek", + "GUARANI": "Gvarania", + "GUJARATI": "Guĝarata", + "HAITIAN": "Haitian", + "HAUSA": "Haŭsa", + "HEBREW": "Hebrea", + "HERERO": "Herera", + "HINDI": "Hindia", + "HIRI_MOTU": "Hirimotua", + "HUNGARIAN": "Hungara", + "ICELANDIC": "Islanda", + "IDO": "Ido", + "IGBO": "Igba", + "INDONESIAN": "Indonezia", + "INTERLINGUA": "Interlingua", + "INTERLINGUE": "Interlingue", + "INUKTITUT": "Inuktituta", + "INUPIAQ": "Inupiaka", + "IRISH": "Irlanda", + "ITALIAN": "Itala", + "JAPANESE": "Japana", + "JAVANESE": "Java", + "KALAALLISUT": "Kalaallisut", + "KANNADA": "Kanara", + "KANURI": "Kanura", + "KASHMIRI": "Kaŝmira", + "KAZAKH": "Kazaĥa", + "KHMER": "Khmer", + "KIKUYU": "Kikuyu", + "KINYARWANDA": "Ruanda", + "KIRGHIZ": "Kirghiz", + "KOMI": "Komia", + "KONGO": "Konga", + "KOREAN": "Korea", + "KUANYAMA": "Kuanyama", + "KURDISH": "Kurda", + "LAO": "Laosa", + "LATIN": "Latina", + "LATVIAN": "Latva", + "LIMBURGAN": "Limburgan", + "LINGALA": "Lingala", + "LITHUANIAN": "Litova", + "LUBA_KATANGA": "Luba-katanga", + "LUXEMBOURGISH": "Luxembourgish", + "MACEDONIAN": "Makedona", + "MALAGASY": "Malagasa", + "MALAY": "Malaja", + "MALAYALAM": "Malajala", + "MALTESE": "Malta", + "MANX": "Manksa", + "MAORI": "Maoria", + "MARATHI": "Marata", + "MARSHALLESE": "Marŝala", + "MONGOLIAN": "Mongola", + "NAURU": "Naura", + "NAVAJO": "Navajo", + "NDONGA": "Ndonga", + "NEPALI": "Nepala", + "NORTHERN_SAMI": "Norda samea", + "NORTH_NDEBELE": "North Ndebele", + "NORWEGIAN": "Norvega", + "NORWEGIAN_NYNORSK": "Nynorsk", + "OCCITAN": "Occitan", + "OJIBWA": "Oĝibva", + "ORIYA": "Orija", + "OROMO": "Oroma", + "OSSETIAN": "Ossetian", + "PALI": "Palia", + "PANJABI": "Panjabi", + "PERSIAN": "Persa", + "POLISH": "Pola", + "PORTUGUESE": "Portugala", + "PUSHTO": "Pushto", + "QUECHUA": "Keĉua", + "ROMANIAN": "Romanian", + "ROMANSH": "Romanĉa", + "RUNDI": "Burunda", + "RUSSIAN": "Rusa", + "SAMOAN": "Samoa", + "SANGO": "Sangoa", + "SANSKRIT": "Sanskrito", + "SARDINIAN": "Sarda", + "SCOTTISH_GAELIC": "Scottish Gaelic", + "SERBIAN": "Serba", + "SHONA": "Ŝona", + "SICHUAN_YI": "Sichuan Yi", + "SINDHI": "Sinda", + "SINHALA": "Sinhala", + "SLOVAK": "Slovaka", + "SLOVENIAN": "Slovena", + "SOMALI": "Somalia", + "SOUTHERN_SOTHO": "Southern Sotho", + "SOUTH_NDEBELE": "South Ndebele", + "SPANISH": "Spanish", + "SUNDANESE": "Sunda", + "SWAHILI": "Svahila", + "SWATI": "Svazia", + "SWEDISH": "Sveda", + "TAGALOG": "Tagaloga", + "TAHITIAN": "Tahitia", + "TAJIK": "Taĝika", + "TAMIL": "Tamila", + "TATAR": "Tatara", + "TELUGU": "Telugua", + "THAI": "Taja", + "TIBETAN": "Tibeta", + "TIGRINYA": "Tigraja", + "TONGA": "Tonga", + "TSONGA": "Conga", + "TSWANA": "Cvana", + "TURKISH": "Turka", + "TURKMEN": "Turkmena", + "TWI": "Tvia", + "UIGHUR": "Uighur", + "UKRAINIAN": "Ukraina", + "UNDEFINED": "undefined", + "URDU": "Urdua", + "UZBEK": "Uzbeka", + "VENDA": "Vendaa", + "VIETNAMESE": "Vjetnama", + "VOLAPUK": "Volapuko", + "WALLOON": "Valona", + "WELSH": "Kimra", + "WESTERN_FRISIAN": "Okcidenta frisa", + "WOLOF": "Volofa", + "XHOSA": "Kosa", + "YIDDISH": "Jida", + "YORUBA": "Joruba", + "ZHUANG": "Zhuang", + "ZULU": "Zulua" + }, + "phrases": { + "5.0(side)": "5.0(side)", + "5.1(side)": "5.1(side)", + "6.1": "6.1", + "6ch": "6ch", + "7.1": "7.1", + "": "", + "Add": "Aldoni", + "Add Pattern": "Aldoni ŝablonon", + "Apply": "Apliki", + "Apply failed: {error}": "Apliko malsukcesis: {error}", + "Are you sure to delete the following filename pattern?": "Ĉu vi certe volas forigi la jenan dosiernoman ŝablonon?", + "Are you sure to delete the following shifted season?": "Ĉu vi certe volas forigi la jenan ŝovitan sezonon?", + "Are you sure to delete the following show?": "Ĉu vi certe volas forigi la jenan serion?", + "Are you sure to delete the following {track_type} track?": "Ĉu vi certe volas forigi la jenan {track_type}-trakon?", + "Are you sure to delete this tag?": "Ĉu vi certe volas forigi ĉi tiun etikedon?", + "Audio Layout": "Aŭda aranĝo", + "Back": "Reen", + "Cancel": "Nuligi", + "Cannot add another stream with disposition flag 'default' or 'forced' set": "Ne eblas aldoni alian fluon kun la dispozicia flago 'default' aŭ 'forced' aktiva", + "Changes applied and file reloaded.": "Ŝanĝoj aplikitaj kaj dosiero reŝargita.", + "Cleanup": "Purigado", + "Cleanup disabled.": "Purigado malŝaltita.", + "Cleanup enabled.": "Purigado ŝaltita.", + "Codec": "Kodeko", + "Continuing edit session.": "Daŭrigante la redaktan seancon.", + "Default": "Defaŭlta", + "Delete": "Forigi", + "Delete Show": "Forigi serion", + "Deleted media tag {tag!r}.": "Forigis la aŭdvidan etikedon {tag!r}.", + "Differences": "Diferencoj", + "Differences (file->db/output)": "Diferencoj (dosiero->DB/eligo)", + "Discard": "Forĵeti", + "Discard pending metadata changes and quit?": "Ĉu forĵeti atendatajn metadatumajn ŝanĝojn kaj eliri?", + "Discard pending metadata changes and reload the file state?": "Ĉu forĵeti atendatajn metadatumajn ŝanĝojn kaj reŝargi la dosieran staton?", + "Down": "Malsupren", + "Dry-run: would rewrite via temporary file {target_path}": "Seka provo: reskribus per provizora dosiero {target_path}", + "Edit": "Redakti", + "Edit Pattern": "Redakti ŝablonon", + "Edit Show": "Redakti serion", + "Edit filename pattern": "Redakti dosiernoman ŝablonon", + "Edit shifted season": "Redakti ŝovitan sezonon", + "Edit stream": "Redakti fluon", + "Episode Offset": "Epizoda deŝovo", + "Episode offset": "Epizoda deŝovo", + "File": "Dosiero", + "File patterns": "Dosieraj ŝablonoj", + "First Episode": "Unua epizodo", + "First episode": "Unua epizodo", + "Forced": "Devigita", + "Help": "Helpo", + "Help Screen": "Helpa ekrano", + "ID": "ID", + "Identify": "Identigi", + "Index": "Indekso", + "Index / Subindex": "Indekso / Subindekso", + "Index Episode Digits": "Ciferoj de epizoda indekso", + "Index Season Digits": "Ciferoj de sezona indekso", + "Indicator Edisode Digits": "Ciferoj de epizoda indikilo", + "Indicator Season Digits": "Ciferoj de sezona indikilo", + "Keep Editing": "Daŭrigi redaktadon", + "Keeping pending changes.": "Konservas atendatajn ŝanĝojn.", + "Key": "Ŝlosilo", + "Language": "Lingvo", + "Last Episode": "Lasta epizodo", + "Last episode": "Lasta epizodo", + "Layout": "Aranĝo", + "Media Tags": "Aŭdvidaj etikedoj", + "More than one default audio stream detected and no prompt set": "Pli ol unu defaŭlta sonfluo detektita kaj neniu instigo agordita", + "More than one default audio stream detected! Please select stream": "Pli ol unu defaŭlta sonfluo detektita! Bonvolu elekti fluon", + "More than one default subtitle stream detected and no prompt set": "Pli ol unu defaŭlta subtitola fluo detektita kaj neniu instigo agordita", + "More than one default subtitle stream detected! Please select stream": "Pli ol unu defaŭlta subtitola fluo detektita! Bonvolu elekti fluon", + "More than one default video stream detected and no prompt set": "Pli ol unu defaŭlta videofluo detektita kaj neniu instigo agordita", + "More than one default video stream detected! Please select stream": "Pli ol unu defaŭlta videofluo detektita! Bonvolu elekti fluon", + "More than one forced audio stream detected and no prompt set": "Pli ol unu devigita sonfluo detektita kaj neniu instigo agordita", + "More than one forced audio stream detected! Please select stream": "Pli ol unu devigita sonfluo detektita! Bonvolu elekti fluon", + "More than one forced subtitle stream detected and no prompt set": "Pli ol unu devigita subtitola fluo detektita kaj neniu instigo agordita", + "More than one forced subtitle stream detected! Please select stream": "Pli ol unu devigita subtitola fluo detektita! Bonvolu elekti fluon", + "More than one forced video stream detected and no prompt set": "Pli ol unu devigita videofluo detektita kaj neniu instigo agordita", + "More than one forced video stream detected! Please select stream": "Pli ol unu devigita videofluo detektita! Bonvolu elekti fluon", + "Name": "Nomo", + "New Pattern": "Nova ŝablono", + "New Show": "Nova serio", + "New filename pattern": "Nova dosiernoma ŝablono", + "New shifted season": "Nova ŝovita sezono", + "New stream": "Nova fluo", + "No": "Ne", + "No changes to apply.": "Neniuj ŝanĝoj por apliki.", + "No changes to revert.": "Neniuj ŝanĝoj por malfari.", + "Normalization disabled.": "Normaligo malŝaltita.", + "Normalization enabled.": "Normaligo ŝaltita.", + "Normalize": "Normaligi", + "Notes": "Notoj", + "Pattern": "Ŝablono", + "Planned Changes (file->edited output)": "Planitaj ŝanĝoj (dosiero->redaktita eligo)", + "Quality": "Kvalito", + "Quit": "Eliri", + "Remove Pattern": "Forigi ŝablonon", + "Revert": "Malfari", + "Reverted pending changes.": "Malfaris atendatajn ŝanĝojn.", + "Save": "Konservi", + "Season Offset": "Sezona deŝovo", + "Select a stream first.": "Bonvolu unue elekti fluon.", + "Set Default": "Agordi kiel defaŭltan", + "Set Forced": "Agordi kiel devigitan", + "Settings Screen": "Agorda ekrano", + "Numbering Mapping": "Ŝovitaj sezonoj", + "Show": "Serio", + "Shows": "Serioj", + "Source Season": "Fonta sezono", + "SrcIndex": "Fontindekso", + "Status": "Stato", + "Stay": "Resti", + "Stream dispositions": "Fluaj dispozicioj", + "Stream tags": "Fluaj etikedoj", + "Streams": "Fluoj", + "SubIndex": "Subindekso", + "Substitute": "Anstataŭigi", + "Substitute pattern": "Anstataŭigi ŝablonon", + "Title": "Titolo", + "Type": "Tipo", + "Unable to update selected stream.": "Ne eblis ĝisdatigi la elektitan fluon.", + "Up": "Supren", + "Update Pattern": "Ĝisdatigi ŝablonon", + "Updated media tag {tag!r}.": "Ĝisdatigis la aŭdvidan etikedon {tag!r}.", + "Updated stream #{index} ({track_type}).": "Ĝisdatigis fluon #{index} ({track_type}).", + "Value": "Valoro", + "Year": "Jaro", + "Yes": "Jes", + "add media tag: key='{key}' value='{value}'": "aldoni aŭdvidan etikedon: ŝlosilo='{key}' valoro='{value}'", + "add {track_type} track: index={index} lang={language}": "aldoni {track_type}-trakon: indekso={index} lingvo={language}", + "attached_pic": "attached_pic", + "attachment": "aldonaĵo", + "audio": "sono", + "captions": "subtekstoj", + "change media tag: key='{key}' value='{value}'": "ŝanĝi aŭdvidan etikedon: ŝlosilo='{key}' valoro='{value}'", + "change stream #{index} ({track_type}:{sub_index}) add disposition={disposition}": "ŝanĝi fluon #{index} ({track_type}:{sub_index}) aldoni dispozicion={disposition}", + "change stream #{index} ({track_type}:{sub_index}) add key={key} value={value}": "ŝanĝi fluon #{index} ({track_type}:{sub_index}) aldoni ŝlosilon={key} valoron={value}", + "change stream #{index} ({track_type}:{sub_index}) change key={key} value={value}": "ŝanĝi fluon #{index} ({track_type}:{sub_index}) ŝanĝi ŝlosilon={key} valoron={value}", + "change stream #{index} ({track_type}:{sub_index}) remove disposition={disposition}": "ŝanĝi fluon #{index} ({track_type}:{sub_index}) forigi dispozicion={disposition}", + "change stream #{index} ({track_type}:{sub_index}) remove key={key} value={value}": "ŝanĝi fluon #{index} ({track_type}:{sub_index}) forigi ŝlosilon={key} valoron={value}", + "clean_effects": "nur efektoj", + "comment": "komento", + "default": "defaŭlta", + "dependent": "dependa", + "descriptions": "priskriboj", + "dub": "dublado", + "for pattern": "por ŝablono", + "forced": "devigita", + "from": "de", + "from pattern": "de ŝablono", + "from show": "el serio", + "hearing_impaired": "aŭdmalhelpita", + "karaoke": "karaokeo", + "lyrics": "kantoteksto", + "metadata": "metadatenoj", + "non_diegetic": "nediĝeta", + "original": "originala", + "pattern #{id}": "ŝablono #{id}", + "remove media tag: key='{key}' value='{value}'": "forigi aŭdvidan etikedon: ŝlosilo='{key}' valoro='{value}'", + "remove stream #{index}": "forigi fluon #{index}", + "show #{id}": "serio #{id}", + "stereo": "stereo", + "still_image": "senmova bildo", + "sub index": "subindekso", + "subtitle": "subtitolo", + "timed_thumbnails": "tempigitaj bildetoj", + "undefined": "nedifinita", + "unknown": "nekonata", + "video": "video", + "visual_impaired": "vidmalhelpita" + } +} diff --git a/assets/i18n/es.json b/assets/i18n/es.json new file mode 100644 index 0000000..8d80b95 --- /dev/null +++ b/assets/i18n/es.json @@ -0,0 +1,361 @@ +{ + "iso_languages": { + "ABKHAZIAN": "Abjaziano", + "AFAR": "Afar", + "AFRIKAANS": "Afrikaans", + "AKAN": "Akan", + "ALBANIAN": "Albanés", + "AMHARIC": "Ámárico", + "ARABIC": "Árábe", + "ARAGONESE": "Aragonés", + "ARMENIAN": "Armenio", + "ASSAMESE": "Assamais", + "AVARIC": "Avaric", + "AVESTAN": "Avestan", + "AYMARA": "Aymará", + "AZERBAIJANI": "Azerbayano", + "BAMBARA": "Bambara", + "BASHKIR": "Bashkir", + "BASQUE": "Vasco", + "BELARUSIAN": "Bieloruso", + "BENGALI": "Bengalí", + "BISLAMA": "Bislama", + "BOKMAL": "Bokmål", + "BOSNIAN": "Bosnio", + "BRETON": "Bretón", + "BULGARIAN": "Búlgaro", + "BURMESE": "Birmano", + "CATALAN": "Catalan", + "CHAMORRO": "Chamorro", + "CHECHEN": "Checheno", + "CHICHEWA": "Chichewa", + "CHINESE": "Chino", + "CHURCH_SLAVIC": "Church Slavic", + "CHUVASH": "Chuvash", + "CORNISH": "Córnico", + "CORSICAN": "Corso", + "CREE": "Cree", + "CROATIAN": "Croata", + "CZECH": "Checo", + "DANISH": "Danés", + "DIVEHI": "Divehi", + "DUTCH": "Dutch", + "DZONGKHA": "Butaní", + "ENGLISH": "Inglés", + "ESPERANTO": "Esperanto", + "ESTONIAN": "Estonio", + "EWE": "Ewe", + "FAROESE": "Feroés", + "FIJIAN": "Fidji", + "FILIPINO": "Filipino", + "FINNISH": "Finés", + "FRENCH": "Francés", + "FULAH": "Fulah", + "GALICIAN": "Gallego", + "GANDA": "Ganda", + "GEORGIAN": "Georgiano", + "GERMAN": "Alemán", + "GREEK": "Greek", + "GUARANI": "Guaraní", + "GUJARATI": "guyaratí", + "HAITIAN": "Haitian", + "HAUSA": "Haussa", + "HEBREW": "Hebreo", + "HERERO": "Herero", + "HINDI": "Hindi", + "HIRI_MOTU": "Hiri Motu", + "HUNGARIAN": "Húngaro", + "ICELANDIC": "Islandés", + "IDO": "Ido", + "IGBO": "Igbo", + "INDONESIAN": "Indonesio", + "INTERLINGUA": "Interlingua", + "INTERLINGUE": "Interlingue", + "INUKTITUT": "Inuktitut", + "INUPIAQ": "Inupiak", + "IRISH": "Irlandés", + "ITALIAN": "Italiano", + "JAPANESE": "Japonés", + "JAVANESE": "Javanés", + "KALAALLISUT": "Kalaallisut", + "KANNADA": "Canarés", + "KANURI": "Kanuri", + "KASHMIRI": "Kashmir", + "KAZAKH": "Kazako", + "KHMER": "Khmer", + "KIKUYU": "Kikuyu", + "KINYARWANDA": "Kinyarwanda", + "KIRGHIZ": "Kirghiz", + "KOMI": "Komi", + "KONGO": "Kongo", + "KOREAN": "Coreano", + "KUANYAMA": "Kuanyama", + "KURDISH": "Kurdo", + "LAO": "laosiano", + "LATIN": "Latín", + "LATVIAN": "Letón", + "LIMBURGAN": "Limburgan", + "LINGALA": "Lingala", + "LITHUANIAN": "Lituano", + "LUBA_KATANGA": "Luba-Katanga", + "LUXEMBOURGISH": "Luxembourgish", + "MACEDONIAN": "Macedonio", + "MALAGASY": "Malgache", + "MALAY": "Malayo", + "MALAYALAM": "malabar", + "MALTESE": "Maltés", + "MANX": "Manx [Gaélico de Manx]", + "MAORI": "Maorí", + "MARATHI": "Marath", + "MARSHALLESE": "Marshall", + "MONGOLIAN": "Mongol", + "NAURU": "Nauru", + "NAVAJO": "Navajo", + "NDONGA": "Ndonga", + "NEPALI": "Nepalés", + "NORTHERN_SAMI": "Sami del Norte", + "NORTH_NDEBELE": "North Ndebele", + "NORWEGIAN": "Noruego", + "NORWEGIAN_NYNORSK": "Nynorsk", + "OCCITAN": "Occitan", + "OJIBWA": "Ojibwa", + "ORIYA": "Oriya", + "OROMO": "Oromo (Afan)", + "OSSETIAN": "Ossetian", + "PALI": "Pali", + "PANJABI": "Panjabi", + "PERSIAN": "Persa", + "POLISH": "Polaco", + "PORTUGUESE": "Portugués", + "PUSHTO": "Pushto", + "QUECHUA": "Quechua", + "ROMANIAN": "Romanian", + "ROMANSH": "Romaní", + "RUNDI": "Kiroundi", + "RUSSIAN": "Ruso", + "SAMOAN": "Samoano", + "SANGO": "Sango", + "SANSKRIT": "Sánscrito", + "SARDINIAN": "Sardo", + "SCOTTISH_GAELIC": "Scottish Gaelic", + "SERBIAN": "Serbio", + "SHONA": "Shona", + "SICHUAN_YI": "Sichuan Yi", + "SINDHI": "Sindhi", + "SINHALA": "Sinhala", + "SLOVAK": "Eslovaco", + "SLOVENIAN": "Esloveno", + "SOMALI": "Somalí", + "SOUTHERN_SOTHO": "Southern Sotho", + "SOUTH_NDEBELE": "South Ndebele", + "SPANISH": "Spanish", + "SUNDANESE": "Sondanés", + "SWAHILI": "Swahili", + "SWATI": "Siswati", + "SWEDISH": "Sueco", + "TAGALOG": "Tagalo", + "TAHITIAN": "Tahitiano", + "TAJIK": "Tajiko", + "TAMIL": "Tamil", + "TATAR": "Tataro", + "TELUGU": "Telugu", + "THAI": "Tailandés", + "TIBETAN": "Tibetano", + "TIGRINYA": "Tigrinya", + "TONGA": "Tonga", + "TSONGA": "Tsonga", + "TSWANA": "Setchwana", + "TURKISH": "Turco", + "TURKMEN": "Turkmeno", + "TWI": "Tchi", + "UIGHUR": "Uighur", + "UKRAINIAN": "Ukranio", + "UNDEFINED": "undefined", + "URDU": "Urdu", + "UZBEK": "Uzbeko", + "VENDA": "Venda", + "VIETNAMESE": "Vietnamita", + "VOLAPUK": "Volapük", + "WALLOON": "valón", + "WELSH": "Galés", + "WESTERN_FRISIAN": "Frisón occidental", + "WOLOF": "Wolof", + "XHOSA": "Xhosa", + "YIDDISH": "Yidish", + "YORUBA": "Yoruba", + "ZHUANG": "Zhuang", + "ZULU": "Zulu" + }, + "phrases": { + "5.0(side)": "5.0(side)", + "5.1(side)": "5.1(side)", + "6.1": "6.1", + "6ch": "6ch", + "7.1": "7.1", + "": "", + "Add": "Añadir", + "Add Pattern": "Añadir patrón", + "Apply": "Aplicar", + "Apply failed: {error}": "Error al aplicar: {error}", + "Are you sure to delete the following filename pattern?": "¿Seguro que quieres eliminar el siguiente patrón de nombre de archivo?", + "Are you sure to delete the following shifted season?": "¿Seguro que quieres eliminar la siguiente temporada desplazada?", + "Are you sure to delete the following show?": "¿Seguro que quieres eliminar la siguiente serie?", + "Are you sure to delete the following {track_type} track?": "¿Seguro que quieres eliminar la pista {track_type} siguiente?", + "Are you sure to delete this tag?": "¿Seguro que quieres eliminar esta etiqueta?", + "Audio Layout": "Disposición de audio", + "Back": "Volver", + "Cancel": "Cancelar", + "Cannot add another stream with disposition flag 'default' or 'forced' set": "No se puede añadir otro flujo con la marca de disposición 'default' o 'forced' activada", + "Changes applied and file reloaded.": "Cambios aplicados y archivo recargado.", + "Cleanup": "Limpieza", + "Cleanup disabled.": "Limpieza desactivada.", + "Cleanup enabled.": "Limpieza activada.", + "Codec": "Códec", + "Continuing edit session.": "Continuando la sesión de edición.", + "Default": "Predeterminado", + "Delete": "Eliminar", + "Delete Show": "Eliminar serie", + "Deleted media tag {tag!r}.": "Etiqueta de medios {tag!r} eliminada.", + "Differences": "Diferencias", + "Differences (file->db/output)": "Diferencias (archivo->BD/salida)", + "Discard": "Descartar", + "Discard pending metadata changes and quit?": "¿Descartar los cambios pendientes de metadatos y salir?", + "Discard pending metadata changes and reload the file state?": "¿Descartar los cambios pendientes de metadatos y recargar el estado del archivo?", + "Down": "Abajo", + "Dry-run: would rewrite via temporary file {target_path}": "Simulación: reescribiría mediante el archivo temporal {target_path}", + "Edit": "Editar", + "Edit Pattern": "Editar patrón", + "Edit Show": "Editar serie", + "Edit filename pattern": "Editar patrón de nombre de archivo", + "Edit shifted season": "Editar temporada desplazada", + "Edit stream": "Editar flujo", + "Episode Offset": "Desplazamiento de episodio", + "Episode offset": "Desplazamiento de episodio", + "File": "Archivo", + "File patterns": "Patrones de archivo", + "First Episode": "Primer episodio", + "First episode": "Primer episodio", + "Forced": "Forzado", + "Help": "Ayuda", + "Help Screen": "Pantalla de ayuda", + "ID": "ID", + "Identify": "Identificar", + "Index": "Índice", + "Index / Subindex": "Índice / Subíndice", + "Index Episode Digits": "Dígitos del índice de episodio", + "Index Season Digits": "Dígitos del índice de temporada", + "Indicator Edisode Digits": "Dígitos del indicador de episodio", + "Indicator Season Digits": "Dígitos del indicador de temporada", + "Keep Editing": "Seguir editando", + "Keeping pending changes.": "Se conservan los cambios pendientes.", + "Key": "Clave", + "Language": "Idioma", + "Last Episode": "Último episodio", + "Last episode": "Último episodio", + "Layout": "Diseño", + "Media Tags": "Etiquetas de medios", + "More than one default audio stream detected and no prompt set": "Se detectó más de un flujo de audio predeterminado y no hay aviso configurado", + "More than one default audio stream detected! Please select stream": "Se detectó más de un flujo de audio predeterminado. Selecciona el flujo", + "More than one default subtitle stream detected and no prompt set": "Se detectó más de un flujo de subtítulos predeterminado y no hay aviso configurado", + "More than one default subtitle stream detected! Please select stream": "Se detectó más de un flujo de subtítulos predeterminado. Selecciona el flujo", + "More than one default video stream detected and no prompt set": "Se detectó más de un flujo de vídeo predeterminado y no hay aviso configurado", + "More than one default video stream detected! Please select stream": "Se detectó más de un flujo de vídeo predeterminado. Selecciona el flujo", + "More than one forced audio stream detected and no prompt set": "Se detectó más de un flujo de audio forzado y no hay aviso configurado", + "More than one forced audio stream detected! Please select stream": "Se detectó más de un flujo de audio forzado. Selecciona el flujo", + "More than one forced subtitle stream detected and no prompt set": "Se detectó más de un flujo de subtítulos forzados y no hay aviso configurado", + "More than one forced subtitle stream detected! Please select stream": "Se detectó más de un flujo de subtítulos forzados. Selecciona el flujo", + "More than one forced video stream detected and no prompt set": "Se detectó más de un flujo de vídeo forzado y no hay aviso configurado", + "More than one forced video stream detected! Please select stream": "Se detectó más de un flujo de vídeo forzado. Selecciona el flujo", + "Name": "Nombre", + "New Pattern": "Nuevo patrón", + "New Show": "Nueva serie", + "New filename pattern": "Nuevo patrón de nombre de archivo", + "New shifted season": "Nueva temporada desplazada", + "New stream": "Nuevo flujo", + "No": "No", + "No changes to apply.": "No hay cambios para aplicar.", + "No changes to revert.": "No hay cambios para revertir.", + "Normalization disabled.": "Normalización desactivada.", + "Normalization enabled.": "Normalización activada.", + "Normalize": "Normalizar", + "Notes": "Notas", + "Pattern": "Patrón", + "Planned Changes (file->edited output)": "Cambios planificados (archivo->salida editada)", + "Quality": "Calidad", + "Quit": "Salir", + "Remove Pattern": "Eliminar patrón", + "Revert": "Revertir", + "Reverted pending changes.": "Se revirtieron los cambios pendientes.", + "Save": "Guardar", + "Season Offset": "Desplazamiento de temporada", + "Select a stream first.": "Selecciona primero un flujo.", + "Set Default": "Establecer como predeterminado", + "Set Forced": "Establecer como forzado", + "Settings Screen": "Pantalla de ajustes", + "Numbering Mapping": "Temporadas desplazadas", + "Show": "Serie", + "Shows": "Series", + "Source Season": "Temporada de origen", + "SrcIndex": "Índice origen", + "Status": "Estado", + "Stay": "Permanecer", + "Stream dispositions": "Disposiciones del flujo", + "Stream tags": "Etiquetas del flujo", + "Streams": "Flujos", + "SubIndex": "Subíndice", + "Substitute": "Sustituir", + "Substitute pattern": "Sustituir patrón", + "Title": "Título", + "Type": "Tipo", + "Unable to update selected stream.": "No se pudo actualizar el flujo seleccionado.", + "Up": "Arriba", + "Update Pattern": "Actualizar patrón", + "Updated media tag {tag!r}.": "Etiqueta de medios {tag!r} actualizada.", + "Updated stream #{index} ({track_type}).": "Flujo #{index} ({track_type}) actualizado.", + "Value": "Valor", + "Year": "Año", + "Yes": "Sí", + "add media tag: key='{key}' value='{value}'": "añadir etiqueta de medios: clave='{key}' valor='{value}'", + "add {track_type} track: index={index} lang={language}": "añadir pista {track_type}: índice={index} idioma={language}", + "attached_pic": "attached_pic", + "attachment": "adjunto", + "audio": "audio", + "captions": "subtítulos", + "change media tag: key='{key}' value='{value}'": "cambiar etiqueta de medios: clave='{key}' valor='{value}'", + "change stream #{index} ({track_type}:{sub_index}) add disposition={disposition}": "cambiar flujo #{index} ({track_type}:{sub_index}) añadir disposición={disposition}", + "change stream #{index} ({track_type}:{sub_index}) add key={key} value={value}": "cambiar flujo #{index} ({track_type}:{sub_index}) añadir clave={key} valor={value}", + "change stream #{index} ({track_type}:{sub_index}) change key={key} value={value}": "cambiar flujo #{index} ({track_type}:{sub_index}) cambiar clave={key} valor={value}", + "change stream #{index} ({track_type}:{sub_index}) remove disposition={disposition}": "cambiar flujo #{index} ({track_type}:{sub_index}) quitar disposición={disposition}", + "change stream #{index} ({track_type}:{sub_index}) remove key={key} value={value}": "cambiar flujo #{index} ({track_type}:{sub_index}) quitar clave={key} valor={value}", + "clean_effects": "solo efectos", + "comment": "comentario", + "default": "predeterminado", + "dependent": "dependiente", + "descriptions": "descripciones", + "dub": "doblaje", + "for pattern": "para el patrón", + "forced": "forzado", + "from": "de", + "from pattern": "del patrón", + "from show": "de la serie", + "hearing_impaired": "personas con discapacidad auditiva", + "karaoke": "karaoke", + "lyrics": "letra", + "metadata": "metadatos", + "non_diegetic": "no diegético", + "original": "original", + "pattern #{id}": "patrón #{id}", + "remove media tag: key='{key}' value='{value}'": "eliminar etiqueta de medios: clave='{key}' valor='{value}'", + "remove stream #{index}": "eliminar flujo #{index}", + "show #{id}": "serie #{id}", + "stereo": "estéreo", + "still_image": "imagen fija", + "sub index": "subíndice", + "subtitle": "subtítulo", + "timed_thumbnails": "miniaturas temporizadas", + "undefined": "indefinido", + "unknown": "desconocido", + "video": "vídeo", + "visual_impaired": "personas con discapacidad visual" + } +} diff --git a/assets/i18n/fr.json b/assets/i18n/fr.json new file mode 100644 index 0000000..18130ce --- /dev/null +++ b/assets/i18n/fr.json @@ -0,0 +1,361 @@ +{ + "iso_languages": { + "ABKHAZIAN": "Abkhaze", + "AFAR": "Afar", + "AFRIKAANS": "Afrikaans", + "AKAN": "Akan", + "ALBANIAN": "Albanais", + "AMHARIC": "Amharique", + "ARABIC": "Arabe", + "ARAGONESE": "Aragonais", + "ARMENIAN": "Arménien", + "ASSAMESE": "Assamais", + "AVARIC": "Avar", + "AVESTAN": "Avestique", + "AYMARA": "Aymara", + "AZERBAIJANI": "Azéri", + "BAMBARA": "Bambara", + "BASHKIR": "Bachkir", + "BASQUE": "Basque", + "BELARUSIAN": "Biélorusse", + "BENGALI": "Bengali", + "BISLAMA": "Bichelamar", + "BOKMAL": "Bokmål", + "BOSNIAN": "Bosniaque", + "BRETON": "Breton", + "BULGARIAN": "Bulgare", + "BURMESE": "Birman", + "CATALAN": "Catalan", + "CHAMORRO": "Chamorro", + "CHECHEN": "Tchétchène", + "CHICHEWA": "Chichewa", + "CHINESE": "Chinois", + "CHURCH_SLAVIC": "Church Slavic", + "CHUVASH": "Tchouvache", + "CORNISH": "Cornique", + "CORSICAN": "Corse", + "CREE": "Cri", + "CROATIAN": "Croate", + "CZECH": "Tchèque", + "DANISH": "Danois", + "DIVEHI": "Divehi", + "DUTCH": "Dutch", + "DZONGKHA": "Dzongkha", + "ENGLISH": "Anglais", + "ESPERANTO": "Espéranto", + "ESTONIAN": "Estonien", + "EWE": "Éwé", + "FAROESE": "Féroïen", + "FIJIAN": "Fidjien", + "FILIPINO": "Filipino", + "FINNISH": "Finnois", + "FRENCH": "Français", + "FULAH": "Peul", + "GALICIAN": "Galicien", + "GANDA": "Ganda", + "GEORGIAN": "Géorgien", + "GERMAN": "Allemand", + "GREEK": "Greek", + "GUARANI": "Guarani", + "GUJARATI": "Goudjarâtî (Gujrâtî)", + "HAITIAN": "Haitian", + "HAUSA": "Haoussa", + "HEBREW": "Hébreu", + "HERERO": "Herero", + "HINDI": "Hindi", + "HIRI_MOTU": "Hiri Motu", + "HUNGARIAN": "Hongrois", + "ICELANDIC": "Islandais", + "IDO": "Ido", + "IGBO": "Igbo", + "INDONESIAN": "Indonésien", + "INTERLINGUA": "Interlingua", + "INTERLINGUE": "Interlingue", + "INUKTITUT": "Inuktitut", + "INUPIAQ": "Inupiaq", + "IRISH": "Irlandais", + "ITALIAN": "Italien", + "JAPANESE": "Japonais", + "JAVANESE": "Javanais", + "KALAALLISUT": "Kalaallisut", + "KANNADA": "Kannara (Canara)", + "KANURI": "Kanouri", + "KASHMIRI": "Kashmiri", + "KAZAKH": "Kazakh", + "KHMER": "Khmer", + "KIKUYU": "Kikuyu", + "KINYARWANDA": "Kinyarwanda", + "KIRGHIZ": "Kirghiz", + "KOMI": "Komi", + "KONGO": "Kongo", + "KOREAN": "Coréen", + "KUANYAMA": "Kuanyama", + "KURDISH": "Kurde", + "LAO": "Laotien", + "LATIN": "Latin", + "LATVIAN": "Letton", + "LIMBURGAN": "Limburgan", + "LINGALA": "Lingala", + "LITHUANIAN": "Lituanien", + "LUBA_KATANGA": "Luba-katanga", + "LUXEMBOURGISH": "Luxembourgish", + "MACEDONIAN": "Macédonien", + "MALAGASY": "Malgache", + "MALAY": "Malais", + "MALAYALAM": "Malayalam", + "MALTESE": "Maltais", + "MANX": "Mannois", + "MAORI": "Maori", + "MARATHI": "Marathe", + "MARSHALLESE": "Marshallais", + "MONGOLIAN": "Mongol", + "NAURU": "Nauru", + "NAVAJO": "Navajo", + "NDONGA": "Ndonga", + "NEPALI": "Népalais", + "NORTHERN_SAMI": "Same du Nord", + "NORTH_NDEBELE": "North Ndebele", + "NORWEGIAN": "Norvégien", + "NORWEGIAN_NYNORSK": "Nynorsk", + "OCCITAN": "Occitan", + "OJIBWA": "Ojibwa", + "ORIYA": "Oriya", + "OROMO": "Oromo", + "OSSETIAN": "Ossetian", + "PALI": "Pali", + "PANJABI": "Panjabi", + "PERSIAN": "Persan", + "POLISH": "Polonais", + "PORTUGUESE": "Portugais", + "PUSHTO": "Pushto", + "QUECHUA": "Quechua", + "ROMANIAN": "Romanian", + "ROMANSH": "Romanche", + "RUNDI": "Rundi", + "RUSSIAN": "Russe", + "SAMOAN": "Samoan", + "SANGO": "Sango", + "SANSKRIT": "Sanskrit", + "SARDINIAN": "Sarde", + "SCOTTISH_GAELIC": "Scottish Gaelic", + "SERBIAN": "Serbe", + "SHONA": "Shona", + "SICHUAN_YI": "Sichuan Yi", + "SINDHI": "Sindhi", + "SINHALA": "Sinhala", + "SLOVAK": "Slovaque", + "SLOVENIAN": "Slovène", + "SOMALI": "Somali", + "SOUTHERN_SOTHO": "Southern Sotho", + "SOUTH_NDEBELE": "South Ndebele", + "SPANISH": "Spanish", + "SUNDANESE": "Sundanais", + "SWAHILI": "Swahili", + "SWATI": "Swati", + "SWEDISH": "Suédois", + "TAGALOG": "Tagalog", + "TAHITIAN": "Tahitien", + "TAJIK": "Tadjik", + "TAMIL": "Tamoul", + "TATAR": "Tatar", + "TELUGU": "Télougou", + "THAI": "Thaï", + "TIBETAN": "Tibétain", + "TIGRINYA": "Tigrigna", + "TONGA": "Tonga", + "TSONGA": "Tsonga", + "TSWANA": "Tswana", + "TURKISH": "Turc", + "TURKMEN": "Turkmène", + "TWI": "Twi", + "UIGHUR": "Uighur", + "UKRAINIAN": "Ukrainien", + "UNDEFINED": "undefined", + "URDU": "Ourdou", + "UZBEK": "Ouszbek", + "VENDA": "Venda", + "VIETNAMESE": "Vietnamien", + "VOLAPUK": "Volapük", + "WALLOON": "Wallon", + "WELSH": "Gallois", + "WESTERN_FRISIAN": "Frison occidental", + "WOLOF": "Wolof", + "XHOSA": "Xhosa", + "YIDDISH": "Yiddish", + "YORUBA": "Yoruba", + "ZHUANG": "Zhuang", + "ZULU": "Zoulou" + }, + "phrases": { + "5.0(side)": "5.0(side)", + "5.1(side)": "5.1(side)", + "6.1": "6.1", + "6ch": "6ch", + "7.1": "7.1", + "": "", + "Add": "Ajouter", + "Add Pattern": "Ajouter un modèle", + "Apply": "Appliquer", + "Apply failed: {error}": "Échec de l'application : {error}", + "Are you sure to delete the following filename pattern?": "Voulez-vous vraiment supprimer le modèle de nom de fichier suivant ?", + "Are you sure to delete the following shifted season?": "Voulez-vous vraiment supprimer la saison décalée suivante ?", + "Are you sure to delete the following show?": "Voulez-vous vraiment supprimer la série suivante ?", + "Are you sure to delete the following {track_type} track?": "Voulez-vous vraiment supprimer la piste {track_type} suivante ?", + "Are you sure to delete this tag?": "Voulez-vous vraiment supprimer cette balise ?", + "Audio Layout": "Disposition audio", + "Back": "Retour", + "Cancel": "Annuler", + "Cannot add another stream with disposition flag 'default' or 'forced' set": "Impossible d'ajouter un autre flux avec l'indicateur de disposition 'default' ou 'forced'", + "Changes applied and file reloaded.": "Modifications appliquées et fichier rechargé.", + "Cleanup": "Nettoyage", + "Cleanup disabled.": "Nettoyage désactivé.", + "Cleanup enabled.": "Nettoyage activé.", + "Codec": "Codec", + "Continuing edit session.": "Poursuite de la session d'édition.", + "Default": "Par défaut", + "Delete": "Supprimer", + "Delete Show": "Supprimer la série", + "Deleted media tag {tag!r}.": "Balise média {tag!r} supprimée.", + "Differences": "Différences", + "Differences (file->db/output)": "Différences (fichier->BD/sortie)", + "Discard": "Ignorer", + "Discard pending metadata changes and quit?": "Ignorer les modifications de métadonnées en attente et quitter ?", + "Discard pending metadata changes and reload the file state?": "Ignorer les modifications de métadonnées en attente et recharger l'état du fichier ?", + "Down": "Descendre", + "Dry-run: would rewrite via temporary file {target_path}": "Simulation : réécrirait via le fichier temporaire {target_path}", + "Edit": "Modifier", + "Edit Pattern": "Modifier le modèle", + "Edit Show": "Modifier la série", + "Edit filename pattern": "Modifier le modèle de nom de fichier", + "Edit shifted season": "Modifier la saison décalée", + "Edit stream": "Modifier le flux", + "Episode Offset": "Décalage d'épisode", + "Episode offset": "Décalage d'épisode", + "File": "Fichier", + "File patterns": "Modèles de fichiers", + "First Episode": "Premier épisode", + "First episode": "Premier épisode", + "Forced": "Forcé", + "Help": "Aide", + "Help Screen": "Écran d'aide", + "ID": "ID", + "Identify": "Identifier", + "Index": "Index", + "Index / Subindex": "Index / Sous-index", + "Index Episode Digits": "Chiffres d'épisode d'index", + "Index Season Digits": "Chiffres de saison d'index", + "Indicator Edisode Digits": "Chiffres d'épisode de l'indicateur", + "Indicator Season Digits": "Chiffres de saison de l'indicateur", + "Keep Editing": "Continuer l'édition", + "Keeping pending changes.": "Les modifications en attente sont conservées.", + "Key": "Clé", + "Language": "Langue", + "Last Episode": "Dernier épisode", + "Last episode": "Dernier épisode", + "Layout": "Disposition", + "Media Tags": "Balises média", + "More than one default audio stream detected and no prompt set": "Plus d'un flux audio par défaut détecté et aucune invite définie", + "More than one default audio stream detected! Please select stream": "Plus d'un flux audio par défaut détecté ! Veuillez sélectionner un flux", + "More than one default subtitle stream detected and no prompt set": "Plus d'un flux de sous-titres par défaut détecté et aucune invite définie", + "More than one default subtitle stream detected! Please select stream": "Plus d'un flux de sous-titres par défaut détecté ! Veuillez sélectionner un flux", + "More than one default video stream detected and no prompt set": "Plus d'un flux vidéo par défaut détecté et aucune invite définie", + "More than one default video stream detected! Please select stream": "Plus d'un flux vidéo par défaut détecté ! Veuillez sélectionner un flux", + "More than one forced audio stream detected and no prompt set": "Plus d'un flux audio forcé détecté et aucune invite définie", + "More than one forced audio stream detected! Please select stream": "Plus d'un flux audio forcé détecté ! Veuillez sélectionner un flux", + "More than one forced subtitle stream detected and no prompt set": "Plus d'un flux de sous-titres forcé détecté et aucune invite définie", + "More than one forced subtitle stream detected! Please select stream": "Plus d'un flux de sous-titres forcé détecté ! Veuillez sélectionner un flux", + "More than one forced video stream detected and no prompt set": "Plus d'un flux vidéo forcé détecté et aucune invite définie", + "More than one forced video stream detected! Please select stream": "Plus d'un flux vidéo forcé détecté ! Veuillez sélectionner un flux", + "Name": "Nom", + "New Pattern": "Nouveau modèle", + "New Show": "Nouvelle série", + "New filename pattern": "Nouveau modèle de nom de fichier", + "New shifted season": "Nouvelle saison décalée", + "New stream": "Nouveau flux", + "No": "Non", + "No changes to apply.": "Aucune modification à appliquer.", + "No changes to revert.": "Aucune modification à annuler.", + "Normalization disabled.": "Normalisation désactivée.", + "Normalization enabled.": "Normalisation activée.", + "Normalize": "Normaliser", + "Notes": "Notes", + "Pattern": "Modèle", + "Planned Changes (file->edited output)": "Modifications prévues (fichier->sortie modifiée)", + "Quality": "Qualité", + "Quit": "Quitter", + "Remove Pattern": "Supprimer le modèle", + "Revert": "Annuler les modifications", + "Reverted pending changes.": "Modifications en attente annulées.", + "Save": "Enregistrer", + "Season Offset": "Décalage de saison", + "Select a stream first.": "Veuillez d'abord sélectionner un flux.", + "Set Default": "Définir par défaut", + "Set Forced": "Définir comme forcé", + "Settings Screen": "Écran des paramètres", + "Numbering Mapping": "Saisons décalées", + "Show": "Série", + "Shows": "Séries", + "Source Season": "Saison source", + "SrcIndex": "Index source", + "Status": "Statut", + "Stay": "Rester", + "Stream dispositions": "Dispositions des flux", + "Stream tags": "Balises du flux", + "Streams": "Flux", + "SubIndex": "Sous-index", + "Substitute": "Remplacer", + "Substitute pattern": "Remplacer le modèle", + "Title": "Titre", + "Type": "Type", + "Unable to update selected stream.": "Impossible de mettre à jour le flux sélectionné.", + "Up": "Monter", + "Update Pattern": "Mettre à jour le modèle", + "Updated media tag {tag!r}.": "Balise média {tag!r} mise à jour.", + "Updated stream #{index} ({track_type}).": "Flux #{index} ({track_type}) mis à jour.", + "Value": "Valeur", + "Year": "Année", + "Yes": "Oui", + "add media tag: key='{key}' value='{value}'": "ajouter une balise média : clé='{key}' valeur='{value}'", + "add {track_type} track: index={index} lang={language}": "ajouter une piste {track_type} : index={index} langue={language}", + "attached_pic": "attached_pic", + "attachment": "pièce jointe", + "audio": "audio", + "captions": "sous-titres", + "change media tag: key='{key}' value='{value}'": "modifier une balise média : clé='{key}' valeur='{value}'", + "change stream #{index} ({track_type}:{sub_index}) add disposition={disposition}": "modifier le flux #{index} ({track_type}:{sub_index}) ajouter disposition={disposition}", + "change stream #{index} ({track_type}:{sub_index}) add key={key} value={value}": "modifier le flux #{index} ({track_type}:{sub_index}) ajouter clé={key} valeur={value}", + "change stream #{index} ({track_type}:{sub_index}) change key={key} value={value}": "modifier le flux #{index} ({track_type}:{sub_index}) changer clé={key} valeur={value}", + "change stream #{index} ({track_type}:{sub_index}) remove disposition={disposition}": "modifier le flux #{index} ({track_type}:{sub_index}) supprimer disposition={disposition}", + "change stream #{index} ({track_type}:{sub_index}) remove key={key} value={value}": "modifier le flux #{index} ({track_type}:{sub_index}) supprimer clé={key} valeur={value}", + "clean_effects": "effets seuls", + "comment": "commentaire", + "default": "par défaut", + "dependent": "dépendant", + "descriptions": "descriptions", + "dub": "doublage", + "for pattern": "pour le modèle", + "forced": "forcé", + "from": "de", + "from pattern": "depuis le modèle", + "from show": "depuis la série", + "hearing_impaired": "malentendants", + "karaoke": "karaoké", + "lyrics": "paroles", + "metadata": "métadonnées", + "non_diegetic": "non diégétique", + "original": "original", + "pattern #{id}": "modèle #{id}", + "remove media tag: key='{key}' value='{value}'": "supprimer une balise média : clé='{key}' valeur='{value}'", + "remove stream #{index}": "supprimer le flux #{index}", + "show #{id}": "série #{id}", + "stereo": "stéréo", + "still_image": "image fixe", + "sub index": "sous-index", + "subtitle": "sous-titre", + "timed_thumbnails": "miniatures horodatées", + "undefined": "indéfini", + "unknown": "inconnu", + "video": "vidéo", + "visual_impaired": "malvoyants" + } +} diff --git a/assets/i18n/ja.json b/assets/i18n/ja.json new file mode 100644 index 0000000..41e6e6f --- /dev/null +++ b/assets/i18n/ja.json @@ -0,0 +1,361 @@ +{ + "iso_languages": { + "ABKHAZIAN": "アブハジア語", + "AFAR": "アファル語", + "AFRIKAANS": "アフリカーンス語", + "AKAN": "アカン語", + "ALBANIAN": "アルバニア語", + "AMHARIC": "アムハラ語", + "ARABIC": "アラビア語", + "ARAGONESE": "アラゴン語", + "ARMENIAN": "アルメニア語", + "ASSAMESE": "アッサム語", + "AVARIC": "アヴァル語", + "AVESTAN": "アヴェスタ語", + "AYMARA": "アイマラ語", + "AZERBAIJANI": "アゼルバイジャン語", + "BAMBARA": "バンバラ語", + "BASHKIR": "バシキール語", + "BASQUE": "バスク語", + "BELARUSIAN": "白ロシア語", + "BENGALI": "ベンガル語", + "BISLAMA": "ビスラマ語", + "BOKMAL": "Bokmål", + "BOSNIAN": "ボスニア語", + "BRETON": "ブルトン語", + "BULGARIAN": "ブルガリア語", + "BURMESE": "ビルマ語", + "CATALAN": "Catalan", + "CHAMORRO": "チャモロ語", + "CHECHEN": "チェチェン語", + "CHICHEWA": "Chichewa", + "CHINESE": "中国語", + "CHURCH_SLAVIC": "Church Slavic", + "CHUVASH": "チュヴァシュ語", + "CORNISH": "コーンウォール語", + "CORSICAN": "コルシカ語", + "CREE": "クリー語", + "CROATIAN": "クロアチア語", + "CZECH": "チェコ語", + "DANISH": "デンマーク語", + "DIVEHI": "Divehi", + "DUTCH": "Dutch", + "DZONGKHA": "ゾンカ語", + "ENGLISH": "英語", + "ESPERANTO": "エスペラント語", + "ESTONIAN": "エストニア語", + "EWE": "エウェ語", + "FAROESE": "フェロー語", + "FIJIAN": "フィジー語", + "FILIPINO": "Filipino", + "FINNISH": "フィン語", + "FRENCH": "フランス語", + "FULAH": "フラ語", + "GALICIAN": "ガリシア語", + "GANDA": "ガンダ語", + "GEORGIAN": "グルジア語", + "GERMAN": "ドイツ語", + "GREEK": "Greek", + "GUARANI": "グアラニー", + "GUJARATI": "グジャラーティー語", + "HAITIAN": "Haitian", + "HAUSA": "ハウサ語", + "HEBREW": "ヘブライ語", + "HERERO": "ヘレロ語", + "HINDI": "ヒンディー語", + "HIRI_MOTU": "ヒリモトゥ語", + "HUNGARIAN": "ハンガリー語", + "ICELANDIC": "アイスランド語", + "IDO": "イド語", + "IGBO": "イボ語", + "INDONESIAN": "インドネシア語", + "INTERLINGUA": "Interlingua", + "INTERLINGUE": "Interlingue", + "INUKTITUT": "イヌクウティトット語", + "INUPIAQ": "イヌピアック語", + "IRISH": "アイルランド語", + "ITALIAN": "イタリア語", + "JAPANESE": "日本語", + "JAVANESE": "ジャワ語", + "KALAALLISUT": "Kalaallisut", + "KANNADA": "カンナダ語", + "KANURI": "カヌリ語", + "KASHMIRI": "カシミーリー語", + "KAZAKH": "カザーフ語", + "KHMER": "Khmer", + "KIKUYU": "Kikuyu", + "KINYARWANDA": "キンヤルワンダ語", + "KIRGHIZ": "Kirghiz", + "KOMI": "コミ語", + "KONGO": "コンゴ語", + "KOREAN": "朝鮮語", + "KUANYAMA": "Kuanyama", + "KURDISH": "クルド語", + "LAO": "ラオ語", + "LATIN": "ラテン語", + "LATVIAN": "ラトビア語", + "LIMBURGAN": "Limburgan", + "LINGALA": "リンガラ語", + "LITHUANIAN": "リトアニア語", + "LUBA_KATANGA": "ルバ語", + "LUXEMBOURGISH": "Luxembourgish", + "MACEDONIAN": "マケドニア語", + "MALAGASY": "マラガシ語", + "MALAY": "マライ語", + "MALAYALAM": "マラヤーラム語", + "MALTESE": "マルタ語", + "MANX": "マン島語", + "MAORI": "マオリ語", + "MARATHI": "マラーティー語", + "MARSHALLESE": "マーシャル語", + "MONGOLIAN": "蒙古語", + "NAURU": "ナウル語", + "NAVAJO": "Navajo", + "NDONGA": "ンドンガ語", + "NEPALI": "ネパール語", + "NORTHERN_SAMI": "北サーミ語", + "NORTH_NDEBELE": "North Ndebele", + "NORWEGIAN": "ノルウェー語", + "NORWEGIAN_NYNORSK": "Nynorsk", + "OCCITAN": "Occitan", + "OJIBWA": "オジブワ語", + "ORIYA": "オリヤー語", + "OROMO": "オロモ語", + "OSSETIAN": "Ossetian", + "PALI": "パーリ語", + "PANJABI": "Panjabi", + "PERSIAN": "ペルシア語", + "POLISH": "ポーランド語", + "PORTUGUESE": "ポルトガル語", + "PUSHTO": "Pushto", + "QUECHUA": "キチュワ語", + "ROMANIAN": "Romanian", + "ROMANSH": "ロマンシュ語", + "RUNDI": "ルンディ語", + "RUSSIAN": "ロシア語", + "SAMOAN": "サモア語", + "SANGO": "サンゴ語", + "SANSKRIT": "梵語", + "SARDINIAN": "サルデーニャ語", + "SCOTTISH_GAELIC": "Scottish Gaelic", + "SERBIAN": "セルビア語", + "SHONA": "ショナ語", + "SICHUAN_YI": "Sichuan Yi", + "SINDHI": "シンディー語", + "SINHALA": "Sinhala", + "SLOVAK": "スロヴァキア語", + "SLOVENIAN": "スロヴェニア語", + "SOMALI": "ソマリ語", + "SOUTHERN_SOTHO": "Southern Sotho", + "SOUTH_NDEBELE": "South Ndebele", + "SPANISH": "Spanish", + "SUNDANESE": "スンダ語", + "SWAHILI": "スワヒリ語", + "SWATI": "シスワティ語", + "SWEDISH": "スウェーデン語", + "TAGALOG": "タガログ語", + "TAHITIAN": "タヒチ語", + "TAJIK": "タジク語", + "TAMIL": "タミル語", + "TATAR": "タタール語", + "TELUGU": "テルグ語", + "THAI": "タイ語", + "TIBETAN": "チベット語", + "TIGRINYA": "ティグリニア語", + "TONGA": "Tonga", + "TSONGA": "ツォンガ語", + "TSWANA": "ツワナ語", + "TURKISH": "トルコ語", + "TURKMEN": "トゥルクメン語", + "TWI": "トウィ語", + "UIGHUR": "Uighur", + "UKRAINIAN": "ウクライナ語", + "UNDEFINED": "undefined", + "URDU": "ウルドゥー語", + "UZBEK": "ウズベク語", + "VENDA": "ベンダ語", + "VIETNAMESE": "ベトナム語", + "VOLAPUK": "ボラピューク語", + "WALLOON": "ワロン語", + "WELSH": "ウェールズ語", + "WESTERN_FRISIAN": "西フリジア語", + "WOLOF": "ウォロフ語", + "XHOSA": "ホサ語", + "YIDDISH": "イディッシュ語", + "YORUBA": "ヨルバ語", + "ZHUANG": "Zhuang", + "ZULU": "ズールー語" + }, + "phrases": { + "5.0(side)": "5.0(side)", + "5.1(side)": "5.1(side)", + "6.1": "6.1", + "6ch": "6ch", + "7.1": "7.1", + "": "<新しい番組>", + "Add": "追加", + "Add Pattern": "パターンを追加", + "Apply": "適用", + "Apply failed: {error}": "適用に失敗しました: {error}", + "Are you sure to delete the following filename pattern?": "次のファイル名パターンを削除してもよろしいですか?", + "Are you sure to delete the following shifted season?": "次のシーズンシフト設定を削除してもよろしいですか?", + "Are you sure to delete the following show?": "次の番組を削除してもよろしいですか?", + "Are you sure to delete the following {track_type} track?": "次の{track_type}ストリームを削除してもよろしいですか?", + "Are you sure to delete this tag?": "このタグを削除してもよろしいですか?", + "Audio Layout": "音声レイアウト", + "Back": "戻る", + "Cancel": "キャンセル", + "Cannot add another stream with disposition flag 'default' or 'forced' set": "default または forced の disposition が設定されたストリームはこれ以上追加できません", + "Changes applied and file reloaded.": "変更を適用し、ファイルを再読み込みしました。", + "Cleanup": "クリーンアップ", + "Cleanup disabled.": "クリーンアップを無効にしました。", + "Cleanup enabled.": "クリーンアップを有効にしました。", + "Codec": "コーデック", + "Continuing edit session.": "編集セッションを続行します。", + "Default": "デフォルト", + "Delete": "削除", + "Delete Show": "番組を削除", + "Deleted media tag {tag!r}.": "メディアタグ {tag!r} を削除しました。", + "Differences": "差分", + "Differences (file->db/output)": "差分 (ファイル->DB/出力)", + "Discard": "破棄", + "Discard pending metadata changes and quit?": "保留中のメタデータ変更を破棄して終了しますか?", + "Discard pending metadata changes and reload the file state?": "保留中のメタデータ変更を破棄してファイル状態を再読み込みしますか?", + "Down": "下へ", + "Dry-run: would rewrite via temporary file {target_path}": "ドライラン: 一時ファイル {target_path} 経由で再書き込みします", + "Edit": "編集", + "Edit Pattern": "パターンを編集", + "Edit Show": "番組を編集", + "Edit filename pattern": "ファイル名パターンを編集", + "Edit shifted season": "シフト済みシーズンを編集", + "Edit stream": "ストリームを編集", + "Episode Offset": "エピソードオフセット", + "Episode offset": "エピソードオフセット", + "File": "ファイル", + "File patterns": "ファイルパターン", + "First Episode": "最初のエピソード", + "First episode": "最初のエピソード", + "Forced": "強制", + "Help": "ヘルプ", + "Help Screen": "ヘルプ画面", + "ID": "ID", + "Identify": "識別", + "Index": "インデックス", + "Index / Subindex": "インデックス / サブインデックス", + "Index Episode Digits": "インデックスのエピソード桁数", + "Index Season Digits": "インデックスのシーズン桁数", + "Indicator Edisode Digits": "インジケーターのエピソード桁数", + "Indicator Season Digits": "インジケーターのシーズン桁数", + "Keep Editing": "編集を続ける", + "Keeping pending changes.": "保留中の変更を保持します。", + "Key": "キー", + "Language": "言語", + "Last Episode": "最後のエピソード", + "Last episode": "最後のエピソード", + "Layout": "レイアウト", + "Media Tags": "メディアタグ", + "More than one default audio stream detected and no prompt set": "デフォルト音声ストリームが複数検出され、プロンプトも設定されていません", + "More than one default audio stream detected! Please select stream": "デフォルト音声ストリームが複数検出されました。ストリームを選択してください", + "More than one default subtitle stream detected and no prompt set": "デフォルト字幕ストリームが複数検出され、プロンプトも設定されていません", + "More than one default subtitle stream detected! Please select stream": "デフォルト字幕ストリームが複数検出されました。ストリームを選択してください", + "More than one default video stream detected and no prompt set": "デフォルト映像ストリームが複数検出され、プロンプトも設定されていません", + "More than one default video stream detected! Please select stream": "デフォルト映像ストリームが複数検出されました。ストリームを選択してください", + "More than one forced audio stream detected and no prompt set": "強制音声ストリームが複数検出され、プロンプトも設定されていません", + "More than one forced audio stream detected! Please select stream": "強制音声ストリームが複数検出されました。ストリームを選択してください", + "More than one forced subtitle stream detected and no prompt set": "強制字幕ストリームが複数検出され、プロンプトも設定されていません", + "More than one forced subtitle stream detected! Please select stream": "強制字幕ストリームが複数検出されました。ストリームを選択してください", + "More than one forced video stream detected and no prompt set": "強制映像ストリームが複数検出され、プロンプトも設定されていません", + "More than one forced video stream detected! Please select stream": "強制映像ストリームが複数検出されました。ストリームを選択してください", + "Name": "名前", + "New Pattern": "新しいパターン", + "New Show": "新しい番組", + "New filename pattern": "新しいファイル名パターン", + "New shifted season": "新しいシーズンシフト", + "New stream": "新しいストリーム", + "No": "いいえ", + "No changes to apply.": "適用する変更はありません。", + "No changes to revert.": "元に戻す変更はありません。", + "Normalization disabled.": "正規化を無効にしました。", + "Normalization enabled.": "正規化を有効にしました。", + "Normalize": "正規化", + "Notes": "メモ", + "Pattern": "パターン", + "Planned Changes (file->edited output)": "予定された変更 (ファイル->編集後出力)", + "Quality": "品質", + "Quit": "終了", + "Remove Pattern": "パターンを削除", + "Revert": "元に戻す", + "Reverted pending changes.": "保留中の変更を元に戻しました。", + "Save": "保存", + "Season Offset": "シーズンオフセット", + "Select a stream first.": "まずストリームを選択してください。", + "Set Default": "デフォルトに設定", + "Set Forced": "強制に設定", + "Settings Screen": "設定画面", + "Numbering Mapping": "シフト済みシーズン", + "Show": "番組", + "Shows": "番組一覧", + "Source Season": "元シーズン", + "SrcIndex": "元インデックス", + "Status": "状態", + "Stay": "このまま", + "Stream dispositions": "ストリーム disposition", + "Stream tags": "ストリームタグ", + "Streams": "ストリーム", + "SubIndex": "サブインデックス", + "Substitute": "置換", + "Substitute pattern": "パターンを置換", + "Title": "タイトル", + "Type": "タイプ", + "Unable to update selected stream.": "選択したストリームを更新できませんでした。", + "Up": "上へ", + "Update Pattern": "パターンを更新", + "Updated media tag {tag!r}.": "メディアタグ {tag!r} を更新しました。", + "Updated stream #{index} ({track_type}).": "ストリーム #{index} ({track_type}) を更新しました。", + "Value": "値", + "Year": "年", + "Yes": "はい", + "add media tag: key='{key}' value='{value}'": "メディアタグを追加: key='{key}' value='{value}'", + "add {track_type} track: index={index} lang={language}": "{track_type}ストリームを追加: index={index} lang={language}", + "attached_pic": "attached_pic", + "attachment": "添付", + "audio": "音声", + "captions": "キャプション", + "change media tag: key='{key}' value='{value}'": "メディアタグを変更: key='{key}' value='{value}'", + "change stream #{index} ({track_type}:{sub_index}) add disposition={disposition}": "ストリーム #{index} ({track_type}:{sub_index}) disposition を追加={disposition}", + "change stream #{index} ({track_type}:{sub_index}) add key={key} value={value}": "ストリーム #{index} ({track_type}:{sub_index}) key を追加={key} value={value}", + "change stream #{index} ({track_type}:{sub_index}) change key={key} value={value}": "ストリーム #{index} ({track_type}:{sub_index}) key を変更={key} value={value}", + "change stream #{index} ({track_type}:{sub_index}) remove disposition={disposition}": "ストリーム #{index} ({track_type}:{sub_index}) disposition を削除={disposition}", + "change stream #{index} ({track_type}:{sub_index}) remove key={key} value={value}": "ストリーム #{index} ({track_type}:{sub_index}) key を削除={key} value={value}", + "clean_effects": "効果音のみ", + "comment": "コメント", + "default": "デフォルト", + "dependent": "依存", + "descriptions": "解説", + "dub": "吹替", + "for pattern": "パターン用", + "forced": "強制", + "from": "元", + "from pattern": "パターンから", + "from show": "番組から", + "hearing_impaired": "聴覚障害者向け", + "karaoke": "カラオケ", + "lyrics": "歌詞", + "metadata": "メタデータ", + "non_diegetic": "非ダイジェティック", + "original": "オリジナル", + "pattern #{id}": "パターン #{id}", + "remove media tag: key='{key}' value='{value}'": "メディアタグを削除: key='{key}' value='{value}'", + "remove stream #{index}": "ストリーム #{index} を削除", + "show #{id}": "番組 #{id}", + "stereo": "ステレオ", + "still_image": "静止画", + "sub index": "サブインデックス", + "subtitle": "字幕", + "timed_thumbnails": "時間指定サムネイル", + "undefined": "未定義", + "unknown": "不明", + "video": "映像", + "visual_impaired": "視覚障害者向け" + } +} diff --git a/assets/i18n/nb.json b/assets/i18n/nb.json new file mode 100644 index 0000000..921fcbc --- /dev/null +++ b/assets/i18n/nb.json @@ -0,0 +1,361 @@ +{ + "iso_languages": { + "ABKHAZIAN": "Abkhazian", + "AFAR": "afar", + "AFRIKAANS": "Afrikansk", + "AKAN": "Akan", + "ALBANIAN": "Albansk", + "AMHARIC": "Amharic", + "ARABIC": "Arabisk", + "ARAGONESE": "aragonsk", + "ARMENIAN": "armensk", + "ASSAMESE": "assamisk", + "AVARIC": "Avaric", + "AVESTAN": "avestisk", + "AYMARA": "aymara", + "AZERBAIJANI": "Aserbadjansk", + "BAMBARA": "bambara", + "BASHKIR": "basjkirsk", + "BASQUE": "Baskisk", + "BELARUSIAN": "Hviterussisk", + "BENGALI": "bengali", + "BISLAMA": "bislama", + "BOKMAL": "Bokmål", + "BOSNIAN": "Bosnisk", + "BRETON": "Breton", + "BULGARIAN": "Bulgarsk", + "BURMESE": "burmesisk", + "CATALAN": "Catalan", + "CHAMORRO": "chamorro", + "CHECHEN": "Chechen", + "CHICHEWA": "Chichewa", + "CHINESE": "Kinesisk", + "CHURCH_SLAVIC": "Church Slavic", + "CHUVASH": "tsjuvansk", + "CORNISH": "Cornish", + "CORSICAN": "Korsikansk", + "CREE": "Cree", + "CROATIAN": "Kroatsisk", + "CZECH": "Tjekkisk", + "DANISH": "Dansk", + "DIVEHI": "Divehi", + "DUTCH": "Dutch", + "DZONGKHA": "dzongkha", + "ENGLISH": "Engelsk", + "ESPERANTO": "Esperanto", + "ESTONIAN": "Estonsk", + "EWE": "ewe", + "FAROESE": "færøysk", + "FIJIAN": "fijiansk", + "FILIPINO": "Filipino", + "FINNISH": "Finsk", + "FRENCH": "Fransk", + "FULAH": "fulani", + "GALICIAN": "Galisisk", + "GANDA": "ganda", + "GEORGIAN": "Georgisk", + "GERMAN": "Tysk", + "GREEK": "Greek", + "GUARANI": "Guarani", + "GUJARATI": "gujarati", + "HAITIAN": "Haitian", + "HAUSA": "Hausa", + "HEBREW": "Hebraisk", + "HERERO": "Herero", + "HINDI": "hindi", + "HIRI_MOTU": "Hiri Motu", + "HUNGARIAN": "Ungarsk", + "ICELANDIC": "Islandsk", + "IDO": "ido", + "IGBO": "ibo", + "INDONESIAN": "Indonesisk", + "INTERLINGUA": "Interlingua", + "INTERLINGUE": "Interlingue", + "INUKTITUT": "inuktitut", + "INUPIAQ": "unupiak", + "IRISH": "Irsk", + "ITALIAN": "Italiensk", + "JAPANESE": "Japansk", + "JAVANESE": "Javanesisk", + "KALAALLISUT": "Kalaallisut", + "KANNADA": "kannada", + "KANURI": "Kanuri", + "KASHMIRI": "kasjmiri", + "KAZAKH": "kasakhisk", + "KHMER": "Khmer", + "KIKUYU": "Kikuyu", + "KINYARWANDA": "kinjarwanda", + "KIRGHIZ": "Kirghiz", + "KOMI": "komi", + "KONGO": "kikongo", + "KOREAN": "Koreansk", + "KUANYAMA": "Kuanyama", + "KURDISH": "Kurdisk", + "LAO": "laotisk", + "LATIN": "Latin", + "LATVIAN": "Latvisk", + "LIMBURGAN": "Limburgan", + "LINGALA": "lingala", + "LITHUANIAN": "Lituaisk", + "LUBA_KATANGA": "luba-katanga", + "LUXEMBOURGISH": "Luxembourgish", + "MACEDONIAN": "Makedonsk", + "MALAGASY": "madagassisk", + "MALAY": "malayisk", + "MALAYALAM": "malayalam", + "MALTESE": "Maltisk", + "MANX": "manx", + "MAORI": "Maori", + "MARATHI": "Marathi", + "MARSHALLESE": "Marshallese", + "MONGOLIAN": "Mongolsk", + "NAURU": "nauru", + "NAVAJO": "Navajo", + "NDONGA": "Ndonga", + "NEPALI": "nepalsk", + "NORTHERN_SAMI": "nordsamisk", + "NORTH_NDEBELE": "North Ndebele", + "NORWEGIAN": "Norsk", + "NORWEGIAN_NYNORSK": "Nynorsk", + "OCCITAN": "Occitan", + "OJIBWA": "ojibwa", + "ORIYA": "oriya", + "OROMO": "oromo", + "OSSETIAN": "Ossetian", + "PALI": "Pali", + "PANJABI": "Panjabi", + "PERSIAN": "Persisk", + "POLISH": "Polsk", + "PORTUGUESE": "Portugisisk", + "PUSHTO": "Pushto", + "QUECHUA": "quechua", + "ROMANIAN": "Romanian", + "ROMANSH": "Romansh", + "RUNDI": "rundi", + "RUSSIAN": "Russisk", + "SAMOAN": "samoansk", + "SANGO": "sango", + "SANSKRIT": "sanskrit", + "SARDINIAN": "Sardinsk", + "SCOTTISH_GAELIC": "Scottish Gaelic", + "SERBIAN": "Serbisk", + "SHONA": "Shona", + "SICHUAN_YI": "Sichuan Yi", + "SINDHI": "sindhi", + "SINHALA": "Sinhala", + "SLOVAK": "Slovakisk", + "SLOVENIAN": "Slovensk", + "SOMALI": "somalisk", + "SOUTHERN_SOTHO": "Southern Sotho", + "SOUTH_NDEBELE": "South Ndebele", + "SPANISH": "Spanish", + "SUNDANESE": "sundanesisk", + "SWAHILI": "swahili", + "SWATI": "swati", + "SWEDISH": "Svensk", + "TAGALOG": "tagalog", + "TAHITIAN": "Tahitisk", + "TAJIK": "Tajik", + "TAMIL": "Tamilsk", + "TATAR": "tatarisk", + "TELUGU": "telugu", + "THAI": "Thai", + "TIBETAN": "tibetansk", + "TIGRINYA": "Tigrinya", + "TONGA": "Tonga", + "TSONGA": "tsonga", + "TSWANA": "tswana", + "TURKISH": "Tyrkisk", + "TURKMEN": "turkmensk", + "TWI": "twi", + "UIGHUR": "Uighur", + "UKRAINIAN": "Ukrainsk", + "UNDEFINED": "undefined", + "URDU": "urdu", + "UZBEK": "usbekisk", + "VENDA": "venda", + "VIETNAMESE": "Vietnamesisk", + "VOLAPUK": "Volapük", + "WALLOON": "Vietnamesisk", + "WELSH": "Walisisk", + "WESTERN_FRISIAN": "Vestfrisisk", + "WOLOF": "wolof", + "XHOSA": "Xhosa", + "YIDDISH": "jiddisk", + "YORUBA": "joruba", + "ZHUANG": "Zhuang", + "ZULU": "Zulu" + }, + "phrases": { + "5.0(side)": "5.0(side)", + "5.1(side)": "5.1(side)", + "6.1": "6.1", + "6ch": "6ch", + "7.1": "7.1", + "": "", + "Add": "Legg til", + "Add Pattern": "Legg til mønster", + "Apply": "Bruk", + "Apply failed: {error}": "Kunne ikke bruke endringene: {error}", + "Are you sure to delete the following filename pattern?": "Er du sikker på at du vil slette følgende filnavnmønster?", + "Are you sure to delete the following shifted season?": "Er du sikker på at du vil slette følgende forskjøvede sesong?", + "Are you sure to delete the following show?": "Er du sikker på at du vil slette følgende serie?", + "Are you sure to delete the following {track_type} track?": "Er du sikker på at du vil slette følgende {track_type}-spor?", + "Are you sure to delete this tag?": "Er du sikker på at du vil slette denne taggen?", + "Audio Layout": "Lydoppsett", + "Back": "Tilbake", + "Cancel": "Avbryt", + "Cannot add another stream with disposition flag 'default' or 'forced' set": "Kan ikke legge til en ny strøm med disposisjonsflagget 'default' eller 'forced' satt", + "Changes applied and file reloaded.": "Endringene er brukt og filen er lastet inn på nytt.", + "Cleanup": "Rydd opp", + "Cleanup disabled.": "Rydding deaktivert.", + "Cleanup enabled.": "Rydding aktivert.", + "Codec": "Kodek", + "Continuing edit session.": "Fortsetter redigeringsøkten.", + "Default": "Standard", + "Delete": "Slett", + "Delete Show": "Slett serie", + "Deleted media tag {tag!r}.": "Mediataggen {tag!r} ble slettet.", + "Differences": "Forskjeller", + "Differences (file->db/output)": "Forskjeller (fil->DB/utdata)", + "Discard": "Forkast", + "Discard pending metadata changes and quit?": "Forkaste ventende metadataendringer og avslutte?", + "Discard pending metadata changes and reload the file state?": "Forkaste ventende metadataendringer og laste filtilstanden på nytt?", + "Down": "Ned", + "Dry-run: would rewrite via temporary file {target_path}": "Tørrkjøring: ville skrevet om via midlertidig fil {target_path}", + "Edit": "Rediger", + "Edit Pattern": "Rediger mønster", + "Edit Show": "Rediger serie", + "Edit filename pattern": "Rediger filnavnmønster", + "Edit shifted season": "Rediger forskjøvet sesong", + "Edit stream": "Rediger strøm", + "Episode Offset": "Episodeforskyvning", + "Episode offset": "Episodeforskyvning", + "File": "Fil", + "File patterns": "Filmønstre", + "First Episode": "Første episode", + "First episode": "Første episode", + "Forced": "Tvungen", + "Help": "Hjelp", + "Help Screen": "Hjelpeskjerm", + "ID": "ID", + "Identify": "Identifiser", + "Index": "Indeks", + "Index / Subindex": "Indeks / Underindeks", + "Index Episode Digits": "Siffer for episodeindeks", + "Index Season Digits": "Siffer for sesongindeks", + "Indicator Edisode Digits": "Siffer for episodeindikator", + "Indicator Season Digits": "Siffer for sesongindikator", + "Keep Editing": "Fortsett redigeringen", + "Keeping pending changes.": "Beholder ventende endringer.", + "Key": "Nøkkel", + "Language": "Språk", + "Last Episode": "Siste episode", + "Last episode": "Siste episode", + "Layout": "Oppsett", + "Media Tags": "Mediatagger", + "More than one default audio stream detected and no prompt set": "Mer enn én standard lydstrøm funnet og ingen forespørsel satt", + "More than one default audio stream detected! Please select stream": "Mer enn én standard lydstrøm funnet. Velg strøm", + "More than one default subtitle stream detected and no prompt set": "Mer enn én standard undertekststrøm funnet og ingen forespørsel satt", + "More than one default subtitle stream detected! Please select stream": "Mer enn én standard undertekststrøm funnet. Velg strøm", + "More than one default video stream detected and no prompt set": "Mer enn én standard videostrøm funnet og ingen forespørsel satt", + "More than one default video stream detected! Please select stream": "Mer enn én standard videostrøm funnet. Velg strøm", + "More than one forced audio stream detected and no prompt set": "Mer enn én tvungen lydstrøm funnet og ingen forespørsel satt", + "More than one forced audio stream detected! Please select stream": "Mer enn én tvungen lydstrøm funnet. Velg strøm", + "More than one forced subtitle stream detected and no prompt set": "Mer enn én tvungen undertekststrøm funnet og ingen forespørsel satt", + "More than one forced subtitle stream detected! Please select stream": "Mer enn én tvungen undertekststrøm funnet. Velg strøm", + "More than one forced video stream detected and no prompt set": "Mer enn én tvungen videostrøm funnet og ingen forespørsel satt", + "More than one forced video stream detected! Please select stream": "Mer enn én tvungen videostrøm funnet. Velg strøm", + "Name": "Navn", + "New Pattern": "Nytt mønster", + "New Show": "Ny serie", + "New filename pattern": "Nytt filnavnmønster", + "New shifted season": "Ny forskjøvet sesong", + "New stream": "Ny strøm", + "No": "Nei", + "No changes to apply.": "Ingen endringer å bruke.", + "No changes to revert.": "Ingen endringer å tilbakestille.", + "Normalization disabled.": "Normalisering deaktivert.", + "Normalization enabled.": "Normalisering aktivert.", + "Normalize": "Normaliser", + "Notes": "Notater", + "Pattern": "Mønster", + "Planned Changes (file->edited output)": "Planlagte endringer (fil->redigert utdata)", + "Quality": "Kvalitet", + "Quit": "Avslutt", + "Remove Pattern": "Fjern mønster", + "Revert": "Tilbakestill", + "Reverted pending changes.": "Ventende endringer ble tilbakestilt.", + "Save": "Lagre", + "Season Offset": "Sesongforskyvning", + "Select a stream first.": "Velg en strøm først.", + "Set Default": "Sett som standard", + "Set Forced": "Sett som tvungen", + "Settings Screen": "Innstillingsskjerm", + "Numbering Mapping": "Forskjøvne sesonger", + "Show": "Serie", + "Shows": "Serier", + "Source Season": "Kildesesong", + "SrcIndex": "Kildeindeks", + "Status": "Status", + "Stay": "Bli", + "Stream dispositions": "Strømdisposisjoner", + "Stream tags": "Strømtagger", + "Streams": "Strømmer", + "SubIndex": "Underindeks", + "Substitute": "Erstatt", + "Substitute pattern": "Erstatt mønster", + "Title": "Tittel", + "Type": "Type", + "Unable to update selected stream.": "Kunne ikke oppdatere valgt strøm.", + "Up": "Opp", + "Update Pattern": "Oppdater mønster", + "Updated media tag {tag!r}.": "Mediataggen {tag!r} ble oppdatert.", + "Updated stream #{index} ({track_type}).": "Strøm #{index} ({track_type}) oppdatert.", + "Value": "Verdi", + "Year": "År", + "Yes": "Ja", + "add media tag: key='{key}' value='{value}'": "legg til mediatagg: nøkkel='{key}' verdi='{value}'", + "add {track_type} track: index={index} lang={language}": "legg til {track_type}-spor: indeks={index} språk={language}", + "attached_pic": "attached_pic", + "attachment": "vedlegg", + "audio": "lyd", + "captions": "teksting", + "change media tag: key='{key}' value='{value}'": "endre mediatagg: nøkkel='{key}' verdi='{value}'", + "change stream #{index} ({track_type}:{sub_index}) add disposition={disposition}": "endre strøm #{index} ({track_type}:{sub_index}) legg til disposisjon={disposition}", + "change stream #{index} ({track_type}:{sub_index}) add key={key} value={value}": "endre strøm #{index} ({track_type}:{sub_index}) legg til nøkkel={key} verdi={value}", + "change stream #{index} ({track_type}:{sub_index}) change key={key} value={value}": "endre strøm #{index} ({track_type}:{sub_index}) endre nøkkel={key} verdi={value}", + "change stream #{index} ({track_type}:{sub_index}) remove disposition={disposition}": "endre strøm #{index} ({track_type}:{sub_index}) fjern disposisjon={disposition}", + "change stream #{index} ({track_type}:{sub_index}) remove key={key} value={value}": "endre strøm #{index} ({track_type}:{sub_index}) fjern nøkkel={key} verdi={value}", + "clean_effects": "bare effekter", + "comment": "kommentar", + "default": "standard", + "dependent": "avhengig", + "descriptions": "beskrivelser", + "dub": "dubbet", + "for pattern": "for mønster", + "forced": "tvungen", + "from": "fra", + "from pattern": "fra mønster", + "from show": "fra serie", + "hearing_impaired": "hørselshemmet", + "karaoke": "karaoke", + "lyrics": "sangtekst", + "metadata": "metadata", + "non_diegetic": "ikke-diegetisk", + "original": "original", + "pattern #{id}": "mønster #{id}", + "remove media tag: key='{key}' value='{value}'": "fjern mediatagg: nøkkel='{key}' verdi='{value}'", + "remove stream #{index}": "fjern strøm #{index}", + "show #{id}": "serie #{id}", + "stereo": "stereo", + "still_image": "stillbilde", + "sub index": "underindeks", + "subtitle": "undertekst", + "timed_thumbnails": "tidsbestemte miniatyrer", + "undefined": "udefinert", + "unknown": "ukjent", + "video": "video", + "visual_impaired": "synshemmet" + } +} diff --git a/assets/i18n/pt.json b/assets/i18n/pt.json new file mode 100644 index 0000000..a6e2723 --- /dev/null +++ b/assets/i18n/pt.json @@ -0,0 +1,361 @@ +{ + "iso_languages": { + "ABKHAZIAN": "abkhazian", + "AFAR": "afar", + "AFRIKAANS": "Africanos", + "AKAN": "Akan", + "ALBANIAN": "Albanês", + "AMHARIC": "Amárico", + "ARABIC": "Árabe", + "ARAGONESE": "Aragonês", + "ARMENIAN": "arménio", + "ASSAMESE": "assamês", + "AVARIC": "Avárico", + "AVESTAN": "avéstico", + "AYMARA": "aimara", + "AZERBAIJANI": "Azerbaijani", + "BAMBARA": "bambara", + "BASHKIR": "bashkir", + "BASQUE": "Basco", + "BELARUSIAN": "Bielorusso", + "BENGALI": "Bengali", + "BISLAMA": "bislamá", + "BOKMAL": "Bokmål", + "BOSNIAN": "Bósnio", + "BRETON": "Bretão", + "BULGARIAN": "Búlgaro", + "BURMESE": "birmanês", + "CATALAN": "Catalan", + "CHAMORRO": "chamorro", + "CHECHEN": "Checheno", + "CHICHEWA": "Chichewa", + "CHINESE": "Chinês", + "CHURCH_SLAVIC": "Church Slavic", + "CHUVASH": "chuvash", + "CORNISH": "Córnico", + "CORSICAN": "córsico", + "CREE": "Cree", + "CROATIAN": "Croata", + "CZECH": "Checo", + "DANISH": "Dinamarquês", + "DIVEHI": "Divehi", + "DUTCH": "Dutch", + "DZONGKHA": "dzonga", + "ENGLISH": "Inglês", + "ESPERANTO": "Esperanto", + "ESTONIAN": "Estoniano", + "EWE": "eve", + "FAROESE": "Faroês", + "FIJIAN": "fijiano", + "FILIPINO": "Filipino", + "FINNISH": "Finlandês", + "FRENCH": "Francês", + "FULAH": "fula", + "GALICIAN": "Galego", + "GANDA": "luganda", + "GEORGIAN": "georgiano", + "GERMAN": "Alemão", + "GREEK": "Greek", + "GUARANI": "Guarani", + "GUJARATI": "Guzerate", + "HAITIAN": "Haitian", + "HAUSA": "Hauçá", + "HEBREW": "Hebreu", + "HERERO": "Hereró", + "HINDI": "Hindi", + "HIRI_MOTU": "Hiri Motu", + "HUNGARIAN": "Húngaro", + "ICELANDIC": "Islandês", + "IDO": "ido", + "IGBO": "ibo", + "INDONESIAN": "Indonésio", + "INTERLINGUA": "Interlingua", + "INTERLINGUE": "Interlingue", + "INUKTITUT": "inuktitut", + "INUPIAQ": "Inupiaque", + "IRISH": "Irlandês", + "ITALIAN": "Italiano", + "JAPANESE": "Japonês", + "JAVANESE": "Javanês", + "KALAALLISUT": "Kalaallisut", + "KANNADA": "Kannada", + "KANURI": "Canúri", + "KASHMIRI": "kashmiri", + "KAZAKH": "cazaque", + "KHMER": "Khmer", + "KIKUYU": "Kikuyu", + "KINYARWANDA": "kinyarwanda", + "KIRGHIZ": "Kirghiz", + "KOMI": "komi", + "KONGO": "congolês", + "KOREAN": "Coreano", + "KUANYAMA": "Kuanyama", + "KURDISH": "Curdo", + "LAO": "Laosiano", + "LATIN": "Latim", + "LATVIAN": "Letão", + "LIMBURGAN": "Limburgan", + "LINGALA": "Lingala", + "LITHUANIAN": "Lituano", + "LUBA_KATANGA": "luba-catanga", + "LUXEMBOURGISH": "Luxembourgish", + "MACEDONIAN": "Macedônio", + "MALAGASY": "malgaxe", + "MALAY": "Malaio", + "MALAYALAM": "malaiala", + "MALTESE": "Maltês", + "MANX": "Manx", + "MAORI": "Maori", + "MARATHI": "marata", + "MARSHALLESE": "Marshalês", + "MONGOLIAN": "Mongol", + "NAURU": "nauruano", + "NAVAJO": "Navajo", + "NDONGA": "dongo", + "NEPALI": "Nepalês", + "NORTHERN_SAMI": "northern sami", + "NORTH_NDEBELE": "North Ndebele", + "NORWEGIAN": "Norueguês", + "NORWEGIAN_NYNORSK": "Nynorsk", + "OCCITAN": "Occitan", + "OJIBWA": "ojibwa", + "ORIYA": "oriya", + "OROMO": "Oromo", + "OSSETIAN": "Ossetian", + "PALI": "Páli", + "PANJABI": "Panjabi", + "PERSIAN": "Persa", + "POLISH": "Polaco", + "PORTUGUESE": "Português", + "PUSHTO": "Pushto", + "QUECHUA": "quíchua", + "ROMANIAN": "Romanian", + "ROMANSH": "Romanche", + "RUNDI": "rundi", + "RUSSIAN": "Russo", + "SAMOAN": "Samoano", + "SANGO": "sango", + "SANSKRIT": "Sânscrito", + "SARDINIAN": "Sardo", + "SCOTTISH_GAELIC": "Scottish Gaelic", + "SERBIAN": "Sérvio", + "SHONA": "Xona", + "SICHUAN_YI": "Sichuan Yi", + "SINDHI": "sindi", + "SINHALA": "Sinhala", + "SLOVAK": "Eslovaco", + "SLOVENIAN": "Eslovêno", + "SOMALI": "somali", + "SOUTHERN_SOTHO": "Southern Sotho", + "SOUTH_NDEBELE": "South Ndebele", + "SPANISH": "Spanish", + "SUNDANESE": "sundanês", + "SWAHILI": "suaíli", + "SWATI": "swati", + "SWEDISH": "Sueco", + "TAGALOG": "Tagalo", + "TAHITIAN": "Taitiano", + "TAJIK": "Tadjique", + "TAMIL": "Tâmil", + "TATAR": "tatar", + "TELUGU": "Telugu", + "THAI": "Tailandês", + "TIBETAN": "tibetano", + "TIGRINYA": "Tigrínia", + "TONGA": "Tonga", + "TSONGA": "tsonga", + "TSWANA": "tswana", + "TURKISH": "Turco", + "TURKMEN": "turcomano", + "TWI": "twi", + "UIGHUR": "Uighur", + "UKRAINIAN": "Ucraniano", + "UNDEFINED": "undefined", + "URDU": "urdu", + "UZBEK": "usbeque", + "VENDA": "venda", + "VIETNAMESE": "Vietnamita", + "VOLAPUK": "Volapuque", + "WALLOON": "walloon", + "WELSH": "galês", + "WESTERN_FRISIAN": "Frísio ocidental", + "WOLOF": "uolofe", + "XHOSA": "xosa", + "YIDDISH": "iídiche", + "YORUBA": "ioruba", + "ZHUANG": "Zhuang", + "ZULU": "zulu" + }, + "phrases": { + "5.0(side)": "5.0(side)", + "5.1(side)": "5.1(side)", + "6.1": "6.1", + "6ch": "6ch", + "7.1": "7.1", + "": "", + "Add": "Adicionar", + "Add Pattern": "Adicionar padrão", + "Apply": "Aplicar", + "Apply failed: {error}": "Falha ao aplicar: {error}", + "Are you sure to delete the following filename pattern?": "Tem certeza de que deseja excluir o seguinte padrão de nome de arquivo?", + "Are you sure to delete the following shifted season?": "Tem certeza de que deseja excluir a seguinte temporada deslocada?", + "Are you sure to delete the following show?": "Tem certeza de que deseja excluir a seguinte série?", + "Are you sure to delete the following {track_type} track?": "Tem certeza de que deseja excluir a seguinte faixa {track_type}?", + "Are you sure to delete this tag?": "Tem certeza de que deseja excluir esta tag?", + "Audio Layout": "Layout de áudio", + "Back": "Voltar", + "Cancel": "Cancelar", + "Cannot add another stream with disposition flag 'default' or 'forced' set": "Não é possível adicionar outro fluxo com a flag de disposição 'default' ou 'forced' definida", + "Changes applied and file reloaded.": "Alterações aplicadas e arquivo recarregado.", + "Cleanup": "Limpeza", + "Cleanup disabled.": "Limpeza desativada.", + "Cleanup enabled.": "Limpeza ativada.", + "Codec": "Codec", + "Continuing edit session.": "Continuando a sessão de edição.", + "Default": "Padrão", + "Delete": "Excluir", + "Delete Show": "Excluir série", + "Deleted media tag {tag!r}.": "Tag de mídia {tag!r} excluída.", + "Differences": "Diferenças", + "Differences (file->db/output)": "Diferenças (arquivo->BD/saída)", + "Discard": "Descartar", + "Discard pending metadata changes and quit?": "Descartar alterações pendentes de metadados e sair?", + "Discard pending metadata changes and reload the file state?": "Descartar alterações pendentes de metadados e recarregar o estado do arquivo?", + "Down": "Baixo", + "Dry-run: would rewrite via temporary file {target_path}": "Execução simulada: regravaria via arquivo temporário {target_path}", + "Edit": "Editar", + "Edit Pattern": "Editar padrão", + "Edit Show": "Editar série", + "Edit filename pattern": "Editar padrão de nome de arquivo", + "Edit shifted season": "Editar temporada deslocada", + "Edit stream": "Editar fluxo", + "Episode Offset": "Deslocamento de episódio", + "Episode offset": "Deslocamento de episódio", + "File": "Arquivo", + "File patterns": "Padrões de arquivo", + "First Episode": "Primeiro episódio", + "First episode": "Primeiro episódio", + "Forced": "Forçado", + "Help": "Ajuda", + "Help Screen": "Tela de ajuda", + "ID": "ID", + "Identify": "Identificar", + "Index": "Índice", + "Index / Subindex": "Índice / Subíndice", + "Index Episode Digits": "Dígitos do índice do episódio", + "Index Season Digits": "Dígitos do índice da temporada", + "Indicator Edisode Digits": "Dígitos do indicador do episódio", + "Indicator Season Digits": "Dígitos do indicador da temporada", + "Keep Editing": "Continuar editando", + "Keeping pending changes.": "Mantendo alterações pendentes.", + "Key": "Chave", + "Language": "Idioma", + "Last Episode": "Último episódio", + "Last episode": "Último episódio", + "Layout": "Layout", + "Media Tags": "Tags de mídia", + "More than one default audio stream detected and no prompt set": "Mais de um fluxo de áudio padrão detectado e nenhum prompt definido", + "More than one default audio stream detected! Please select stream": "Mais de um fluxo de áudio padrão detectado! Selecione o fluxo", + "More than one default subtitle stream detected and no prompt set": "Mais de um fluxo de legenda padrão detectado e nenhum prompt definido", + "More than one default subtitle stream detected! Please select stream": "Mais de um fluxo de legenda padrão detectado! Selecione o fluxo", + "More than one default video stream detected and no prompt set": "Mais de um fluxo de vídeo padrão detectado e nenhum prompt definido", + "More than one default video stream detected! Please select stream": "Mais de um fluxo de vídeo padrão detectado! Selecione o fluxo", + "More than one forced audio stream detected and no prompt set": "Mais de um fluxo de áudio forçado detectado e nenhum prompt definido", + "More than one forced audio stream detected! Please select stream": "Mais de um fluxo de áudio forçado detectado! Selecione o fluxo", + "More than one forced subtitle stream detected and no prompt set": "Mais de um fluxo de legenda forçada detectado e nenhum prompt definido", + "More than one forced subtitle stream detected! Please select stream": "Mais de um fluxo de legenda forçada detectado! Selecione o fluxo", + "More than one forced video stream detected and no prompt set": "Mais de um fluxo de vídeo forçado detectado e nenhum prompt definido", + "More than one forced video stream detected! Please select stream": "Mais de um fluxo de vídeo forçado detectado! Selecione o fluxo", + "Name": "Nome", + "New Pattern": "Novo padrão", + "New Show": "Nova série", + "New filename pattern": "Novo padrão de nome de arquivo", + "New shifted season": "Nova temporada deslocada", + "New stream": "Novo fluxo", + "No": "Não", + "No changes to apply.": "Nenhuma alteração para aplicar.", + "No changes to revert.": "Nenhuma alteração para reverter.", + "Normalization disabled.": "Normalização desativada.", + "Normalization enabled.": "Normalização ativada.", + "Normalize": "Normalizar", + "Notes": "Notas", + "Pattern": "Padrão", + "Planned Changes (file->edited output)": "Alterações planejadas (arquivo->saída editada)", + "Quality": "Qualidade", + "Quit": "Sair", + "Remove Pattern": "Remover padrão", + "Revert": "Reverter", + "Reverted pending changes.": "Alterações pendentes revertidas.", + "Save": "Salvar", + "Season Offset": "Deslocamento de temporada", + "Select a stream first.": "Selecione um fluxo primeiro.", + "Set Default": "Definir como padrão", + "Set Forced": "Definir como forçado", + "Settings Screen": "Tela de configurações", + "Numbering Mapping": "Temporadas deslocadas", + "Show": "Série", + "Shows": "Séries", + "Source Season": "Temporada de origem", + "SrcIndex": "Índice de origem", + "Status": "Status", + "Stay": "Permanecer", + "Stream dispositions": "Disposições do fluxo", + "Stream tags": "Tags do fluxo", + "Streams": "Fluxos", + "SubIndex": "Subíndice", + "Substitute": "Substituir", + "Substitute pattern": "Substituir padrão", + "Title": "Título", + "Type": "Tipo", + "Unable to update selected stream.": "Não foi possível atualizar o fluxo selecionado.", + "Up": "Cima", + "Update Pattern": "Atualizar padrão", + "Updated media tag {tag!r}.": "Tag de mídia {tag!r} atualizada.", + "Updated stream #{index} ({track_type}).": "Fluxo #{index} ({track_type}) atualizado.", + "Value": "Valor", + "Year": "Ano", + "Yes": "Sim", + "add media tag: key='{key}' value='{value}'": "adicionar tag de mídia: chave='{key}' valor='{value}'", + "add {track_type} track: index={index} lang={language}": "adicionar faixa {track_type}: índice={index} idioma={language}", + "attached_pic": "attached_pic", + "attachment": "anexo", + "audio": "áudio", + "captions": "legendas", + "change media tag: key='{key}' value='{value}'": "alterar tag de mídia: chave='{key}' valor='{value}'", + "change stream #{index} ({track_type}:{sub_index}) add disposition={disposition}": "alterar fluxo #{index} ({track_type}:{sub_index}) adicionar disposição={disposition}", + "change stream #{index} ({track_type}:{sub_index}) add key={key} value={value}": "alterar fluxo #{index} ({track_type}:{sub_index}) adicionar chave={key} valor={value}", + "change stream #{index} ({track_type}:{sub_index}) change key={key} value={value}": "alterar fluxo #{index} ({track_type}:{sub_index}) alterar chave={key} valor={value}", + "change stream #{index} ({track_type}:{sub_index}) remove disposition={disposition}": "alterar fluxo #{index} ({track_type}:{sub_index}) remover disposição={disposition}", + "change stream #{index} ({track_type}:{sub_index}) remove key={key} value={value}": "alterar fluxo #{index} ({track_type}:{sub_index}) remover chave={key} valor={value}", + "clean_effects": "apenas efeitos", + "comment": "comentário", + "default": "padrão", + "dependent": "dependente", + "descriptions": "descrições", + "dub": "dublado", + "for pattern": "para o padrão", + "forced": "forçado", + "from": "de", + "from pattern": "do padrão", + "from show": "da série", + "hearing_impaired": "deficiência auditiva", + "karaoke": "karaokê", + "lyrics": "letra", + "metadata": "metadados", + "non_diegetic": "não diegético", + "original": "original", + "pattern #{id}": "padrão #{id}", + "remove media tag: key='{key}' value='{value}'": "remover tag de mídia: chave='{key}' valor='{value}'", + "remove stream #{index}": "remover fluxo #{index}", + "show #{id}": "série #{id}", + "stereo": "estéreo", + "still_image": "imagem estática", + "sub index": "subíndice", + "subtitle": "legenda", + "timed_thumbnails": "miniaturas temporizadas", + "undefined": "indefinido", + "unknown": "desconhecido", + "video": "vídeo", + "visual_impaired": "deficiência visual" + } +} diff --git a/assets/i18n/ta.json b/assets/i18n/ta.json new file mode 100644 index 0000000..fade8c0 --- /dev/null +++ b/assets/i18n/ta.json @@ -0,0 +1,361 @@ +{ + "iso_languages": { + "ABKHAZIAN": "அப்காசியன்", + "AFAR": "அஃபர்", + "AFRIKAANS": "ஆப்ரிக்கான்ச்", + "AKAN": "அகான்", + "ALBANIAN": "அல்பேனியன்", + "AMHARIC": "அம்ஆரிக்", + "ARABIC": "அராபிக்", + "ARAGONESE": "அரகோன்ச்", + "ARMENIAN": "அர்மேனியன்", + "ASSAMESE": "அச்சாமி", + "AVARIC": "அவாரிக்", + "AVESTAN": "அவேச்டன்", + "AYMARA": "அய்மாரா", + "AZERBAIJANI": "அசெர்பெய்சானி", + "BAMBARA": "பம்பரா", + "BASHKIR": "பாச்கிர்", + "BASQUE": "பாச்க்", + "BELARUSIAN": "பெலாருசியன்", + "BENGALI": "பெங்காலி", + "BISLAMA": "பிச்லாமா", + "BOKMAL": "Bokmål", + "BOSNIAN": "போச்னியன்", + "BRETON": "ப்ரெடன்", + "BULGARIAN": "பல்கேரியன்", + "BURMESE": "பர்மீசி", + "CATALAN": "Catalan", + "CHAMORRO": "சாமோர்ரோ", + "CHECHEN": "செக்சன்", + "CHICHEWA": "Chichewa", + "CHINESE": "சைனீச்", + "CHURCH_SLAVIC": "Church Slavic", + "CHUVASH": "சுவாச்", + "CORNISH": "கோர்னிச்", + "CORSICAN": "கோர்சிகேன்", + "CREE": "சிரீ", + "CROATIAN": "குரேசியன்", + "CZECH": "செக்", + "DANISH": "டானிச்", + "DIVEHI": "Divehi", + "DUTCH": "Dutch", + "DZONGKHA": "ட்சொங்க்கா", + "ENGLISH": "ஆங்கிலம்", + "ESPERANTO": "எச்பெரான்டொ", + "ESTONIAN": "எச்டோனியன்", + "EWE": "இவ்", + "FAROESE": "ஃபரோச்", + "FIJIAN": "ஃபிசியன்", + "FILIPINO": "Filipino", + "FINNISH": "பின்னிச்", + "FRENCH": "பிரெஞ்சு", + "FULAH": "ஃபுல்லா", + "GALICIAN": "காலிசியன்", + "GANDA": "கான்டா", + "GEORGIAN": "சியார்சியன்", + "GERMAN": "செர்மன்", + "GREEK": "Greek", + "GUARANI": "குர்ரானி", + "GUJARATI": "குசராத்தி", + "HAITIAN": "Haitian", + "HAUSA": "ஔசா", + "HEBREW": "ஈப்ரு", + "HERERO": "இரீரோ", + "HINDI": "இந்தி", + "HIRI_MOTU": "இரி மோட்டு", + "HUNGARIAN": "அங்கேரியன்", + "ICELANDIC": "ஐச்லாண்டிக்", + "IDO": "ஐடூ", + "IGBO": "இக்போ", + "INDONESIAN": "இந்தோனேசியன்", + "INTERLINGUA": "Interlingua", + "INTERLINGUE": "Interlingue", + "INUKTITUT": "இனுடிடட்", + "INUPIAQ": "இனுபைக்யூ", + "IRISH": "ஐரிச்", + "ITALIAN": "இத்தாலியன்", + "JAPANESE": "சப்பானிய", + "JAVANESE": "சவானிச்", + "KALAALLISUT": "Kalaallisut", + "KANNADA": "கன்னடம்", + "KANURI": "கனுரி", + "KASHMIRI": "காச்மீரி", + "KAZAKH": "கசாக்ச்", + "KHMER": "Khmer", + "KIKUYU": "Kikuyu", + "KINYARWANDA": "கின்யார்வான்டா", + "KIRGHIZ": "Kirghiz", + "KOMI": "கோமி", + "KONGO": "காங்கோ", + "KOREAN": "கொரியன்", + "KUANYAMA": "Kuanyama", + "KURDISH": "குர்திச்", + "LAO": "லாவோ", + "LATIN": "லத்தீன்", + "LATVIAN": "லாட்வியன்", + "LIMBURGAN": "Limburgan", + "LINGALA": "லின்காலா", + "LITHUANIAN": "லிதுவேனியன்", + "LUBA_KATANGA": "லூபா-கடான்கா", + "LUXEMBOURGISH": "Luxembourgish", + "MACEDONIAN": "மேசடோனியன்", + "MALAGASY": "மலகாசி", + "MALAY": "மலாய்", + "MALAYALAM": "மலையாளம்", + "MALTESE": "மல்டீச்", + "MANX": "மான்ச்", + "MAORI": "மௌரி", + "MARATHI": "மராத்தி", + "MARSHALLESE": "மார்சலீசீ", + "MONGOLIAN": "மங்கோலியன்", + "NAURU": "நவூரு", + "NAVAJO": "Navajo", + "NDONGA": "நடோன்கா", + "NEPALI": "நேபாலி", + "NORTHERN_SAMI": "கிழக்கு சாமி", + "NORTH_NDEBELE": "North Ndebele", + "NORWEGIAN": "நார்வேசியன்", + "NORWEGIAN_NYNORSK": "Nynorsk", + "OCCITAN": "Occitan", + "OJIBWA": "ஒசிப்வா", + "ORIYA": "ஒரியா", + "OROMO": "ஒரோமோ", + "OSSETIAN": "Ossetian", + "PALI": "பாலி", + "PANJABI": "Panjabi", + "PERSIAN": "பெர்சியன்", + "POLISH": "போலிச்", + "PORTUGUESE": "போர்த்துக்கீசிய", + "PUSHTO": "Pushto", + "QUECHUA": "க்யுசோ", + "ROMANIAN": "Romanian", + "ROMANSH": "ரோமான்ச்ச்", + "RUNDI": "ருண்டி", + "RUSSIAN": "ரச்யன்", + "SAMOAN": "சாமோயன்", + "SANGO": "சான்ங்கோ", + "SANSKRIT": "சான்ச்கிரிட்", + "SARDINIAN": "சார்டினியன்", + "SCOTTISH_GAELIC": "Scottish Gaelic", + "SERBIAN": "செர்பியன்", + "SHONA": "சோனா", + "SICHUAN_YI": "Sichuan Yi", + "SINDHI": "சிந்தி", + "SINHALA": "Sinhala", + "SLOVAK": "சுலோவாக்", + "SLOVENIAN": "ச்லோவெனியன்", + "SOMALI": "சோமாலி", + "SOUTHERN_SOTHO": "Southern Sotho", + "SOUTH_NDEBELE": "South Ndebele", + "SPANISH": "Spanish", + "SUNDANESE": "சூடானீச்", + "SWAHILI": "ச்வாஇலி", + "SWATI": "ச்வாதி", + "SWEDISH": "சுவீடிச்", + "TAGALOG": "டங்லாக்", + "TAHITIAN": "தஇதியன்", + "TAJIK": "தாசிக்", + "TAMIL": "தமிழ்", + "TATAR": "டாட்டர்", + "TELUGU": "தெலுங்கு", + "THAI": "தாய்", + "TIBETAN": "திபெத்திய", + "TIGRINYA": "தைக்ரின்யா", + "TONGA": "Tonga", + "TSONGA": "ட்சாங்கோ", + "TSWANA": "ட்ச்வனா", + "TURKISH": "துருக்கி", + "TURKMEN": "டர்க்மென்", + "TWI": "டிவி", + "UIGHUR": "Uighur", + "UKRAINIAN": "உக்ரெனியன்", + "UNDEFINED": "undefined", + "URDU": "உருது", + "UZBEK": "உச்பெக்", + "VENDA": "வேண்டா", + "VIETNAMESE": "வியட்னாம்", + "VOLAPUK": "வோலாபுக்", + "WALLOON": "வாலூன்", + "WELSH": "வெல்ச்", + "WESTERN_FRISIAN": "மேற்கு ஃபிரிசியன்", + "WOLOF": "ஓலோஃப்", + "XHOSA": "சோசா", + "YIDDISH": "இட்டிச்", + "YORUBA": "யோருபா", + "ZHUANG": "Zhuang", + "ZULU": "சுலு" + }, + "phrases": { + "5.0(side)": "5.0(side)", + "5.1(side)": "5.1(side)", + "6.1": "6.1", + "6ch": "6ch", + "7.1": "7.1", + "": "<புதிய தொடர்>", + "Add": "சேர்", + "Add Pattern": "வடிவத்தை சேர்", + "Apply": "பயன்படுத்து", + "Apply failed: {error}": "பயன்படுத்தல் தோல்வியடைந்தது: {error}", + "Are you sure to delete the following filename pattern?": "பின்வரும் கோப்பு பெயர் வடிவத்தை நீக்க விரும்புகிறீர்களா?", + "Are you sure to delete the following shifted season?": "பின்வரும் மாற்றிய சீசனை நீக்க விரும்புகிறீர்களா?", + "Are you sure to delete the following show?": "பின்வரும் தொடரை நீக்க விரும்புகிறீர்களா?", + "Are you sure to delete the following {track_type} track?": "பின்வரும் {track_type} ஸ்ட்ரீமை நீக்க விரும்புகிறீர்களா?", + "Are you sure to delete this tag?": "இந்த குறிச்சொல்லை நீக்க விரும்புகிறீர்களா?", + "Audio Layout": "ஒலி அமைப்பு", + "Back": "பின்", + "Cancel": "ரத்து", + "Cannot add another stream with disposition flag 'default' or 'forced' set": "'default' அல்லது 'forced' disposition கொடி அமைந்த மற்றொரு ஸ்ட்ரீமை சேர்க்க முடியாது", + "Changes applied and file reloaded.": "மாற்றங்கள் பயன்படுத்தப்பட்டு கோப்பு மீளேற்றப்பட்டது.", + "Cleanup": "சுத்திகரிப்பு", + "Cleanup disabled.": "சுத்திகரிப்பு முடக்கப்பட்டது.", + "Cleanup enabled.": "சுத்திகரிப்பு இயக்கப்பட்டது.", + "Codec": "கோடெக்", + "Continuing edit session.": "திருத்த அமர்வு தொடர்கிறது.", + "Default": "இயல்புநிலை", + "Delete": "நீக்கு", + "Delete Show": "தொடரை நீக்கு", + "Deleted media tag {tag!r}.": "மீடியா குறிச்சொல் {tag!r} நீக்கப்பட்டது.", + "Differences": "வேறுபாடுகள்", + "Differences (file->db/output)": "வேறுபாடுகள் (கோப்பு->DB/வெளியீடு)", + "Discard": "கைவிடு", + "Discard pending metadata changes and quit?": "நிலுவையில் உள்ள மெட்டாடேட்டா மாற்றங்களை கைவிட்டு வெளியேறவா?", + "Discard pending metadata changes and reload the file state?": "நிலுவையில் உள்ள மெட்டாடேட்டா மாற்றங்களை கைவிட்டு கோப்பு நிலையை மீளேற்றவா?", + "Down": "கீழ்", + "Dry-run: would rewrite via temporary file {target_path}": "Dry-run: தற்காலிக கோப்பு {target_path} வழியாக மறுஎழுதப்படும்", + "Edit": "திருத்து", + "Edit Pattern": "வடிவத்தை திருத்து", + "Edit Show": "தொடரை திருத்து", + "Edit filename pattern": "கோப்பு பெயர் வடிவத்தை திருத்து", + "Edit shifted season": "மாற்றிய சீசனை திருத்து", + "Edit stream": "ஸ்ட்ரீமை திருத்து", + "Episode Offset": "அத்தியாய இடச்சரிவு", + "Episode offset": "அத்தியாய இடச்சரிவு", + "File": "கோப்பு", + "File patterns": "கோப்பு வடிவங்கள்", + "First Episode": "முதல் அத்தியாயம்", + "First episode": "முதல் அத்தியாயம்", + "Forced": "கட்டாயம்", + "Help": "உதவி", + "Help Screen": "உதவி திரை", + "ID": "அடையாளம்", + "Identify": "அடையாளம் காட்டு", + "Index": "சுட்டி", + "Index / Subindex": "சுட்டி / துணைச்சுட்டி", + "Index Episode Digits": "அத்தியாய சுட்டி இலக்கங்கள்", + "Index Season Digits": "சீசன் சுட்டி இலக்கங்கள்", + "Indicator Edisode Digits": "அத்தியாய குறியீட்டு இலக்கங்கள்", + "Indicator Season Digits": "சீசன் குறியீட்டு இலக்கங்கள்", + "Keep Editing": "திருத்தலை தொடரு", + "Keeping pending changes.": "நிலுவையில் உள்ள மாற்றங்கள் வைக்கப்படுகின்றன.", + "Key": "சாவி", + "Language": "மொழி", + "Last Episode": "கடைசி அத்தியாயம்", + "Last episode": "கடைசி அத்தியாயம்", + "Layout": "அமைப்பு", + "Media Tags": "மீடியா குறிச்சொற்கள்", + "More than one default audio stream detected and no prompt set": "ஒருக்கும் மேற்பட்ட இயல்புநிலை ஒலி ஸ்ட்ரீம்கள் கண்டறியப்பட்டன, மேலும் எந்த prompt-வும் அமைக்கப்படவில்லை", + "More than one default audio stream detected! Please select stream": "ஒருக்கும் மேற்பட்ட இயல்புநிலை ஒலி ஸ்ட்ரீம்கள் கண்டறியப்பட்டன! ஸ்ட்ரீமைத் தேர்ந்தெடுக்கவும்", + "More than one default subtitle stream detected and no prompt set": "ஒருக்கும் மேற்பட்ட இயல்புநிலை வசன ஸ்ட்ரீம்கள் கண்டறியப்பட்டன, மேலும் எந்த prompt-வும் அமைக்கப்படவில்லை", + "More than one default subtitle stream detected! Please select stream": "ஒருக்கும் மேற்பட்ட இயல்புநிலை வசன ஸ்ட்ரீம்கள் கண்டறியப்பட்டன! ஸ்ட்ரீமைத் தேர்ந்தெடுக்கவும்", + "More than one default video stream detected and no prompt set": "ஒருக்கும் மேற்பட்ட இயல்புநிலை வீடியோ ஸ்ட்ரீம்கள் கண்டறியப்பட்டன, மேலும் எந்த prompt-வும் அமைக்கப்படவில்லை", + "More than one default video stream detected! Please select stream": "ஒருக்கும் மேற்பட்ட இயல்புநிலை வீடியோ ஸ்ட்ரீம்கள் கண்டறியப்பட்டன! ஸ்ட்ரீமைத் தேர்ந்தெடுக்கவும்", + "More than one forced audio stream detected and no prompt set": "ஒருக்கும் மேற்பட்ட கட்டாய ஒலி ஸ்ட்ரீம்கள் கண்டறியப்பட்டன, மேலும் எந்த prompt-வும் அமைக்கப்படவில்லை", + "More than one forced audio stream detected! Please select stream": "ஒருக்கும் மேற்பட்ட கட்டாய ஒலி ஸ்ட்ரீம்கள் கண்டறியப்பட்டன! ஸ்ட்ரீமைத் தேர்ந்தெடுக்கவும்", + "More than one forced subtitle stream detected and no prompt set": "ஒருக்கும் மேற்பட்ட கட்டாய வசன ஸ்ட்ரீம்கள் கண்டறியப்பட்டன, மேலும் எந்த prompt-வும் அமைக்கப்படவில்லை", + "More than one forced subtitle stream detected! Please select stream": "ஒருக்கும் மேற்பட்ட கட்டாய வசன ஸ்ட்ரீம்கள் கண்டறியப்பட்டன! ஸ்ட்ரீமைத் தேர்ந்தெடுக்கவும்", + "More than one forced video stream detected and no prompt set": "ஒருக்கும் மேற்பட்ட கட்டாய வீடியோ ஸ்ட்ரீம்கள் கண்டறியப்பட்டன, மேலும் எந்த prompt-வும் அமைக்கப்படவில்லை", + "More than one forced video stream detected! Please select stream": "ஒருக்கும் மேற்பட்ட கட்டாய வீடியோ ஸ்ட்ரீம்கள் கண்டறியப்பட்டன! ஸ்ட்ரீமைத் தேர்ந்தெடுக்கவும்", + "Name": "பெயர்", + "New Pattern": "புதிய வடிவம்", + "New Show": "புதிய தொடர்", + "New filename pattern": "புதிய கோப்பு பெயர் வடிவம்", + "New shifted season": "புதிய மாற்றிய சீசன்", + "New stream": "புதிய ஸ்ட்ரீம்", + "No": "இல்லை", + "No changes to apply.": "பயன்படுத்த மாற்றங்கள் இல்லை.", + "No changes to revert.": "மீட்டெடுக்க மாற்றங்கள் இல்லை.", + "Normalization disabled.": "சீரமைப்பு முடக்கப்பட்டது.", + "Normalization enabled.": "சீரமைப்பு இயக்கப்பட்டது.", + "Normalize": "சீரமை", + "Notes": "குறிப்புகள்", + "Pattern": "வடிவம்", + "Planned Changes (file->edited output)": "திட்டமிட்ட மாற்றங்கள் (கோப்பு->திருத்திய வெளியீடு)", + "Quality": "தரம்", + "Quit": "வெளியேறு", + "Remove Pattern": "வடிவத்தை நீக்கு", + "Revert": "மீட்டு", + "Reverted pending changes.": "நிலுவையில் உள்ள மாற்றங்கள் மீட்டெடுக்கப்பட்டன.", + "Save": "சேமி", + "Season Offset": "சீசன் இடச்சரிவு", + "Select a stream first.": "முதலில் ஒரு ஸ்ட்ரீமைத் தேர்ந்தெடுக்கவும்.", + "Set Default": "இயல்புநிலையாக அமை", + "Set Forced": "கட்டாயமாக அமை", + "Settings Screen": "அமைப்புகள் திரை", + "Numbering Mapping": "மாற்றிய சீசன்கள்", + "Show": "தொடர்", + "Shows": "தொடர்கள்", + "Source Season": "மூல சீசன்", + "SrcIndex": "மூலச் சுட்டி", + "Status": "நிலை", + "Stay": "இரு", + "Stream dispositions": "ஸ்ட்ரீம் disposition-கள்", + "Stream tags": "ஸ்ட்ரீம் குறிச்சொற்கள்", + "Streams": "ஸ்ட்ரீம்கள்", + "SubIndex": "துணைச்சுட்டி", + "Substitute": "மாற்று", + "Substitute pattern": "வடிவத்தை மாற்று", + "Title": "தலைப்பு", + "Type": "வகை", + "Unable to update selected stream.": "தேர்ந்தெடுக்கப்பட்ட ஸ்ட்ரீமைப் புதுப்பிக்க முடியவில்லை.", + "Up": "மேல்", + "Update Pattern": "வடிவத்தை புதுப்பி", + "Updated media tag {tag!r}.": "மீடியா குறிச்சொல் {tag!r} புதுப்பிக்கப்பட்டது.", + "Updated stream #{index} ({track_type}).": "ஸ்ட்ரீம் #{index} ({track_type}) புதுப்பிக்கப்பட்டது.", + "Value": "மதிப்பு", + "Year": "ஆண்டு", + "Yes": "ஆம்", + "add media tag: key='{key}' value='{value}'": "மீடியா குறிச்சொல் சேர்: key='{key}' value='{value}'", + "add {track_type} track: index={index} lang={language}": "{track_type} ஸ்ட்ரீம் சேர்: index={index} lang={language}", + "attached_pic": "attached_pic", + "attachment": "இணைப்பு", + "audio": "ஒலி", + "captions": "உரைப்பதிவுகள்", + "change media tag: key='{key}' value='{value}'": "மீடியா குறிச்சொல் மாற்று: key='{key}' value='{value}'", + "change stream #{index} ({track_type}:{sub_index}) add disposition={disposition}": "ஸ்ட்ரீம் #{index} ({track_type}:{sub_index}) disposition சேர்={disposition}", + "change stream #{index} ({track_type}:{sub_index}) add key={key} value={value}": "ஸ்ட்ரீம் #{index} ({track_type}:{sub_index}) key சேர்={key} value={value}", + "change stream #{index} ({track_type}:{sub_index}) change key={key} value={value}": "ஸ்ட்ரீம் #{index} ({track_type}:{sub_index}) key மாற்று={key} value={value}", + "change stream #{index} ({track_type}:{sub_index}) remove disposition={disposition}": "ஸ்ட்ரீம் #{index} ({track_type}:{sub_index}) disposition நீக்கு={disposition}", + "change stream #{index} ({track_type}:{sub_index}) remove key={key} value={value}": "ஸ்ட்ரீம் #{index} ({track_type}:{sub_index}) key நீக்கு={key} value={value}", + "clean_effects": "ஒலி விளைவுகள் மட்டும்", + "comment": "கருத்துரை", + "default": "இயல்புநிலை", + "dependent": "சார்ந்த", + "descriptions": "விளக்கங்கள்", + "dub": "டப்", + "for pattern": "வடிவத்திற்கு", + "forced": "கட்டாயம்", + "from": "இருந்து", + "from pattern": "வடிவத்திலிருந்து", + "from show": "தொடரிலிருந்து", + "hearing_impaired": "கேள்வித்திறன் குறைபாடு", + "karaoke": "கரோக்கே", + "lyrics": "பாடல்வரிகள்", + "metadata": "மெட்டாடேட்டா", + "non_diegetic": "அல்லாத-டைஜெடிக்", + "original": "மூலம்", + "pattern #{id}": "வடிவு #{id}", + "remove media tag: key='{key}' value='{value}'": "மீடியா குறிச்சொல் நீக்கு: key='{key}' value='{value}'", + "remove stream #{index}": "ஸ்ட்ரீம் #{index} நீக்கு", + "show #{id}": "தொடர் #{id}", + "stereo": "ஸ்டீரியோ", + "still_image": "நிலைப்படம்", + "sub index": "துணைச்சுட்டி", + "subtitle": "வசனம்", + "timed_thumbnails": "நேர நிர்ணய சிறுபடங்கள்", + "undefined": "வரையறுக்கப்படாத", + "unknown": "தெரியாத", + "video": "வீடியோ", + "visual_impaired": "பார்வைத்திறன் குறைபாடு" + } +} diff --git a/pyproject.toml b/pyproject.toml index 2685f1a..7bb1f25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ffx" description = "FFX recoding and metadata managing tool" -version = "0.2.5" +version = "0.2.6" license = {file = "LICENSE.md"} dependencies = [ "requests", diff --git a/requirements/project.md b/requirements/project.md index 1e81e40..7de50a7 100644 --- a/requirements/project.md +++ b/requirements/project.md @@ -98,7 +98,7 @@ - Intended for local execution, not server deployment. - Stores default state in `~/.local/etc/ffx.json`, `~/.local/var/ffx/ffx.db`, and `~/.local/var/log/ffx.log`. - Timeline constraints: - - The current implemented scope reflects a compact alpha release stream up to version `0.2.5`. + - The current implemented scope reflects a compact alpha release stream up to version `0.2.6`. - Team capacity assumptions: - Maintained as a small codebase where simple patterns and direct controller logic are preferred over framework-heavy abstractions. - Third-party dependencies: diff --git a/requirements/shifted_seasons_handling.md b/requirements/shifted_seasons_handling.md index b20cd30..b00f300 100644 --- a/requirements/shifted_seasons_handling.md +++ b/requirements/shifted_seasons_handling.md @@ -1,4 +1,4 @@ -# Shifted Seasons Handling +# Numbering Mapping Handling This file defines the behavioral contract for mapping source season and episode numbering to target season and episode numbering through stored shifted-season diff --git a/src/ffx/_iso_language.py b/src/ffx/_iso_language.py new file mode 100644 index 0000000..4dab399 --- /dev/null +++ b/src/ffx/_iso_language.py @@ -0,0 +1,220 @@ +from enum import Enum +import difflib + + +class IsoLanguage(Enum): + + ABKHAZIAN = {"name": "Abkhazian", "iso639_1": "ab", "iso639_2": ["abk"]} + AFAR = {"name": "Afar", "iso639_1": "aa", "iso639_2": ["aar"]} + AFRIKAANS = {"name": "Afrikaans", "iso639_1": "af", "iso639_2": ["afr"]} + AKAN = {"name": "Akan", "iso639_1": "ak", "iso639_2": ["aka"]} + ALBANIAN = {"name": "Albanian", "iso639_1": "sq", "iso639_2": ["sqi", "alb"]} + AMHARIC = {"name": "Amharic", "iso639_1": "am", "iso639_2": ["amh"]} + ARABIC = {"name": "Arabic", "iso639_1": "ar", "iso639_2": ["ara"]} + ARAGONESE = {"name": "Aragonese", "iso639_1": "an", "iso639_2": ["arg"]} + ARMENIAN = {"name": "Armenian", "iso639_1": "hy", "iso639_2": ["hye", "arm"]} + ASSAMESE = {"name": "Assamese", "iso639_1": "as", "iso639_2": ["asm"]} + AVARIC = {"name": "Avaric", "iso639_1": "av", "iso639_2": ["ava"]} + AVESTAN = {"name": "Avestan", "iso639_1": "ae", "iso639_2": ["ave"]} + AYMARA = {"name": "Aymara", "iso639_1": "ay", "iso639_2": ["aym"]} + AZERBAIJANI = {"name": "Azerbaijani", "iso639_1": "az", "iso639_2": ["aze"]} + BAMBARA = {"name": "Bambara", "iso639_1": "bm", "iso639_2": ["bam"]} + BASHKIR = {"name": "Bashkir", "iso639_1": "ba", "iso639_2": ["bak"]} + BASQUE = {"name": "Basque", "iso639_1": "eu", "iso639_2": ["eus", "baq"]} + BELARUSIAN = {"name": "Belarusian", "iso639_1": "be", "iso639_2": ["bel"]} + BENGALI = {"name": "Bengali", "iso639_1": "bn", "iso639_2": ["ben"]} + BISLAMA = {"name": "Bislama", "iso639_1": "bi", "iso639_2": ["bis"]} + BOKMAL = {"name": "Bokmål", "iso639_1": "nb", "iso639_2": ["nob"]} + BOSNIAN = {"name": "Bosnian", "iso639_1": "bs", "iso639_2": ["bos"]} + BRETON = {"name": "Breton", "iso639_1": "br", "iso639_2": ["bre"]} + BULGARIAN = {"name": "Bulgarian", "iso639_1": "bg", "iso639_2": ["bul"]} + BURMESE = {"name": "Burmese", "iso639_1": "my", "iso639_2": ["mya", "bur"]} + CATALAN = {"name": "Catalan", "iso639_1": "ca", "iso639_2": ["cat"]} + CHAMORRO = {"name": "Chamorro", "iso639_1": "ch", "iso639_2": ["cha"]} + CHECHEN = {"name": "Chechen", "iso639_1": "ce", "iso639_2": ["che"]} + CHICHEWA = {"name": "Chichewa", "iso639_1": "ny", "iso639_2": ["nya"]} + CHINESE = {"name": "Chinese", "iso639_1": "zh", "iso639_2": ["zho", "chi"]} + CHURCH_SLAVIC = {"name": "Church Slavic", "iso639_1": "cu", "iso639_2": ["chu"]} + CHUVASH = {"name": "Chuvash", "iso639_1": "cv", "iso639_2": ["chv"]} + CORNISH = {"name": "Cornish", "iso639_1": "kw", "iso639_2": ["cor"]} + CORSICAN = {"name": "Corsican", "iso639_1": "co", "iso639_2": ["cos"]} + CREE = {"name": "Cree", "iso639_1": "cr", "iso639_2": ["cre"]} + CROATIAN = {"name": "Croatian", "iso639_1": "hr", "iso639_2": ["hrv"]} + CZECH = {"name": "Czech", "iso639_1": "cs", "iso639_2": ["ces", "cze"]} + DANISH = {"name": "Danish", "iso639_1": "da", "iso639_2": ["dan"]} + DIVEHI = {"name": "Divehi", "iso639_1": "dv", "iso639_2": ["div"]} + DUTCH = {"name": "Dutch", "iso639_1": "nl", "iso639_2": ["nld", "dut"]} + DZONGKHA = {"name": "Dzongkha", "iso639_1": "dz", "iso639_2": ["dzo"]} + ENGLISH = {"name": "English", "iso639_1": "en", "iso639_2": ["eng"]} + ESPERANTO = {"name": "Esperanto", "iso639_1": "eo", "iso639_2": ["epo"]} + ESTONIAN = {"name": "Estonian", "iso639_1": "et", "iso639_2": ["est"]} + EWE = {"name": "Ewe", "iso639_1": "ee", "iso639_2": ["ewe"]} + FAROESE = {"name": "Faroese", "iso639_1": "fo", "iso639_2": ["fao"]} + FIJIAN = {"name": "Fijian", "iso639_1": "fj", "iso639_2": ["fij"]} + FINNISH = {"name": "Finnish", "iso639_1": "fi", "iso639_2": ["fin"]} + FRENCH = {"name": "French", "iso639_1": "fr", "iso639_2": ["fra", "fre"]} + FULAH = {"name": "Fulah", "iso639_1": "ff", "iso639_2": ["ful"]} + GALICIAN = {"name": "Galician", "iso639_1": "gl", "iso639_2": ["glg"]} + GANDA = {"name": "Ganda", "iso639_1": "lg", "iso639_2": ["lug"]} + GEORGIAN = {"name": "Georgian", "iso639_1": "ka", "iso639_2": ["kat", "geo"]} + GERMAN = {"name": "German", "iso639_1": "de", "iso639_2": ["deu", "ger"]} + GREEK = {"name": "Greek", "iso639_1": "el", "iso639_2": ["ell", "gre"]} + GUARANI = {"name": "Guarani", "iso639_1": "gn", "iso639_2": ["grn"]} + GUJARATI = {"name": "Gujarati", "iso639_1": "gu", "iso639_2": ["guj"]} + HAITIAN = {"name": "Haitian", "iso639_1": "ht", "iso639_2": ["hat"]} + HAUSA = {"name": "Hausa", "iso639_1": "ha", "iso639_2": ["hau"]} + HEBREW = {"name": "Hebrew", "iso639_1": "he", "iso639_2": ["heb"]} + HERERO = {"name": "Herero", "iso639_1": "hz", "iso639_2": ["her"]} + HINDI = {"name": "Hindi", "iso639_1": "hi", "iso639_2": ["hin"]} + HIRI_MOTU = {"name": "Hiri Motu", "iso639_1": "ho", "iso639_2": ["hmo"]} + HUNGARIAN = {"name": "Hungarian", "iso639_1": "hu", "iso639_2": ["hun"]} + ICELANDIC = {"name": "Icelandic", "iso639_1": "is", "iso639_2": ["isl", "ice"]} + IDO = {"name": "Ido", "iso639_1": "io", "iso639_2": ["ido"]} + IGBO = {"name": "Igbo", "iso639_1": "ig", "iso639_2": ["ibo"]} + INDONESIAN = {"name": "Indonesian", "iso639_1": "id", "iso639_2": ["ind"]} + INTERLINGUA = {"name": "Interlingua", "iso639_1": "ia", "iso639_2": ["ina"]} + INTERLINGUE = {"name": "Interlingue", "iso639_1": "ie", "iso639_2": ["ile"]} + INUKTITUT = {"name": "Inuktitut", "iso639_1": "iu", "iso639_2": ["iku"]} + INUPIAQ = {"name": "Inupiaq", "iso639_1": "ik", "iso639_2": ["ipk"]} + IRISH = {"name": "Irish", "iso639_1": "ga", "iso639_2": ["gle"]} + ITALIAN = {"name": "Italian", "iso639_1": "it", "iso639_2": ["ita"]} + JAPANESE = {"name": "Japanese", "iso639_1": "ja", "iso639_2": ["jpn"]} + JAVANESE = {"name": "Javanese", "iso639_1": "jv", "iso639_2": ["jav"]} + KALAALLISUT = {"name": "Kalaallisut", "iso639_1": "kl", "iso639_2": ["kal"]} + KANNADA = {"name": "Kannada", "iso639_1": "kn", "iso639_2": ["kan"]} + KANURI = {"name": "Kanuri", "iso639_1": "kr", "iso639_2": ["kau"]} + KASHMIRI = {"name": "Kashmiri", "iso639_1": "ks", "iso639_2": ["kas"]} + KAZAKH = {"name": "Kazakh", "iso639_1": "kk", "iso639_2": ["kaz"]} + KHMER = {"name": "Khmer", "iso639_1": "km", "iso639_2": ["khm"]} + KIKUYU = {"name": "Kikuyu", "iso639_1": "ki", "iso639_2": ["kik"]} + KINYARWANDA = {"name": "Kinyarwanda", "iso639_1": "rw", "iso639_2": ["kin"]} + KIRGHIZ = {"name": "Kirghiz", "iso639_1": "ky", "iso639_2": ["kir"]} + KOMI = {"name": "Komi", "iso639_1": "kv", "iso639_2": ["kom"]} + KONGO = {"name": "Kongo", "iso639_1": "kg", "iso639_2": ["kon"]} + KOREAN = {"name": "Korean", "iso639_1": "ko", "iso639_2": ["kor"]} + KUANYAMA = {"name": "Kuanyama", "iso639_1": "kj", "iso639_2": ["kua"]} + KURDISH = {"name": "Kurdish", "iso639_1": "ku", "iso639_2": ["kur"]} + LAO = {"name": "Lao", "iso639_1": "lo", "iso639_2": ["lao"]} + LATIN = {"name": "Latin", "iso639_1": "la", "iso639_2": ["lat"]} + LATVIAN = {"name": "Latvian", "iso639_1": "lv", "iso639_2": ["lav"]} + LIMBURGAN = {"name": "Limburgan", "iso639_1": "li", "iso639_2": ["lim"]} + LINGALA = {"name": "Lingala", "iso639_1": "ln", "iso639_2": ["lin"]} + LITHUANIAN = {"name": "Lithuanian", "iso639_1": "lt", "iso639_2": ["lit"]} + LUBA_KATANGA = {"name": "Luba-Katanga", "iso639_1": "lu", "iso639_2": ["lub"]} + LUXEMBOURGISH = {"name": "Luxembourgish", "iso639_1": "lb", "iso639_2": ["ltz"]} + MACEDONIAN = {"name": "Macedonian", "iso639_1": "mk", "iso639_2": ["mkd", "mac"]} + MALAGASY = {"name": "Malagasy", "iso639_1": "mg", "iso639_2": ["mlg"]} + MALAY = {"name": "Malay", "iso639_1": "ms", "iso639_2": ["msa", "may"]} + MALAYALAM = {"name": "Malayalam", "iso639_1": "ml", "iso639_2": ["mal"]} + MALTESE = {"name": "Maltese", "iso639_1": "mt", "iso639_2": ["mlt"]} + MANX = {"name": "Manx", "iso639_1": "gv", "iso639_2": ["glv"]} + MAORI = {"name": "Maori", "iso639_1": "mi", "iso639_2": ["mri", "mao"]} + MARATHI = {"name": "Marathi", "iso639_1": "mr", "iso639_2": ["mar"]} + MARSHALLESE = {"name": "Marshallese", "iso639_1": "mh", "iso639_2": ["mah"]} + MONGOLIAN = {"name": "Mongolian", "iso639_1": "mn", "iso639_2": ["mon"]} + NAURU = {"name": "Nauru", "iso639_1": "na", "iso639_2": ["nau"]} + NAVAJO = {"name": "Navajo", "iso639_1": "nv", "iso639_2": ["nav"]} + NDONGA = {"name": "Ndonga", "iso639_1": "ng", "iso639_2": ["ndo"]} + NEPALI = {"name": "Nepali", "iso639_1": "ne", "iso639_2": ["nep"]} + NORTH_NDEBELE = {"name": "North Ndebele", "iso639_1": "nd", "iso639_2": ["nde"]} + NORTHERN_SAMI = {"name": "Northern Sami", "iso639_1": "se", "iso639_2": ["sme"]} + NORWEGIAN = {"name": "Norwegian", "iso639_1": "no", "iso639_2": ["nor"]} + NORWEGIAN_NYNORSK = {"name": "Nynorsk", "iso639_1": "nn", "iso639_2": ["nno"]} + OCCITAN = {"name": "Occitan", "iso639_1": "oc", "iso639_2": ["oci"]} + OJIBWA = {"name": "Ojibwa", "iso639_1": "oj", "iso639_2": ["oji"]} + ORIYA = {"name": "Oriya", "iso639_1": "or", "iso639_2": ["ori"]} + OROMO = {"name": "Oromo", "iso639_1": "om", "iso639_2": ["orm"]} + OSSETIAN = {"name": "Ossetian", "iso639_1": "os", "iso639_2": ["oss"]} + PALI = {"name": "Pali", "iso639_1": "pi", "iso639_2": ["pli"]} + PANJABI = {"name": "Panjabi", "iso639_1": "pa", "iso639_2": ["pan"]} + PERSIAN = {"name": "Persian", "iso639_1": "fa", "iso639_2": ["fas", "per"]} + POLISH = {"name": "Polish", "iso639_1": "pl", "iso639_2": ["pol"]} + PORTUGUESE = {"name": "Portuguese", "iso639_1": "pt", "iso639_2": ["por"]} + PUSHTO = {"name": "Pushto", "iso639_1": "ps", "iso639_2": ["pus"]} + QUECHUA = {"name": "Quechua", "iso639_1": "qu", "iso639_2": ["que"]} + ROMANIAN = {"name": "Romanian", "iso639_1": "ro", "iso639_2": ["ron", "rum"]} + ROMANSH = {"name": "Romansh", "iso639_1": "rm", "iso639_2": ["roh"]} + RUNDI = {"name": "Rundi", "iso639_1": "rn", "iso639_2": ["run"]} + RUSSIAN = {"name": "Russian", "iso639_1": "ru", "iso639_2": ["rus"]} + SAMOAN = {"name": "Samoan", "iso639_1": "sm", "iso639_2": ["smo"]} + SANGO = {"name": "Sango", "iso639_1": "sg", "iso639_2": ["sag"]} + SANSKRIT = {"name": "Sanskrit", "iso639_1": "sa", "iso639_2": ["san"]} + SARDINIAN = {"name": "Sardinian", "iso639_1": "sc", "iso639_2": ["srd"]} + SCOTTISH_GAELIC = {"name": "Scottish Gaelic", "iso639_1": "gd", "iso639_2": ["gla"]} + SERBIAN = {"name": "Serbian", "iso639_1": "sr", "iso639_2": ["srp"]} + SHONA = {"name": "Shona", "iso639_1": "sn", "iso639_2": ["sna"]} + SICHUAN_YI = {"name": "Sichuan Yi", "iso639_1": "ii", "iso639_2": ["iii"]} + SINDHI = {"name": "Sindhi", "iso639_1": "sd", "iso639_2": ["snd"]} + SINHALA = {"name": "Sinhala", "iso639_1": "si", "iso639_2": ["sin"]} + SLOVAK = {"name": "Slovak", "iso639_1": "sk", "iso639_2": ["slk", "slo"]} + SLOVENIAN = {"name": "Slovenian", "iso639_1": "sl", "iso639_2": ["slv"]} + SOMALI = {"name": "Somali", "iso639_1": "so", "iso639_2": ["som"]} + SOUTH_NDEBELE = {"name": "South Ndebele", "iso639_1": "nr", "iso639_2": ["nbl"]} + SOUTHERN_SOTHO = {"name": "Southern Sotho", "iso639_1": "st", "iso639_2": ["sot"]} + SPANISH = {"name": "Spanish", "iso639_1": "es", "iso639_2": ["spa"]} + SUNDANESE = {"name": "Sundanese", "iso639_1": "su", "iso639_2": ["sun"]} + SWAHILI = {"name": "Swahili", "iso639_1": "sw", "iso639_2": ["swa"]} + SWATI = {"name": "Swati", "iso639_1": "ss", "iso639_2": ["ssw"]} + SWEDISH = {"name": "Swedish", "iso639_1": "sv", "iso639_2": ["swe"]} + TAGALOG = {"name": "Tagalog", "iso639_1": "tl", "iso639_2": ["tgl"]} + TAHITIAN = {"name": "Tahitian", "iso639_1": "ty", "iso639_2": ["tah"]} + TAJIK = {"name": "Tajik", "iso639_1": "tg", "iso639_2": ["tgk"]} + TAMIL = {"name": "Tamil", "iso639_1": "ta", "iso639_2": ["tam"]} + TATAR = {"name": "Tatar", "iso639_1": "tt", "iso639_2": ["tat"]} + TELUGU = {"name": "Telugu", "iso639_1": "te", "iso639_2": ["tel"]} + THAI = {"name": "Thai", "iso639_1": "th", "iso639_2": ["tha"]} + TIBETAN = {"name": "Tibetan", "iso639_1": "bo", "iso639_2": ["bod", "tib"]} + TIGRINYA = {"name": "Tigrinya", "iso639_1": "ti", "iso639_2": ["tir"]} + TONGA = {"name": "Tonga", "iso639_1": "to", "iso639_2": ["ton"]} + TSONGA = {"name": "Tsonga", "iso639_1": "ts", "iso639_2": ["tso"]} + TSWANA = {"name": "Tswana", "iso639_1": "tn", "iso639_2": ["tsn"]} + TURKISH = {"name": "Turkish", "iso639_1": "tr", "iso639_2": ["tur"]} + TURKMEN = {"name": "Turkmen", "iso639_1": "tk", "iso639_2": ["tuk"]} + TWI = {"name": "Twi", "iso639_1": "tw", "iso639_2": ["twi"]} + UIGHUR = {"name": "Uighur", "iso639_1": "ug", "iso639_2": ["uig"]} + UKRAINIAN = {"name": "Ukrainian", "iso639_1": "uk", "iso639_2": ["ukr"]} + URDU = {"name": "Urdu", "iso639_1": "ur", "iso639_2": ["urd"]} + UZBEK = {"name": "Uzbek", "iso639_1": "uz", "iso639_2": ["uzb"]} + VENDA = {"name": "Venda", "iso639_1": "ve", "iso639_2": ["ven"]} + VIETNAMESE = {"name": "Vietnamese", "iso639_1": "vi", "iso639_2": ["vie"]} + VOLAPUK = {"name": "Volapük", "iso639_1": "vo", "iso639_2": ["vol"]} + WALLOON = {"name": "Walloon", "iso639_1": "wa", "iso639_2": ["wln"]} + WELSH = {"name": "Welsh", "iso639_1": "cy", "iso639_2": ["cym", "wel"]} + WESTERN_FRISIAN = {"name": "Western Frisian", "iso639_1": "fy", "iso639_2": ["fry"]} + WOLOF = {"name": "Wolof", "iso639_1": "wo", "iso639_2": ["wol"]} + XHOSA = {"name": "Xhosa", "iso639_1": "xh", "iso639_2": ["xho"]} + YIDDISH = {"name": "Yiddish", "iso639_1": "yi", "iso639_2": ["yid"]} + YORUBA = {"name": "Yoruba", "iso639_1": "yo", "iso639_2": ["yor"]} + ZHUANG = {"name": "Zhuang", "iso639_1": "za", "iso639_2": ["zha"]} + ZULU = {"name": "Zulu", "iso639_1": "zu", "iso639_2": ["zul"]} + + FILIPINO = {"name": "Filipino", "iso639_1": "tl", "iso639_2": ["fil"]} + + UNDEFINED = {"name": "undefined", "iso639_1": "xx", "iso639_2": ["und"]} + + + @staticmethod + def find(label : str): + + closestMatches = difflib.get_close_matches(label, [l.value["name"] for l in IsoLanguage], n=1) + + if closestMatches: + foundLangs = [l for l in IsoLanguage if l.value["name"] == closestMatches[0]] + return foundLangs[0] if foundLangs else IsoLanguage.UNDEFINED + else: + return IsoLanguage.UNDEFINED + + @staticmethod + def findThreeLetter(theeLetter : str): + foundLangs = [l for l in IsoLanguage if str(theeLetter) in l.value["iso639_2"]] + return foundLangs[0] if foundLangs else IsoLanguage.UNDEFINED + + + def label(self): + return str(self.value["name"]) + + def twoLetter(self): + return str(self.value["iso639_1"]) + + def threeLetter(self): + return str(self.value["iso639_2"][0]) diff --git a/src/ffx/cli.py b/src/ffx/cli.py index 756ee91..d827108 100755 --- a/src/ffx/cli.py +++ b/src/ffx/cli.py @@ -250,10 +250,11 @@ def buildRenameTargetFilename( @click.group() @click.pass_context +@click.option('--language', 'app_language', type=str, default='', help='Set application language') @click.option('--database-file', type=str, default='', help='Path to database file') @click.option('-v', '--verbose', type=int, default=0, help='Set verbosity of output') @click.option("--dry-run", is_flag=True, default=False) -def ffx(ctx, database_file, verbose, dry_run): +def ffx(ctx, app_language, database_file, verbose, dry_run): """FFX""" ctx.obj = {} @@ -261,6 +262,19 @@ def ffx(ctx, database_file, verbose, dry_run): if ctx.resilient_parsing: return + from ffx.i18n import ( + read_configured_language, + resolve_application_language, + set_current_language, + ) + + resolvedLanguage = resolve_application_language( + cli_language=app_language, + config_language=read_configured_language(), + ) + set_current_language(resolvedLanguage) + ctx.obj['language'] = resolvedLanguage + if ctx.invoked_subcommand in LIGHTWEIGHT_COMMANDS: ctx.obj['dry_run'] = dry_run ctx.obj['verbosity'] = verbose @@ -273,6 +287,11 @@ def ffx(ctx, database_file, verbose, dry_run): ctx.obj['dry_run'] = dry_run ctx.obj['verbosity'] = verbose + ctx.obj['language'] = resolve_application_language( + cli_language=app_language, + config_language=ctx.obj['config'].getLanguage(), + ) + set_current_language(ctx.obj['language']) # Critical 50 # Error 40 @@ -520,14 +539,8 @@ def inspect(ctx, shift, filenames): @ffx.command() @click.pass_context -@click.option( - '--no-cleanup', - is_flag=True, - default=False, - help='Disable config-driven metadata cleanup and only apply manual edits.', -) @click.argument('filename', nargs=1) -def edit(ctx, no_cleanup, filename): +def edit(ctx, filename): if not os.path.isfile(filename): raise click.ClickException(f"File not found: {filename}") @@ -537,7 +550,8 @@ def edit(ctx, no_cleanup, filename): ctx.obj['arguments'] = {'filename': filename} ctx.obj['use_pattern'] = False ctx.obj['no_signature'] = True - ctx.obj['apply_metadata_cleanup'] = not no_cleanup + ctx.obj['apply_metadata_cleanup'] = True + ctx.obj['apply_metadata_normalization'] = True ctx.obj['resource_limits'] = ctx.obj.get('resource_limits', {}) app = FfxApp(ctx.obj) @@ -832,6 +846,7 @@ def shows(ctx): def checkUniqueDispositions(context, mediaDescriptor: MediaDescriptor): + from ffx.i18n import t from ffx.track_disposition import TrackDisposition from ffx.track_type import TrackType @@ -841,38 +856,38 @@ def checkUniqueDispositions(context, mediaDescriptor: MediaDescriptor): # The correct tokens should then be created by if len([v for v in mediaDescriptor.getVideoTracks() if v.getDispositionFlag(TrackDisposition.DEFAULT)]) > 1: if context['no_prompt']: - raise click.ClickException('More than one default video stream detected and no prompt set') - defaultVideoTrackSubIndex = click.prompt("More than one default video stream detected! Please select stream", type=int) + raise click.ClickException(t('More than one default video stream detected and no prompt set')) + defaultVideoTrackSubIndex = click.prompt(t("More than one default video stream detected! Please select stream"), type=int) mediaDescriptor.setDefaultSubTrack(TrackType.VIDEO, defaultVideoTrackSubIndex) if len([v for v in mediaDescriptor.getVideoTracks() if v.getDispositionFlag(TrackDisposition.FORCED)]) > 1: if context['no_prompt']: - raise click.ClickException('More than one forced video stream detected and no prompt set') - forcedVideoTrackSubIndex = click.prompt("More than one forced video stream detected! Please select stream", type=int) + raise click.ClickException(t('More than one forced video stream detected and no prompt set')) + forcedVideoTrackSubIndex = click.prompt(t("More than one forced video stream detected! Please select stream"), type=int) mediaDescriptor.setForcedSubTrack(TrackType.VIDEO, forcedVideoTrackSubIndex) if len([a for a in mediaDescriptor.getAudioTracks() if a.getDispositionFlag(TrackDisposition.DEFAULT)]) > 1: if context['no_prompt']: - raise click.ClickException('More than one default audio stream detected and no prompt set') - defaultAudioTrackSubIndex = click.prompt("More than one default audio stream detected! Please select stream", type=int) + raise click.ClickException(t('More than one default audio stream detected and no prompt set')) + defaultAudioTrackSubIndex = click.prompt(t("More than one default audio stream detected! Please select stream"), type=int) mediaDescriptor.setDefaultSubTrack(TrackType.AUDIO, defaultAudioTrackSubIndex) if len([a for a in mediaDescriptor.getAudioTracks() if a.getDispositionFlag(TrackDisposition.FORCED)]) > 1: if context['no_prompt']: - raise click.ClickException('More than one forced audio stream detected and no prompt set') - forcedAudioTrackSubIndex = click.prompt("More than one forced audio stream detected! Please select stream", type=int) + raise click.ClickException(t('More than one forced audio stream detected and no prompt set')) + forcedAudioTrackSubIndex = click.prompt(t("More than one forced audio stream detected! Please select stream"), type=int) mediaDescriptor.setForcedSubTrack(TrackType.AUDIO, forcedAudioTrackSubIndex) if len([s for s in mediaDescriptor.getSubtitleTracks() if s.getDispositionFlag(TrackDisposition.DEFAULT)]) > 1: if context['no_prompt']: - raise click.ClickException('More than one default subtitle stream detected and no prompt set') - defaultSubtitleTrackSubIndex = click.prompt("More than one default subtitle stream detected! Please select stream", type=int) + raise click.ClickException(t('More than one default subtitle stream detected and no prompt set')) + defaultSubtitleTrackSubIndex = click.prompt(t("More than one default subtitle stream detected! Please select stream"), type=int) mediaDescriptor.setDefaultSubTrack(TrackType.SUBTITLE, defaultSubtitleTrackSubIndex) if len([s for s in mediaDescriptor.getSubtitleTracks() if s.getDispositionFlag(TrackDisposition.FORCED)]) > 1: if context['no_prompt']: - raise click.ClickException('More than one forced subtitle stream detected and no prompt set') - forcedSubtitleTrackSubIndex = click.prompt("More than one forced subtitle stream detected! Please select stream", type=int) + raise click.ClickException(t('More than one forced subtitle stream detected and no prompt set')) + forcedSubtitleTrackSubIndex = click.prompt(t("More than one forced subtitle stream detected! Please select stream"), type=int) mediaDescriptor.setForcedSubTrack(TrackType.SUBTITLE, forcedSubtitleTrackSubIndex) diff --git a/src/ffx/configuration_controller.py b/src/ffx/configuration_controller.py index 4a6bd29..05bc581 100644 --- a/src/ffx/configuration_controller.py +++ b/src/ffx/configuration_controller.py @@ -16,6 +16,7 @@ class ConfigurationController(): DATABASE_PATH_CONFIG_KEY = 'databasePath' LOG_DIRECTORY_CONFIG_KEY = 'logDirectory' SUBTITLES_DIRECTORY_CONFIG_KEY = 'subtitlesDirectory' + LANGUAGE_CONFIG_KEY = 'language' OUTPUT_FILENAME_TEMPLATE_KEY = 'outputFilenameTemplate' DEFAULT_INDEX_SEASON_DIGITS_CONFIG_KEY = 'defaultIndexSeasonDigits' DEFAULT_INDEX_EPISODE_DIGITS_CONFIG_KEY = 'defaultIndexEpisodeDigits' @@ -68,6 +69,9 @@ class ConfigurationController(): ) return os.path.expanduser(str(subtitlesDirectory)) if subtitlesDirectory else '' + def getLanguage(self): + return str(self.__configurationData.get(ConfigurationController.LANGUAGE_CONFIG_KEY, '')).strip() + @classmethod def getConfiguredIntegerValue(cls, configurationData: dict, configKey: str, defaultValue: int) -> int: configuredValue = configurationData.get(configKey, defaultValue) diff --git a/src/ffx/confirm_screen.py b/src/ffx/confirm_screen.py index f2e68ae..10b7994 100644 --- a/src/ffx/confirm_screen.py +++ b/src/ffx/confirm_screen.py @@ -2,18 +2,26 @@ from textual.containers import Grid from textual.screen import Screen from textual.widgets import Button, Footer, Header, Static +from .i18n import t class ConfirmScreen(Screen): + BINDINGS = [ + ("escape", "back", t("Back")), + ] + CSS = """ Grid { grid-size: 4 7; grid-rows: 2 2 2 2 2 2 2; - grid-columns: 30 30 30 30; + grid-columns: 1fr 1fr 1fr 1fr; height: 100%; width: 100%; + min-width: 80; padding: 1; + overflow-x: auto; + overflow-y: auto; } Button { @@ -33,15 +41,20 @@ class ConfirmScreen(Screen): ): super().__init__() self.__message = str(message) - self.__confirmLabel = str(confirm_label) - self.__cancelLabel = str(cancel_label) + self.__confirmLabel = str(t(confirm_label)) + self.__cancelLabel = str(t(cancel_label)) def compose(self): yield Header() with Grid(): + # Row 1 yield Static(self.__message, classes="four") + + # Row 2 yield Static(" ", classes="four") + + # Row 3 yield Button(self.__confirmLabel, id="confirm_button") yield Button(self.__cancelLabel, id="cancel_button") @@ -53,3 +66,6 @@ class ConfirmScreen(Screen): if event.button.id == "cancel_button": self.dismiss(False) + + def action_back(self): + self.dismiss(False) diff --git a/src/ffx/constants.py b/src/ffx/constants.py index 30bcc8e..7d2bee2 100644 --- a/src/ffx/constants.py +++ b/src/ffx/constants.py @@ -1,4 +1,4 @@ -VERSION='0.2.5' +VERSION='0.2.6' DATABASE_VERSION = 3 DEFAULT_QUALITY = 32 diff --git a/src/ffx/ffx_app.py b/src/ffx/ffx_app.py index 911baab..c293fb4 100644 --- a/src/ffx/ffx_app.py +++ b/src/ffx/ffx_app.py @@ -1,5 +1,6 @@ from textual.app import App +from .i18n import set_current_language, t from .shows_screen import ShowsScreen from .inspect_details_screen import InspectDetailsScreen from .media_edit_screen import MediaEditScreen @@ -10,8 +11,8 @@ class FfxApp(App): TITLE = "FFX" BINDINGS = [ - ("q", "quit()", "Quit"), - ("h", "switch_mode('help')", "Help"), + ("q", "quit()", t("Quit")), + ("h", "switch_mode('help')", t("Help")), ] @@ -20,6 +21,7 @@ class FfxApp(App): # Data 'input' variable self.context = context + set_current_language(self.context.get("language")) def on_mount(self) -> None: diff --git a/src/ffx/help_screen.py b/src/ffx/help_screen.py index b14f284..d162f6e 100644 --- a/src/ffx/help_screen.py +++ b/src/ffx/help_screen.py @@ -2,12 +2,22 @@ from textual.app import ComposeResult from textual.screen import Screen from textual.widgets import Footer, Placeholder +from .i18n import t +from .screen_support import go_back_or_exit + class HelpScreen(Screen): + BINDINGS = [ + ("escape", "back", t("Back")), + ] + def __init__(self): super().__init__() context = self.app.getContext() def compose(self) -> ComposeResult: - yield Placeholder("Help Screen") + # Row 1 + yield Placeholder(t("Help Screen")) yield Footer() + def action_back(self): + go_back_or_exit(self) diff --git a/src/ffx/i18n.py b/src/ffx/i18n.py new file mode 100644 index 0000000..13500d3 --- /dev/null +++ b/src/ffx/i18n.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +import json +import os +from pathlib import Path + + +DEFAULT_LANGUAGE = "de" +SOURCE_LANGUAGE = "en" +SUPPORTED_LANGUAGES = { + "de": "Deutsch", + "en": "English", + "fr": "Français", + "ja": "日本語", + "nb": "Norsk bokmål", + "eo": "Esperanto", + "ta": "தமிழ்", + "pt": "Português", + "es": "Español", +} +LANGUAGE_ALIASES = { + "deu": "de", + "ger": "de", + "english": "en", + "eng": "en", + "fra": "fr", + "fre": "fr", + "french": "fr", + "jpn": "ja", + "japanese": "ja", + "nor": "nb", + "nob": "nb", + "no": "nb", + "nn": "nb", + "bokmal": "nb", + "norwegian": "nb", + "epo": "eo", + "esperanto": "eo", + "tam": "ta", + "tamil": "ta", + "por": "pt", + "portuguese": "pt", + "spa": "es", + "spanish": "es", +} + +_catalog_cache: dict[str, dict] = {} +_current_language = DEFAULT_LANGUAGE + + +def _assets_directory() -> Path: + return Path(__file__).resolve().parents[2] / "assets" / "i18n" + + +def normalize_language_code(value: str | None) -> str | None: + if value is None: + return None + + normalized = str(value).strip().replace("-", "_") + if not normalized: + return None + + base_language = normalized.split(".")[0].split("_")[0].lower() + if base_language in SUPPORTED_LANGUAGES: + return base_language + + return LANGUAGE_ALIASES.get(base_language) + + +def detect_system_language(env: dict[str, str] | None = None) -> str | None: + environment = env or os.environ + for key in ("LC_ALL", "LC_MESSAGES", "LANG"): + if language_code := normalize_language_code(environment.get(key)): + return language_code + return None + + +def get_default_config_path(home_directory: str | None = None) -> Path: + base_home = Path(home_directory or os.path.expanduser("~")) + return base_home / ".local" / "etc" / "ffx.json" + + +def read_configured_language( + config_path: str | os.PathLike | None = None, + *, + home_directory: str | None = None, +) -> str | None: + resolved_path = Path(config_path) if config_path is not None else get_default_config_path(home_directory) + if not resolved_path.is_file(): + return None + + try: + config_data = json.loads(resolved_path.read_text(encoding="utf-8")) + except (OSError, ValueError, TypeError): + return None + + return normalize_language_code(config_data.get("language")) + + +def resolve_application_language( + *, + cli_language: str | None = None, + config_language: str | None = None, + system_language: str | None = None, + env: dict[str, str] | None = None, +) -> str: + for candidate in ( + cli_language, + config_language, + system_language or detect_system_language(env), + ): + if normalized := normalize_language_code(candidate): + return normalized + + return DEFAULT_LANGUAGE + + +def set_current_language(language_code: str | None) -> str: + global _current_language + _current_language = normalize_language_code(language_code) or DEFAULT_LANGUAGE + return _current_language + + +def get_current_language() -> str: + return _current_language + + +def _load_catalog(language_code: str) -> dict: + normalized = normalize_language_code(language_code) or DEFAULT_LANGUAGE + if normalized not in _catalog_cache: + catalog_path = _assets_directory() / f"{normalized}.json" + if catalog_path.is_file(): + _catalog_cache[normalized] = json.loads(catalog_path.read_text(encoding="utf-8")) + else: + _catalog_cache[normalized] = {"phrases": {}, "iso_languages": {}} + return _catalog_cache[normalized] + + +def _lookup_phrase(language_code: str, source_text: str) -> str | None: + phrases = _load_catalog(language_code).get("phrases", {}) + return phrases.get(source_text) + + +def t(source_text: str, **kwargs) -> str: + translated = ( + _lookup_phrase(get_current_language(), source_text) + or _lookup_phrase(SOURCE_LANGUAGE, source_text) + or source_text + ) + return translated.format(**kwargs) if kwargs else translated + + +def translate_iso_language(member_name: str, fallback: str) -> str: + for language_code in (get_current_language(), SOURCE_LANGUAGE): + translations = _load_catalog(language_code).get("iso_languages", {}) + if member_name in translations: + return str(translations[member_name]) + return str(fallback) diff --git a/src/ffx/inspect_details_screen.py b/src/ffx/inspect_details_screen.py index 289a350..dc08d6d 100644 --- a/src/ffx/inspect_details_screen.py +++ b/src/ffx/inspect_details_screen.py @@ -1,6 +1,7 @@ import re import click +from rich.text import Text from textual.containers import Grid from textual.widgets import Button, Footer, Header, Input, Static @@ -11,26 +12,116 @@ from ffx.media_descriptor_change_set import MediaDescriptorChangeSet from ffx.show_descriptor import ShowDescriptor from ffx.track_descriptor import TrackDescriptor +from .i18n import t from .media_workflow_screen_base import MediaWorkflowScreenBase from .pattern_details_screen import PatternDetailsScreen -from .screen_support import build_screen_controllers +from .screen_support import ( + add_auto_table_column, + build_screen_controllers, + go_back_or_exit, + localized_column_width, + update_table_column_label, +) from .show_details_screen import ShowDetailsScreen class InspectDetailsScreen(MediaWorkflowScreenBase): + GRID_COLUMN_LABEL_MIN = 12 + GRID_COLUMN_2 = 20 + GRID_COLUMN_3 = 40 + GRID_COLUMN_4 = "4fr" + GRID_COLUMN_5 = 10 + GRID_COLUMN_6 = "5fr" + + CSS = f""" + + Grid {{ + grid-size: 6 11; + grid-rows: 9 2 2 2 2 8 2 2 2 8 8; + grid-columns: {GRID_COLUMN_LABEL_MIN} {GRID_COLUMN_2} {GRID_COLUMN_3} {GRID_COLUMN_4} {GRID_COLUMN_5} {GRID_COLUMN_6}; + height: 100%; + width: 100%; + min-width: 120; + padding: 1; + overflow-x: auto; + overflow-y: auto; + }} + + DataTable .datatable--cursor {{ + background: darkorange; + color: black; + }} + + DataTable .datatable--header {{ + background: steelblue; + color: white; + }} + + Input {{ + border: none; + }} + Button {{ + border: none; + }} + + DataTable {{ + min-height: 24; + width: 100%; + }} + + .two {{ + column-span: 2; + }} + .three {{ + column-span: 3; + }} + .four {{ + column-span: 4; + }} + .five {{ + column-span: 5; + }} + + #differences-table {{ + row-span: 10; + }} + """ + + @classmethod + def _grid_columns_spec(cls, label_column_width: int | None = None) -> str: + return " ".join( + [ + str( + cls.GRID_COLUMN_LABEL_MIN + if label_column_width is None + else int(label_column_width) + ), + str(cls.GRID_COLUMN_2), + str(cls.GRID_COLUMN_3), + str(cls.GRID_COLUMN_4), + str(cls.GRID_COLUMN_5), + str(cls.GRID_COLUMN_6), + ] + ) + + COMMAND_NAME = "inspect" DIFFERENCES_COLUMN_LABEL = "Differences (file->db/output)" BINDINGS = [ - ("q", "app.quit", "Quit"), - ("n", "new_pattern", "New Pattern"), - ("u", "update_pattern", "Update Pattern"), - ("e", "edit_pattern", "Edit Pattern"), + ("escape", "back", t("Back")), + ("q", "app.quit", t("Quit")), + ("n", "new_pattern", t("New Pattern")), + ("u", "update_pattern", t("Update Pattern")), + ("e", "edit_pattern", t("Edit Pattern")), ] def __init__(self): self._showRowData: dict[object, ShowDescriptor | None] = {} + self._showSortColumnKey = None + self._showSortReverse = False + self._showColumnLabels: dict[object, str] = {} super().__init__() controllers = build_screen_controllers( @@ -54,60 +145,159 @@ class InspectDetailsScreen(MediaWorkflowScreenBase): yield Header() - with Grid(): + with Grid(id="main_grid"): + self.showsTable = self._build_shows_table() - yield Static("Show") + # Row 1 + yield Static(t("Show")) yield self.showsTable yield Static(" ") yield self.differencesTable - yield Static(" ", classes="four") + # Row 2 + yield Static(" ", classes="five") + # Row 3 yield Static(" ") - yield Button("Substitute", id="pattern_button") + yield Button(t("Substitute"), id="pattern_button") + yield Static(" ", classes="three") + + # Row 4 + yield Static(t("Pattern")) + yield Input(type="text", id="pattern_input", classes="three") + yield Static(" ") + + # Row 5 + yield Static(" ", classes="five") + + # Row 6 + yield Static(t("Media Tags")) + yield self.mediaTagsTable yield Static(" ", classes="two") - yield Static("Pattern") - yield Input(type="text", id="pattern_input", classes="two") + # Row 7 + yield Static(" ", classes="five") + + # Row 8 yield Static(" ") + yield Button(t("Set Default"), id="select_default_button") + yield Button(t("Set Forced"), id="select_forced_button") + yield Static(" ", classes="two") - yield Static(" ", classes="four") - - yield Static("Media Tags") - yield self.mediaTagsTable - yield Static(" ") - - yield Static(" ", classes="four") - - yield Static(" ") - yield Button("Set Default", id="select_default_button") - yield Button("Set Forced", id="select_forced_button") - yield Static(" ") - - yield Static("Streams") + # Row 9 + yield Static(t("Streams")) yield self.tracksTable yield Static(" ") yield Footer() + def _update_grid_layout(self) -> None: + leftColumnWidth = max( + localized_column_width(t("Show"), self.GRID_COLUMN_LABEL_MIN), + localized_column_width(t("Pattern"), self.GRID_COLUMN_LABEL_MIN), + localized_column_width(t("Media Tags"), self.GRID_COLUMN_LABEL_MIN), + localized_column_width(t("Streams"), self.GRID_COLUMN_LABEL_MIN), + ) + grid = self.query_one("#main_grid", Grid) + grid.styles.grid_columns = self._grid_columns_spec(leftColumnWidth) + + def action_back(self): + go_back_or_exit(self) + def _build_shows_table(self): from textual.widgets import DataTable - showsTable = DataTable(classes="two") - showsTable.add_column("ID", width=10) - showsTable.add_column("Name", width=80) - showsTable.add_column("Year", width=10) + showsTable = DataTable(classes="three") + idLabel = t("ID") + nameLabel = t("Name") + yearLabel = t("Year") + self._showColumnKeyId = add_auto_table_column(showsTable, idLabel) + self._showColumnKeyName = add_auto_table_column(showsTable, nameLabel) + self._showColumnKeyYear = add_auto_table_column(showsTable, yearLabel) + self._showColumnLabels = { + self._showColumnKeyId: idLabel, + self._showColumnKeyName: nameLabel, + self._showColumnKeyYear: yearLabel, + } showsTable.cursor_type = "row" return showsTable + def _get_selected_show_row_key(self): + try: + row_key, _ = self.showsTable.coordinate_to_cell_key( + self.showsTable.cursor_coordinate + ) + return row_key + except CellDoesNotExist: + return None + + def _move_show_cursor_to_row_key(self, row_key): + if row_key is None: + return + + try: + row_index = int(self.showsTable.get_row_index(row_key)) + except Exception: + return + + self.showsTable.move_cursor(row=row_index) + + def _sort_key_for_show_column(self, column_key): + if column_key == self._showColumnKeyId: + return lambda value: int(value) if str(value).strip().isdigit() else -1 + if column_key == self._showColumnKeyYear: + return lambda value: int(value) if str(value).strip().isdigit() else -1 + if column_key == self._showColumnKeyName: + return lambda value: str(value).casefold() + return None + + def _update_show_header_labels(self): + if not hasattr(self, "showsTable"): + return + + arrow_up = "▴" + arrow_down = "▾" + + for column_key, base_label in self._showColumnLabels.items(): + column = self.showsTable.columns.get(column_key) + if column is None: + continue + + label_text = base_label + if column_key == self._showSortColumnKey: + label_text = ( + f"{base_label} " + f"{arrow_down if self._showSortReverse else arrow_up}" + ) + + update_table_column_label(self.showsTable, column_key, Text(label_text)) + + def _apply_show_sort(self, *, preserve_row_key=None): + if self._showSortColumnKey is None: + self._update_show_header_labels() + return + + self.showsTable.sort( + self._showSortColumnKey, + key=self._sort_key_for_show_column(self._showSortColumnKey), + reverse=self._showSortReverse, + ) + self._move_show_cursor_to_row_key(preserve_row_key) + self._update_show_header_labels() + def on_mount(self): + self._update_grid_layout() + if self._currentPattern is None: self._add_show_row(None) for show in self._sc.getAllShows(): self._add_show_row(show.getDescriptor(self.context)) + self._showSortColumnKey = self._showColumnKeyName + self._apply_show_sort() + if self._currentPattern is not None: showIdentifier = self._currentPattern.getShowId() showRowIndex = self.getRowIndexFromShowId(showIdentifier) @@ -143,6 +333,20 @@ class InspectDetailsScreen(MediaWorkflowScreenBase): self.updateTracks() self.updateDifferences() + def on_data_table_header_selected(self, event) -> None: + if event.data_table is not self.showsTable: + return + + selected_row_key = self._get_selected_show_row_key() + + if self._showSortColumnKey == event.column_key: + self._showSortReverse = not self._showSortReverse + else: + self._showSortColumnKey = event.column_key + self._showSortReverse = False + + self._apply_show_sort(preserve_row_key=selected_row_key) + def removeShow(self, showId: int = -1): for row_key, show_descriptor in list(self._showRowData.items()): if ( @@ -171,7 +375,7 @@ class InspectDetailsScreen(MediaWorkflowScreenBase): def _add_show_row(self, show_descriptor: ShowDescriptor | None): if show_descriptor is None: - row_key = self.showsTable.add_row(" ", "", " ") + row_key = self.showsTable.add_row(" ", t(""), " ") else: row_key = self.showsTable.add_row( str(show_descriptor.getId()), @@ -218,7 +422,8 @@ class InspectDetailsScreen(MediaWorkflowScreenBase): showRowIndex = self.getRowIndexFromShowId(showDescriptor.getId()) if showRowIndex is None: - self._add_show_row(showDescriptor) + row_key = self._add_show_row(showDescriptor) + self._apply_show_sort(preserve_row_key=row_key) showRowIndex = self.getRowIndexFromShowId(showDescriptor.getId()) if showRowIndex is not None: diff --git a/src/ffx/iso_language.py b/src/ffx/iso_language.py index 4dab399..d24ba9e 100644 --- a/src/ffx/iso_language.py +++ b/src/ffx/iso_language.py @@ -1,6 +1,8 @@ from enum import Enum import difflib +from .i18n import translate_iso_language + class IsoLanguage(Enum): @@ -196,11 +198,15 @@ class IsoLanguage(Enum): @staticmethod def find(label : str): - closestMatches = difflib.get_close_matches(label, [l.value["name"] for l in IsoLanguage], n=1) + candidate_map = {} + for language in IsoLanguage: + candidate_map[language.value["name"]] = language + candidate_map[translate_iso_language(language.name, language.value["name"])] = language + + closestMatches = difflib.get_close_matches(label, list(candidate_map.keys()), n=1) if closestMatches: - foundLangs = [l for l in IsoLanguage if l.value["name"] == closestMatches[0]] - return foundLangs[0] if foundLangs else IsoLanguage.UNDEFINED + return candidate_map.get(closestMatches[0], IsoLanguage.UNDEFINED) else: return IsoLanguage.UNDEFINED @@ -211,7 +217,7 @@ class IsoLanguage(Enum): def label(self): - return str(self.value["name"]) + return str(translate_iso_language(self.name, self.value["name"])) def twoLetter(self): return str(self.value["iso639_1"]) diff --git a/src/ffx/media_descriptor_change_set.py b/src/ffx/media_descriptor_change_set.py index ee6c3b9..282e504 100644 --- a/src/ffx/media_descriptor_change_set.py +++ b/src/ffx/media_descriptor_change_set.py @@ -8,6 +8,7 @@ from ffx.helper import dictDiff, setDiff, DIFF_ADDED_KEY, DIFF_CHANGED_KEY, DIFF from ffx.track_codec import TrackCodec from ffx.track_disposition import TrackDisposition +from ffx.track_type import TrackType class MediaDescriptorChangeSet(): @@ -30,6 +31,9 @@ class MediaDescriptorChangeSet(): metadataConfiguration = self.__configurationData['metadata'] if 'metadata' in self.__configurationData.keys() else {} applyCleanup = bool(self.__context.get('apply_metadata_cleanup', True)) + self.__applyMetadataNormalization = bool( + self.__context.get("apply_metadata_normalization", True) + ) self.__signatureTags = metadataConfiguration['signature'] if 'signature' in metadataConfiguration.keys() else {} self.__removeGlobalKeys = ( @@ -130,7 +134,11 @@ class MediaDescriptorChangeSet(): sourceTrackTags = sourceTrackDescriptor.getTags() if sourceTrackDescriptor is not None else {} targetTrackTags = ( - self.normalizeTrackTags(targetTrackDescriptor.getTags()) + self.normalizeTrackTags( + targetTrackDescriptor.getTags(), + trackDescriptor=targetTrackDescriptor, + fallbackTrackTags=sourceTrackTags, + ) if targetTrackDescriptor is not None else {} ) @@ -159,7 +167,7 @@ class MediaDescriptorChangeSet(): return trackCompareResult def normalizeTrackTagValue(self, tagKey, tagValue): - if tagKey != "language": + if not self.__applyMetadataNormalization or tagKey != "language": return tagValue if isinstance(tagValue, IsoLanguage): @@ -171,12 +179,40 @@ class MediaDescriptorChangeSet(): return tagValue - def normalizeTrackTags(self, trackTags: dict): - return { + def resolveTrackLanguage(self, tagValue): + if isinstance(tagValue, IsoLanguage): + return tagValue + + trackLanguage = IsoLanguage.findThreeLetter(str(tagValue)) + if trackLanguage != IsoLanguage.UNDEFINED: + return trackLanguage + + return None + + def normalizeTrackTags( + self, + trackTags: dict, + trackDescriptor: TrackDescriptor = None, + fallbackTrackTags: dict = None, + ): + normalizedTrackTags = { tagKey: self.normalizeTrackTagValue(tagKey, tagValue) for tagKey, tagValue in trackTags.items() } + if ( + self.__applyMetadataNormalization + and trackDescriptor is not None + and trackDescriptor.getType() == TrackType.SUBTITLE + ): + trackTitle = str(normalizedTrackTags.get("title", "")).strip() + fallbackTitle = str((fallbackTrackTags or {}).get("title", "")).strip() + trackLanguage = self.resolveTrackLanguage(normalizedTrackTags.get("language")) + if not trackTitle and not fallbackTitle and trackLanguage is not None: + normalizedTrackTags["title"] = trackLanguage.label() + + return normalizedTrackTags + def generateDispositionTokens(self): """ @@ -278,7 +314,10 @@ class MediaDescriptorChangeSet(): addedTracks: dict = self.__changeSetObj[MediaDescriptorChangeSet.TRACKS_KEY][DIFF_ADDED_KEY] trackDescriptor: TrackDescriptor for trackDescriptor in addedTracks.values(): - for tagKey, tagValue in self.normalizeTrackTags(trackDescriptor.getTags()).items(): + for tagKey, tagValue in self.normalizeTrackTags( + trackDescriptor.getTags(), + trackDescriptor=trackDescriptor, + ).items(): if not tagKey in self.__removeTrackKeys: metadataTokens += [f"-metadata:s:{trackDescriptor.getType().indicator()}" + f":{trackDescriptor.getSubIndex()}", @@ -302,7 +341,11 @@ class MediaDescriptorChangeSet(): trackDescriptor = self.__targetTrackDescriptorsByIndex[trackIndex] - for tagKey, tagValue in self.normalizeTrackTags(outputTrackTags).items(): + for tagKey, tagValue in self.normalizeTrackTags( + outputTrackTags, + trackDescriptor=trackDescriptor, + fallbackTrackTags=unchangedTrackTags | removedTrackTags, + ).items(): metadataTokens += [f"-metadata:s:{trackDescriptor.getType().indicator()}" + f":{trackDescriptor.getSubIndex()}", f"{tagKey}={tagValue}"] @@ -320,7 +363,10 @@ class MediaDescriptorChangeSet(): } | unchangedTrackTags ) - for tagKey, tagValue in self.normalizeTrackTags(preservedTrackTags).items(): + for tagKey, tagValue in self.normalizeTrackTags( + preservedTrackTags, + trackDescriptor=trackDescriptor, + ).items(): metadataTokens += [f"-metadata:s:{trackDescriptor.getType().indicator()}" + f":{trackDescriptor.getSubIndex()}", f"{tagKey}={tagValue}"] diff --git a/src/ffx/media_edit_screen.py b/src/ffx/media_edit_screen.py index 98a6b53..4d02c39 100644 --- a/src/ffx/media_edit_screen.py +++ b/src/ffx/media_edit_screen.py @@ -6,8 +6,10 @@ from textual.widgets import Button, Footer, Header, Static from ffx.metadata_editor import apply_metadata_edits from ffx.track_descriptor import TrackDescriptor +from .i18n import t from .confirm_screen import ConfirmScreen from .media_workflow_screen_base import MediaWorkflowScreenBase +from .screen_support import localized_column_width from .tag_delete_screen import TagDeleteScreen from .tag_details_screen import TagDetailsScreen from .track_details_screen import TrackDetailsScreen @@ -15,14 +17,98 @@ from .track_details_screen import TrackDetailsScreen class MediaEditScreen(MediaWorkflowScreenBase): + GRID_COLUMN_LABEL_MIN = 12 + GRID_COLUMN_2 = 20 + GRID_COLUMN_3 = 25 + GRID_COLUMN_4 = "4fr" + GRID_COLUMN_5 = 12 + GRID_COLUMN_6 = "5fr" + + CSS = f""" + + Grid {{ + grid-size: 6 10; + grid-rows: 2 2 2 8 2 2 8 2 8 2 2; + grid-columns: {GRID_COLUMN_LABEL_MIN} {GRID_COLUMN_2} {GRID_COLUMN_3} {GRID_COLUMN_4} {GRID_COLUMN_5} {GRID_COLUMN_6}; + height: 100%; + width: 100%; + min-width: 120; + padding: 1; + overflow-x: auto; + overflow-y: auto; + }} + + DataTable .datatable--cursor {{ + background: darkorange; + color: black; + }} + + DataTable .datatable--header {{ + background: steelblue; + color: white; + }} + + Input {{ + border: none; + }} + Button {{ + border: none; + }} + + DataTable {{ + min-height: 24; + width: 100%; + }} + + .two {{ + column-span: 2; + }} + .three {{ + column-span: 3; + }} + .four {{ + column-span: 4; + }} + .five {{ + column-span: 5; + }} + + #differences-table {{ + row-span: 10; + }} + + #file_label {{ + width: 100%; + }} + """ + + @classmethod + def _grid_columns_spec(cls, label_column_width: int | None = None) -> str: + return " ".join( + [ + str( + cls.GRID_COLUMN_LABEL_MIN + if label_column_width is None + else int(label_column_width) + ), + str(cls.GRID_COLUMN_2), + str(cls.GRID_COLUMN_3), + str(cls.GRID_COLUMN_4), + str(cls.GRID_COLUMN_5), + str(cls.GRID_COLUMN_6), + ] + ) + + COMMAND_NAME = "edit" EDIT_MODE = True DIFFERENCES_COLUMN_LABEL = "Planned Changes (file->edited output)" BINDINGS = [ - ("q", "quit_screen", "Quit"), - ("a", "apply_changes", "Apply"), - ("r", "revert_changes", "Revert"), + ("escape", "back", t("Back")), + ("q", "quit_screen", t("Quit")), + ("a", "apply_changes", t("Apply")), + ("r", "revert_changes", t("Revert")), ] def compose(self): @@ -32,63 +118,104 @@ class MediaEditScreen(MediaWorkflowScreenBase): yield Header() - with Grid(): - yield Static("File") - yield Static(self._mediaFilename, id="file_label", classes="two", markup=False) + with Grid(id="main_grid"): + + # Row 1 + yield Static(t("File")) + yield Static(self._mediaFilename, id="file_label", classes="three", markup=False) yield Static(" ") yield self.differencesTable - yield Static("Cleanup") - yield Static( - "Enabled" if self._applyCleanup else "Disabled", - id="cleanup_label", - classes="two", - markup=False, - ) + # Row 2 + yield Static(" ") + yield Button(t("Cleanup"), id="cleanup_toggle_button") + yield Button(t("Normalize"), id="normalize_toggle_button") + yield Static(" ", classes="two") + + # Row 3 + yield Static(t("Media Tags")) + yield Button(t("Add"), id="button_add_tag") + yield Button(t("Edit"), id="button_edit_tag") + yield Button(t("Delete"), id="button_delete_tag") yield Static(" ") - yield Static("Status") - yield Static("", id="message_label", classes="two", markup=False) - yield Static(" ") - - yield Static("Media Tags") - yield Button("Add", id="button_add_tag") - yield Button("Edit", id="button_edit_tag") - yield Button("Delete", id="button_delete_tag") - + # Row 4 yield Static(" ") yield self.mediaTagsTable yield Static(" ") - yield Static("Streams") - yield Button("Edit", id="button_edit_track") - yield Button("Set Default", id="select_default_button") - yield Button("Set Forced", id="select_forced_button") + # Row 5 + yield Static("", classes="five") + # Row 6 + yield Static(t("Streams")) + yield Button(t("Edit"), id="button_edit_track") + yield Button(t("Set Default"), id="select_default_button") + yield Button(t("Set Forced"), id="select_forced_button") + yield Static(" ") + + # Row 7 yield Static(" ") yield self.tracksTable yield Static(" ") + # Row 8 + yield Static("", classes="five") + + # Row 9 + yield Static(" ") + yield Button(t("Apply"), id="apply_button") + yield Button(t("Revert"), id="revert_button") + yield Button(t("Quit"), id="quit_button") yield Static(" ") - yield Button("Apply", id="apply_button") - yield Button("Revert", id="revert_button") - yield Button("Quit", id="quit_button") yield Footer() def on_mount(self): + self._update_grid_layout() self.updateMediaTags() self.updateTracks() self.updateDifferences() - self.setMessage(self._messageText) + self.updateToggleButtons() + + def _update_grid_layout(self) -> None: + leftColumnWidth = max( + localized_column_width(t("File"), self.GRID_COLUMN_LABEL_MIN), + localized_column_width(t("Media Tags"), self.GRID_COLUMN_LABEL_MIN), + localized_column_width(t("Streams"), self.GRID_COLUMN_LABEL_MIN), + ) + grid = self.query_one("#main_grid", Grid) + grid.styles.grid_columns = self._grid_columns_spec(leftColumnWidth) + + def action_back(self): + self.action_quit_screen() def setMessage(self, message: str): self._messageText = str(message) + if self._messageText: + self.notify(self._messageText) + def updateToggleButtons(self): + self._set_toggle_button_state( + "#cleanup_toggle_button", + t("Cleanup"), + self._applyCleanup, + ) + self._set_toggle_button_state( + "#normalize_toggle_button", + t("Normalize"), + self._applyNormalization, + ) + + def _set_toggle_button_state(self, selector: str, label: str, enabled: bool): try: - self.query_one("#message_label", Static).update(self._messageText) + button = self.query_one(selector, Button) except Exception: - pass + return + + button.label = label + button.styles.color = "black" if enabled else "white" + button.styles.background = "darkorange" if enabled else "black" def refreshAfterDraftChange(self): self.updateMediaTags() @@ -126,6 +253,12 @@ class MediaEditScreen(MediaWorkflowScreenBase): if event.button.id == "button_edit_track": self.action_edit_selected_track() + if event.button.id == "cleanup_toggle_button": + self.action_toggle_cleanup() + + if event.button.id == "normalize_toggle_button": + self.action_toggle_normalization() + if event.button.id == "apply_button": self.action_apply_changes() @@ -138,7 +271,7 @@ class MediaEditScreen(MediaWorkflowScreenBase): def action_edit_selected_track(self): selectedTrack = self.getSelectedTrackDescriptor() if selectedTrack is None: - self.setMessage("Select a stream first.") + self.setMessage(t("Select a stream first.")) return self.app.push_screen( @@ -151,12 +284,31 @@ class MediaEditScreen(MediaWorkflowScreenBase): self.handle_edit_track, ) + def action_toggle_cleanup(self): + self.setApplyCleanup(not self._applyCleanup) + self.updateToggleButtons() + self.updateMediaTags() + self.updateDifferences() + self.setMessage( + t("Cleanup enabled.") if self._applyCleanup else t("Cleanup disabled.") + ) + + def action_toggle_normalization(self): + self.setApplyNormalization(not self._applyNormalization) + self.updateToggleButtons() + self.updateDifferences() + self.setMessage( + t("Normalization enabled.") + if self._applyNormalization + else t("Normalization disabled.") + ) + def handle_update_media_tag(self, tag): if tag is None: return self._sourceMediaDescriptor.getTags()[str(tag[0])] = str(tag[1]) - self.setMessage(f"Updated media tag {tag[0]!r}.") + self.setMessage(t("Updated media tag {tag!r}.", tag=tag[0])) self.refreshAfterDraftChange() def handle_delete_media_tag(self, tag): @@ -164,7 +316,7 @@ class MediaEditScreen(MediaWorkflowScreenBase): return self._sourceMediaDescriptor.getTags().pop(str(tag[0]), None) - self.setMessage(f"Deleted media tag {tag[0]!r}.") + self.setMessage(t("Deleted media tag {tag!r}.", tag=tag[0])) self.refreshAfterDraftChange() def handle_edit_track(self, trackDescriptor: TrackDescriptor): @@ -184,20 +336,24 @@ class MediaEditScreen(MediaWorkflowScreenBase): updatedTracks.append(currentTrack) if not replaced: - self.setMessage("Unable to update selected stream.") + self.setMessage(t("Unable to update selected stream.")) return self._sourceMediaDescriptor = self._sourceMediaDescriptor.clone(context=self.context) self._sourceMediaDescriptor.getTrackDescriptors().clear() self._sourceMediaDescriptor.getTrackDescriptors().extend(updatedTracks) self.setMessage( - f"Updated stream #{trackDescriptor.getIndex()} ({trackDescriptor.getType().label()})." + t( + "Updated stream #{index} ({track_type}).", + index=trackDescriptor.getIndex(), + track_type=t(trackDescriptor.getType().label()), + ) ) self.refreshAfterDraftChange() def action_apply_changes(self): if not self.hasPendingChanges(): - self.setMessage("No changes to apply.") + self.setMessage(t("No changes to apply.")) return try: @@ -212,52 +368,55 @@ class MediaEditScreen(MediaWorkflowScreenBase): "Failed to apply metadata edits for %s", self._mediaFilename, ) - self.setMessage(f"Apply failed: {ex}") + self.setMessage(t("Apply failed: {error}", error=ex)) return if applyResult.get("dry_run", False): self.setMessage( - f"Dry-run: would rewrite via temporary file {applyResult['target_path']}" + t( + "Dry-run: would rewrite via temporary file {target_path}", + target_path=applyResult["target_path"], + ) ) return self.reloadProperties(reset_draft=True) self.refreshAfterDraftChange() - self.setMessage("Changes applied and file reloaded.") + self.setMessage(t("Changes applied and file reloaded.")) def action_revert_changes(self): if not self.hasPendingChanges(): - self.setMessage("No changes to revert.") + self.setMessage(t("No changes to revert.")) return self.app.push_screen( ConfirmScreen( - "Discard pending metadata changes and reload the file state?", - confirm_label="Discard", - cancel_label="Keep Editing", + t("Discard pending metadata changes and reload the file state?"), + confirm_label=t("Discard"), + cancel_label=t("Keep Editing"), ), self.handle_revert_confirmation, ) def handle_revert_confirmation(self, confirmed): if not confirmed: - self.setMessage("Keeping pending changes.") + self.setMessage(t("Keeping pending changes.")) return self.reloadProperties(reset_draft=True) self.refreshAfterDraftChange() - self.setMessage("Reverted pending changes.") + self.setMessage(t("Reverted pending changes.")) def action_quit_screen(self): if self.hasPendingChanges(): self.app.push_screen( ConfirmScreen( - "Discard pending metadata changes and quit?", - confirm_label="Discard", - cancel_label="Stay", - ), - self.handle_quit_confirmation, - ) + t("Discard pending metadata changes and quit?"), + confirm_label=t("Discard"), + cancel_label=t("Stay"), + ), + self.handle_quit_confirmation, + ) return self.app.exit() @@ -266,4 +425,4 @@ class MediaEditScreen(MediaWorkflowScreenBase): if confirmed: self.app.exit() else: - self.setMessage("Continuing edit session.") + self.setMessage(t("Continuing edit session.")) diff --git a/src/ffx/media_workflow_screen_base.py b/src/ffx/media_workflow_screen_base.py index 034b6ae..a46805c 100644 --- a/src/ffx/media_workflow_screen_base.py +++ b/src/ffx/media_workflow_screen_base.py @@ -14,60 +14,12 @@ from ffx.track_descriptor import TrackDescriptor from ffx.track_disposition import TrackDisposition from ffx.track_type import TrackType -from .screen_support import build_screen_bootstrap, populate_tag_table +from .i18n import t +from .screen_support import add_auto_table_column, build_screen_bootstrap, populate_tag_table class MediaWorkflowScreenBase(Screen): - CSS = """ - - Grid { - grid-size: 5 10; - grid-rows: 2 2 2 2 8 2 2 2 8 2; - grid-columns: 15 25 90 10 105; - height: 100%; - width: 100%; - padding: 1; - } - - DataTable .datatable--cursor { - background: darkorange; - color: black; - } - - DataTable .datatable--header { - background: steelblue; - color: white; - } - - Input { - border: none; - } - Button { - border: none; - } - - DataTable { - min-height: 24; - } - - .two { - column-span: 2; - } - .three { - column-span: 3; - } - .four { - column-span: 4; - } - .five { - column-span: 5; - } - - #differences-table { - row-span: 10; - } - """ TRACKS_TABLE_INDEX_COLUMN_LABEL = "Index" TRACKS_TABLE_TYPE_COLUMN_LABEL = "Type" @@ -89,9 +41,11 @@ class MediaWorkflowScreenBase(Screen): bootstrap = build_screen_bootstrap(self.app.getContext()) self.context = bootstrap.context - self._applyCleanup = bootstrap.apply_cleanup - self._removeGlobalKeys = bootstrap.remove_global_keys - self._ignoreGlobalKeys = bootstrap.ignore_global_keys + self._applyCleanup = False + self._applyNormalization = bool(self.context.get("apply_metadata_normalization", True)) + self._removeGlobalKeys = [] + self._ignoreGlobalKeys = [] + self._apply_bootstrap_settings(bootstrap) command = self.context.get("command") if command != self.COMMAND_NAME: @@ -121,55 +75,52 @@ class MediaWorkflowScreenBase(Screen): self.reloadProperties(reset_draft=True) + def _apply_bootstrap_settings(self, bootstrap) -> None: + self._applyCleanup = bootstrap.apply_cleanup + self._removeGlobalKeys = bootstrap.remove_global_keys + self._ignoreGlobalKeys = bootstrap.ignore_global_keys + + def refreshCleanupSettings(self) -> None: + self._apply_bootstrap_settings(build_screen_bootstrap(self.context)) + + def setApplyCleanup(self, enabled: bool) -> None: + self.context["apply_metadata_cleanup"] = bool(enabled) + self.refreshCleanupSettings() + + def refreshNormalizationSettings(self) -> None: + self._applyNormalization = bool( + self.context.get("apply_metadata_normalization", True) + ) + + def setApplyNormalization(self, enabled: bool) -> None: + self.context["apply_metadata_normalization"] = bool(enabled) + self.refreshNormalizationSettings() + def _build_media_tags_table(self): - self.mediaTagsTable = DataTable(classes="two") - self.mediaTagsTable.add_column("Key", width=30) - self.mediaTagsTable.add_column("Value", width=70) + self.mediaTagsTable = DataTable(classes="three") + add_auto_table_column(self.mediaTagsTable, t("Key")) + add_auto_table_column(self.mediaTagsTable, t("Value")) self.mediaTagsTable.cursor_type = "row" def _build_tracks_table(self): - self.tracksTable = DataTable(classes="two") - self.tracksTable.add_column( - self.TRACKS_TABLE_INDEX_COLUMN_LABEL, - width=5, - ) - self.tracksTable.add_column( - self.TRACKS_TABLE_TYPE_COLUMN_LABEL, - width=10, - ) - self.tracksTable.add_column( - self.TRACKS_TABLE_SUB_INDEX_COLUMN_LABEL, - width=8, - ) - self.tracksTable.add_column( - self.TRACKS_TABLE_CODEC_COLUMN_LABEL, - width=10, - ) - self.tracksTable.add_column( - self.TRACKS_TABLE_LAYOUT_COLUMN_LABEL, - width=10, - ) - self.tracksTable.add_column( - self.TRACKS_TABLE_LANGUAGE_COLUMN_LABEL, - width=15, - ) - self.tracksTable.add_column( - self.TRACKS_TABLE_TITLE_COLUMN_LABEL, - width=48, - ) - self.tracksTable.add_column( - self.TRACKS_TABLE_DEFAULT_COLUMN_LABEL, - width=8, - ) - self.tracksTable.add_column( - self.TRACKS_TABLE_FORCED_COLUMN_LABEL, - width=8, - ) + self.tracksTable = DataTable(classes="three") + self._configure_tracks_table_columns() self.tracksTable.cursor_type = "row" + def _configure_tracks_table_columns(self): + add_auto_table_column(self.tracksTable, t(self.TRACKS_TABLE_INDEX_COLUMN_LABEL)) + add_auto_table_column(self.tracksTable, t(self.TRACKS_TABLE_TYPE_COLUMN_LABEL)) + add_auto_table_column(self.tracksTable, t(self.TRACKS_TABLE_SUB_INDEX_COLUMN_LABEL)) + add_auto_table_column(self.tracksTable, t(self.TRACKS_TABLE_CODEC_COLUMN_LABEL)) + add_auto_table_column(self.tracksTable, t(self.TRACKS_TABLE_LAYOUT_COLUMN_LABEL)) + add_auto_table_column(self.tracksTable, t(self.TRACKS_TABLE_LANGUAGE_COLUMN_LABEL)) + add_auto_table_column(self.tracksTable, t(self.TRACKS_TABLE_TITLE_COLUMN_LABEL)) + add_auto_table_column(self.tracksTable, t(self.TRACKS_TABLE_DEFAULT_COLUMN_LABEL)) + add_auto_table_column(self.tracksTable, t(self.TRACKS_TABLE_FORCED_COLUMN_LABEL)) + def _build_differences_table(self): self.differencesTable = DataTable(id="differences-table") - self.differencesTable.add_column(self.DIFFERENCES_COLUMN_LABEL, width=100) + add_auto_table_column(self.differencesTable, t(self.DIFFERENCES_COLUMN_LABEL)) self.differencesTable.cursor_type = "row" def reloadProperties(self, reset_draft: bool = True): @@ -228,7 +179,8 @@ class MediaWorkflowScreenBase(Screen): ) def updateTracks(self): - self.tracksTable.clear() + self.tracksTable.clear(columns=True) + self._configure_tracks_table_columns() self._trackRowData = {} trackDescriptorList = self._sourceMediaDescriptor.getTrackDescriptors() @@ -243,17 +195,17 @@ class MediaWorkflowScreenBase(Screen): audioLayout = trackDescriptor.getAudioLayout() row = ( trackDescriptor.getIndex(), - trackType.label(), + t(trackType.label()), typeCounter[trackType], trackDescriptor.getCodec().label(), - audioLayout.label() + t(audioLayout.label()) if trackType == TrackType.AUDIO and audioLayout != AudioLayout.LAYOUT_UNDEFINED else " ", trackDescriptor.getLanguage().label(), trackDescriptor.getTitle(), - "Yes" if TrackDisposition.DEFAULT in dispositionSet else "No", - "Yes" if TrackDisposition.FORCED in dispositionSet else "No", + t("Yes") if TrackDisposition.DEFAULT in dispositionSet else t("No"), + t("Yes") if TrackDisposition.FORCED in dispositionSet else t("No"), ) row_key = self.tracksTable.add_row(*map(str, row)) @@ -285,7 +237,7 @@ class MediaWorkflowScreenBase(Screen): for tagKey, tagValue in tagDifferences.get(DIFF_ADDED_KEY, {}).items(): if tagKey not in self._ignoreGlobalKeys: self.differencesTable.add_row( - f"add media tag: key='{tagKey}' value='{tagValue}'" + t("add media tag: key='{key}' value='{value}'", key=tagKey, value=tagValue) ) for tagKey, tagValue in tagDifferences.get(DIFF_REMOVED_KEY, {}).items(): @@ -294,25 +246,29 @@ class MediaWorkflowScreenBase(Screen): if not self.EDIT_MODE and tagKey in self._removeGlobalKeys: continue self.differencesTable.add_row( - f"remove media tag: key='{tagKey}' value='{tagValue}'" + t("remove media tag: key='{key}' value='{value}'", key=tagKey, value=tagValue) ) for tagKey, tagValue in tagDifferences.get(DIFF_CHANGED_KEY, {}).items(): if tagKey not in self._ignoreGlobalKeys: self.differencesTable.add_row( - f"change media tag: key='{tagKey}' value='{tagValue}'" + t("change media tag: key='{key}' value='{value}'", key=tagKey, value=tagValue) ) trackDifferences = self._mediaChangeSetObj.get(MediaDescriptorChangeSet.TRACKS_KEY, {}) for trackDescriptor in trackDifferences.get(DIFF_ADDED_KEY, {}).values(): self.differencesTable.add_row( - f"add {trackDescriptor.getType().label()} track: " - + f"index={trackDescriptor.getIndex()} lang={trackDescriptor.getLanguage().threeLetter()}" + t( + "add {track_type} track: index={index} lang={language}", + track_type=t(trackDescriptor.getType().label()), + index=trackDescriptor.getIndex(), + language=trackDescriptor.getLanguage().threeLetter(), + ) ) for trackIndex in trackDifferences.get(DIFF_REMOVED_KEY, {}).keys(): - self.differencesTable.add_row(f"remove stream #{trackIndex}") + self.differencesTable.add_row(t("remove stream #{index}", index=trackIndex)) for trackIndex, trackDiffObj in trackDifferences.get(DIFF_CHANGED_KEY, {}).items(): targetTrackDescriptor = targetTrackDescriptorsByIndex.get(trackIndex) @@ -322,35 +278,58 @@ class MediaWorkflowScreenBase(Screen): tagsDiff = trackDiffObj.get(MediaDescriptorChangeSet.TAGS_KEY, {}) for tagKey, tagValue in tagsDiff.get(DIFF_REMOVED_KEY, {}).items(): self.differencesTable.add_row( - f"change stream #{targetTrackDescriptor.getIndex()} " - + f"({targetTrackDescriptor.getType().label()}:{targetTrackDescriptor.getSubIndex()}) " - + f"remove key={tagKey} value={tagValue}" + t( + "change stream #{index} ({track_type}:{sub_index}) remove key={key} value={value}", + index=targetTrackDescriptor.getIndex(), + track_type=t(targetTrackDescriptor.getType().label()), + sub_index=targetTrackDescriptor.getSubIndex(), + key=tagKey, + value=tagValue, + ) ) for tagKey, tagValue in tagsDiff.get(DIFF_ADDED_KEY, {}).items(): self.differencesTable.add_row( - f"change stream #{targetTrackDescriptor.getIndex()} " - + f"({targetTrackDescriptor.getType().label()}:{targetTrackDescriptor.getSubIndex()}) " - + f"add key={tagKey} value={tagValue}" + t( + "change stream #{index} ({track_type}:{sub_index}) add key={key} value={value}", + index=targetTrackDescriptor.getIndex(), + track_type=t(targetTrackDescriptor.getType().label()), + sub_index=targetTrackDescriptor.getSubIndex(), + key=tagKey, + value=tagValue, + ) ) for tagKey, tagValue in tagsDiff.get(DIFF_CHANGED_KEY, {}).items(): self.differencesTable.add_row( - f"change stream #{targetTrackDescriptor.getIndex()} " - + f"({targetTrackDescriptor.getType().label()}:{targetTrackDescriptor.getSubIndex()}) " - + f"change key={tagKey} value={tagValue}" + t( + "change stream #{index} ({track_type}:{sub_index}) change key={key} value={value}", + index=targetTrackDescriptor.getIndex(), + track_type=t(targetTrackDescriptor.getType().label()), + sub_index=targetTrackDescriptor.getSubIndex(), + key=tagKey, + value=tagValue, + ) ) dispositionDiff = trackDiffObj.get(MediaDescriptorChangeSet.DISPOSITION_SET_KEY, {}) for addedDisposition in dispositionDiff.get(DIFF_ADDED_KEY, set()): self.differencesTable.add_row( - f"change stream #{targetTrackDescriptor.getIndex()} " - + f"({targetTrackDescriptor.getType().label()}:{targetTrackDescriptor.getSubIndex()}) " - + f"add disposition={addedDisposition.label()}" + t( + "change stream #{index} ({track_type}:{sub_index}) add disposition={disposition}", + index=targetTrackDescriptor.getIndex(), + track_type=t(targetTrackDescriptor.getType().label()), + sub_index=targetTrackDescriptor.getSubIndex(), + disposition=t(addedDisposition.label()), + ) ) for removedDisposition in dispositionDiff.get(DIFF_REMOVED_KEY, set()): self.differencesTable.add_row( - f"change stream #{targetTrackDescriptor.getIndex()} " - + f"({targetTrackDescriptor.getType().label()}:{targetTrackDescriptor.getSubIndex()}) " - + f"remove disposition={removedDisposition.label()}" + t( + "change stream #{index} ({track_type}:{sub_index}) remove disposition={disposition}", + index=targetTrackDescriptor.getIndex(), + track_type=t(targetTrackDescriptor.getType().label()), + sub_index=targetTrackDescriptor.getSubIndex(), + disposition=t(removedDisposition.label()), + ) ) def getSelectedMediaTag(self): diff --git a/src/ffx/pattern_delete_screen.py b/src/ffx/pattern_delete_screen.py index fde42df..29ce8f5 100644 --- a/src/ffx/pattern_delete_screen.py +++ b/src/ffx/pattern_delete_screen.py @@ -4,8 +4,10 @@ from textual.screen import Screen from textual.widgets import Header, Footer, Static, Button from textual.containers import Grid +from .i18n import t from .show_controller import ShowController from .pattern_controller import PatternController +from .screen_support import go_back_or_exit from ffx.model.pattern import Pattern @@ -13,15 +15,22 @@ from ffx.model.pattern import Pattern # Screen[dict[int, str, int]] class PatternDeleteScreen(Screen): + BINDINGS = [ + ("escape", "back", t("Back")), + ] + CSS = """ Grid { grid-size: 2; grid-rows: 2 auto; - grid-columns: 30 330; + grid-columns: 18 5fr; height: 100%; width: 100%; + min-width: 90; padding: 1; + overflow-x: auto; + overflow-y: auto; } Input { @@ -70,23 +79,29 @@ class PatternDeleteScreen(Screen): yield Header() with Grid(): + # Row 1 + yield Static(t("Are you sure to delete the following filename pattern?"), id="toplabel", classes="two") - yield Static("Are you sure to delete the following filename pattern?", id="toplabel", classes="two") - + # Row 2 yield Static("", classes="two") - yield Static("Pattern") + # Row 3 + yield Static(t("Pattern")) yield Static("", id="patternlabel") + # Row 4 yield Static("", classes="two") - yield Static("from show") + # Row 5 + yield Static(t("from show")) yield Static("", id="showlabel") + # Row 6 yield Static("", classes="two") - yield Button("Delete", id="delete_button") - yield Button("Cancel", id="cancel_button") + # Row 7 + yield Button(t("Delete"), id="delete_button") + yield Button(t("Cancel"), id="cancel_button") yield Footer() @@ -109,3 +124,5 @@ class PatternDeleteScreen(Screen): if event.button.id == "cancel_button": self.app.pop_screen() + def action_back(self): + go_back_or_exit(self) diff --git a/src/ffx/pattern_details_screen.py b/src/ffx/pattern_details_screen.py index 1ea1b11..74a0945 100644 --- a/src/ffx/pattern_details_screen.py +++ b/src/ffx/pattern_details_screen.py @@ -15,8 +15,10 @@ from .shifted_season_details_screen import ShiftedSeasonDetailsScreen from .tag_details_screen import TagDetailsScreen from .tag_delete_screen import TagDeleteScreen from .screen_support import ( + add_auto_table_column, build_screen_bootstrap, build_screen_controllers, + go_back_or_exit, populate_tag_table, ) @@ -31,20 +33,28 @@ from ffx.file_properties import FileProperties from ffx.iso_language import IsoLanguage from ffx.audio_layout import AudioLayout from ffx.model.shifted_season import ShiftedSeason +from .i18n import t # Screen[dict[int, str, int]] class PatternDetailsScreen(Screen): + BINDINGS = [ + ("escape", "back", t("Back")), + ] + CSS = """ Grid { grid-size: 7 20; grid-rows: 2 2 2 2 2 2 6 2 2 8 2 2 8 2 2 8 2 2 2 2; - grid-columns: 25 25 25 25 25 25 25; + grid-columns: 18 1fr 1fr 1fr 1fr 1fr 1fr; height: 100%; width: 100%; + min-width: 140; padding: 1; + overflow-x: auto; + overflow-y: auto; } Input { @@ -56,6 +66,7 @@ class PatternDetailsScreen(Screen): DataTable { min-height: 6; + width: 100%; } DataTable .datatable--cursor { @@ -160,15 +171,15 @@ class PatternDetailsScreen(Screen): audioLayout = td.getAudioLayout() row = (td.getIndex(), - trackType.label(), + t(trackType.label()), typeCounter[trackType], td.getCodec().label(), - audioLayout.label() if trackType == TrackType.AUDIO + t(audioLayout.label()) if trackType == TrackType.AUDIO and audioLayout != AudioLayout.LAYOUT_UNDEFINED else ' ', trackLanguage.label() if trackLanguage != IsoLanguage.UNDEFINED else ' ', td.getTitle(), - 'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No', - 'Yes' if TrackDisposition.FORCED in dispoSet else 'No', + t('Yes') if TrackDisposition.DEFAULT in dispoSet else t('No'), + t('Yes') if TrackDisposition.FORCED in dispoSet else t('No'), td.getSourceIndex()) row_key = self.tracksTable.add_row(*map(str, row)) @@ -336,34 +347,34 @@ class PatternDetailsScreen(Screen): self.tagsTable = DataTable(classes="seven") # Define the columns with headers - self.column_key_tag_key = self.tagsTable.add_column("Key", width=50) - self.column_key_tag_value = self.tagsTable.add_column("Value", width=100) + self.column_key_tag_key = add_auto_table_column(self.tagsTable, t("Key")) + self.column_key_tag_value = add_auto_table_column(self.tagsTable, t("Value")) self.tagsTable.cursor_type = 'row' self.tracksTable = DataTable(id="tracks_table", classes="seven") - self.column_key_track_index = self.tracksTable.add_column("Index", width=5) - self.column_key_track_type = self.tracksTable.add_column("Type", width=10) - self.column_key_track_sub_index = self.tracksTable.add_column("SubIndex", width=8) - self.column_key_track_codec = self.tracksTable.add_column("Codec", width=10) - self.column_key_track_audio_layout = self.tracksTable.add_column("Layout", width=10) - self.column_key_track_language = self.tracksTable.add_column("Language", width=15) - self.column_key_track_title = self.tracksTable.add_column("Title", width=48) - self.column_key_track_default = self.tracksTable.add_column("Default", width=8) - self.column_key_track_forced = self.tracksTable.add_column("Forced", width=8) - self.column_key_track_source_index = self.tracksTable.add_column("SrcIndex", width=8) + self.column_key_track_index = add_auto_table_column(self.tracksTable, t("Index")) + self.column_key_track_type = add_auto_table_column(self.tracksTable, t("Type")) + self.column_key_track_sub_index = add_auto_table_column(self.tracksTable, t("SubIndex")) + self.column_key_track_codec = add_auto_table_column(self.tracksTable, t("Codec")) + self.column_key_track_audio_layout = add_auto_table_column(self.tracksTable, t("Layout")) + self.column_key_track_language = add_auto_table_column(self.tracksTable, t("Language")) + self.column_key_track_title = add_auto_table_column(self.tracksTable, t("Title")) + self.column_key_track_default = add_auto_table_column(self.tracksTable, t("Default")) + self.column_key_track_forced = add_auto_table_column(self.tracksTable, t("Forced")) + self.column_key_track_source_index = add_auto_table_column(self.tracksTable, t("SrcIndex")) self.tracksTable.cursor_type = 'row' self.shiftedSeasonsTable = DataTable(classes="seven") - self.column_key_original_season = self.shiftedSeasonsTable.add_column("Source Season", width=18) - self.column_key_first_episode = self.shiftedSeasonsTable.add_column("First Episode", width=18) - self.column_key_last_episode = self.shiftedSeasonsTable.add_column("Last Episode", width=18) - self.column_key_season_offset = self.shiftedSeasonsTable.add_column("Season Offset", width=18) - self.column_key_episode_offset = self.shiftedSeasonsTable.add_column("Episode Offset", width=18) + self.column_key_original_season = add_auto_table_column(self.shiftedSeasonsTable, t("Source Season")) + self.column_key_first_episode = add_auto_table_column(self.shiftedSeasonsTable, t("First Episode")) + self.column_key_last_episode = add_auto_table_column(self.shiftedSeasonsTable, t("Last Episode")) + self.column_key_season_offset = add_auto_table_column(self.shiftedSeasonsTable, t("Season Offset")) + self.column_key_episode_offset = add_auto_table_column(self.shiftedSeasonsTable, t("Episode Offset")) self.shiftedSeasonsTable.cursor_type = 'row' @@ -372,103 +383,103 @@ class PatternDetailsScreen(Screen): with Grid(): - # 1 - yield Static("Edit filename pattern" if self.__pattern is not None else "New filename pattern", id="toplabel") + # Row 1 + yield Static(t("Edit filename pattern") if self.__pattern is not None else t("New filename pattern"), id="toplabel") yield Input(type="text", id="pattern_input", classes="six") - # 2 - yield Static("from show") + # Row 2 + yield Static(t("from show")) yield Static("", id="showlabel", classes="five") - yield Button("Substitute pattern", id="pattern_button") + yield Button(t("Substitute pattern"), id="pattern_button") - # 3 + # Row 3 yield Static(" ", classes="seven") - # 4 - yield Static("Quality") + # Row 4 + yield Static(t("Quality")) yield Input(type="integer", id="quality_input") yield Static(' ', classes="five") - # 5 + # Row 5 yield Static(" ", classes="seven") - # 6 - yield Static("Notes") + # Row 6 + yield Static(t("Notes")) yield Static(" ", classes="six") - # 7 + # Row 7 yield TextArea(id="notes_textarea", classes="four_box seven") - # 8 + # Row 8 yield Static(" ", classes="seven") - # 9 - yield Static("Shifted Seasons") + # Row 9 + yield Static(t("Numbering Mapping")) if self.__pattern is not None: - yield Button("Add", id="button_add_shifted_season") - yield Button("Edit", id="button_edit_shifted_season") - yield Button("Delete", id="button_delete_shifted_season") + yield Button(t("Add"), id="button_add_shifted_season") + yield Button(t("Edit"), id="button_edit_shifted_season") + yield Button(t("Delete"), id="button_delete_shifted_season") else: yield Static(" ") yield Static(" ") yield Static(" ") - yield Static(" ") - yield Static(" ") - yield Static(" ") + yield Static(" ") + yield Static(" ") + yield Static(" ") - # 10 + # Row 10 yield self.shiftedSeasonsTable - # 11 + # Row 11 yield Static(" ", classes="seven") - # 12 - yield Static("Media Tags") - yield Button("Add", id="button_add_tag") - yield Button("Edit", id="button_edit_tag") - yield Button("Delete", id="button_delete_tag") + # Row 12 + yield Static(t("Media Tags")) + yield Button(t("Add"), id="button_add_tag") + yield Button(t("Edit"), id="button_edit_tag") + yield Button(t("Delete"), id="button_delete_tag") yield Static(" ") yield Static(" ") yield Static(" ") - # 13 + # Row 13 yield self.tagsTable - # 14 + # Row 14 yield Static(" ", classes="seven") - # 15 - yield Static("Streams") - yield Button("Add", id="button_add_track") - yield Button("Edit", id="button_edit_track") - yield Button("Delete", id="button_delete_track") + # Row 15 + yield Static(t("Streams")) + yield Button(t("Add"), id="button_add_track") + yield Button(t("Edit"), id="button_edit_track") + yield Button(t("Delete"), id="button_delete_track") yield Static(" ") - yield Button("Up", id="button_track_up") - yield Button("Down", id="button_track_down") + yield Button(t("Up"), id="button_track_up") + yield Button(t("Down"), id="button_track_down") - # 16 + # Row 16 yield self.tracksTable - # 17 + # Row 17 yield Static(" ", classes="seven") - # 18 + # Row 18 yield Static(" ", classes="seven") - # 19 - yield Button("Save", id="save_button") - yield Button("Cancel", id="cancel_button") + # Row 19 + yield Button(t("Save"), id="save_button") + yield Button(t("Cancel"), id="cancel_button") yield Static(" ", classes="five") - # 20 + # Row 20 yield Static(" ", classes="seven") yield Footer() @@ -758,5 +769,8 @@ class PatternDetailsScreen(Screen): def handle_update_shifted_season(self, screenResult): self.updateShiftedSeasons() + def action_back(self): + go_back_or_exit(self) + def handle_delete_shifted_season(self, screenResult): self.updateShiftedSeasons() diff --git a/src/ffx/screen_support.py b/src/ffx/screen_support.py index 2cb4c96..bfe565b 100644 --- a/src/ffx/screen_support.py +++ b/src/ffx/screen_support.py @@ -3,6 +3,10 @@ from __future__ import annotations from collections.abc import Mapping from dataclasses import dataclass +from rich.cells import cell_len +from rich.measure import measure_renderables +from rich.text import Text + from .helper import formatRichColor from .pattern_controller import PatternController from .show_controller import ShowController @@ -99,3 +103,52 @@ def populate_tag_table( row_data[row_key] = (raw_key, raw_value) return row_data + + +def localized_column_width(label: str, minimum: int, *, padding: int = 2) -> int: + """Ensure translated table headers fit within their visible column width.""" + + text = str(label) + return max( + int(minimum), + len(text) + int(padding), + int(cell_len(text)) + int(padding), + ) + + +def add_auto_table_column(table, label, *, key=None, default=None): + """Add a DataTable column that sizes itself from header and cell content.""" + + return table.add_column(label, key=key, default=default) + + +def update_table_column_label(table, column_key, label) -> None: + """Update a column label and keep auto-width columns in sync with it.""" + + column = table.columns.get(column_key) + if column is None: + return + + text_label = Text.from_markup(label) if isinstance(label, str) else label + column.label = text_label + + if column.auto_width: + measured = measure_renderables( + table.app.console, + table.app.console.options, + [text_label], + ).maximum + column.content_width = max(column.content_width, measured) + + table.refresh() + + +def go_back_or_exit(screen) -> None: + """Pop the current screen when possible, otherwise exit the app.""" + + screen_stack = getattr(screen.app, "screen_stack", ()) + if len(screen_stack) > 2: + screen.app.pop_screen() + return + + screen.app.exit() diff --git a/src/ffx/settings_screen.py b/src/ffx/settings_screen.py index a7dbd25..4ea5eb5 100644 --- a/src/ffx/settings_screen.py +++ b/src/ffx/settings_screen.py @@ -2,11 +2,22 @@ from textual.app import ComposeResult from textual.screen import Screen from textual.widgets import Footer, Placeholder +from .i18n import t +from .screen_support import go_back_or_exit + class SettingsScreen(Screen): + BINDINGS = [ + ("escape", "back", t("Back")), + ] + def __init__(self): super().__init__() context = self.app.getContext() def compose(self) -> ComposeResult: - yield Placeholder("Settings Screen") + # Row 1 + yield Placeholder(t("Settings Screen")) yield Footer() + + def action_back(self): + go_back_or_exit(self) diff --git a/src/ffx/shifted_season_delete_screen.py b/src/ffx/shifted_season_delete_screen.py index 2f3a810..21f2ede 100644 --- a/src/ffx/shifted_season_delete_screen.py +++ b/src/ffx/shifted_season_delete_screen.py @@ -4,7 +4,9 @@ from textual.screen import Screen from textual.widgets import Header, Footer, Static, Button from textual.containers import Grid +from .i18n import t from .shifted_season_controller import ShiftedSeasonController +from .screen_support import go_back_or_exit from ffx.model.shifted_season import ShiftedSeason @@ -12,15 +14,22 @@ from ffx.model.shifted_season import ShiftedSeason # Screen[dict[int, str, int]] class ShiftedSeasonDeleteScreen(Screen): + BINDINGS = [ + ("escape", "back", t("Back")), + ] + CSS = """ Grid { grid-size: 2; grid-rows: 2 auto; - grid-columns: 30 330; + grid-columns: 18 5fr; height: 100%; width: 100%; + min-width: 90; padding: 1; + overflow-x: auto; + overflow-y: auto; } Input { @@ -61,9 +70,9 @@ class ShiftedSeasonDeleteScreen(Screen): shiftedSeason: ShiftedSeason = self.__ssc.getShiftedSeason(self.__shiftedSeasonId) ownerLabel = ( - f"pattern #{self._patternId}" + t("pattern #{id}", id=self._patternId) if self._patternId is not None - else f"show #{self._showId}" + else t("show #{id}", id=self._showId) ) self.query_one("#static_owner", Static).update(ownerLabel) self.query_one("#static_original_season", Static).update(str(shiftedSeason.getOriginalSeason())) @@ -78,35 +87,45 @@ class ShiftedSeasonDeleteScreen(Screen): yield Header() with Grid(): + # Row 1 + yield Static(t("Are you sure to delete the following shifted season?"), id="toplabel", classes="two") - yield Static("Are you sure to delete the following shifted season?", id="toplabel", classes="two") - + # Row 2 yield Static(" ", classes="two") - yield Static("from") + # Row 3 + yield Static(t("from")) yield Static(" ", id="static_owner") + # Row 4 yield Static(" ", classes="two") - yield Static("Source season") + # Row 5 + yield Static(t("Source Season")) yield Static(" ", id="static_original_season") - yield Static("First episode") + # Row 6 + yield Static(t("First episode")) yield Static(" ", id="static_first_episode") - yield Static("Last episode") + # Row 7 + yield Static(t("Last episode")) yield Static(" ", id="static_last_episode") - yield Static("Season offset") + # Row 8 + yield Static(t("Season Offset")) yield Static(" ", id="static_season_offset") - yield Static("Episode offset") + # Row 9 + yield Static(t("Episode offset")) yield Static(" ", id="static_episode_offset") + # Row 10 yield Static(" ", classes="two") - yield Button("Delete", id="delete_button") - yield Button("Cancel", id="cancel_button") + # Row 11 + yield Button(t("Delete"), id="delete_button") + yield Button(t("Cancel"), id="cancel_button") yield Footer() @@ -128,3 +147,6 @@ class ShiftedSeasonDeleteScreen(Screen): if event.button.id == "cancel_button": self.app.pop_screen() + + def action_back(self): + go_back_or_exit(self) diff --git a/src/ffx/shifted_season_details_screen.py b/src/ffx/shifted_season_details_screen.py index 1c3a6d9..3384142 100644 --- a/src/ffx/shifted_season_details_screen.py +++ b/src/ffx/shifted_season_details_screen.py @@ -4,7 +4,9 @@ from textual.screen import Screen from textual.widgets import Header, Footer, Static, Button, Input from textual.containers import Grid +from .i18n import t from .shifted_season_controller import ShiftedSeasonController +from .screen_support import go_back_or_exit from ffx.model.shifted_season import ShiftedSeason @@ -12,15 +14,22 @@ from ffx.model.shifted_season import ShiftedSeason # Screen[dict[int, str, int]] class ShiftedSeasonDetailsScreen(Screen): + BINDINGS = [ + ("escape", "back", t("Back")), + ] + CSS = """ Grid { grid-size: 3 10; grid-rows: 2 2 2 2 2 2 2 2 2 2; - grid-columns: 40 40 40; + grid-columns: 20 1fr 1fr; height: 100%; width: 100%; + min-width: 80; padding: 1; + overflow-x: auto; + overflow-y: auto; } Input { @@ -125,41 +134,45 @@ class ShiftedSeasonDetailsScreen(Screen): with Grid(): - # 1 - yield Static("Edit shifted season" if self.__shiftedSeasonId is not None else "New shifted season", id="toplabel", classes="three") + # Row 1 + yield Static( + t("Edit shifted season") if self.__shiftedSeasonId is not None else t("New shifted season"), + id="toplabel", + classes="three", + ) - # 2 + # Row 2 yield Static(" ", classes="three") - # 3 - yield Static("Source season") + # Row 3 + yield Static(t("Source Season")) yield Input(id="input_original_season", classes="two") - # 4 - yield Static("First Episode") + # Row 4 + yield Static(t("First Episode")) yield Input(id="input_first_episode", classes="two") - # 5 - yield Static("Last Episode") + # Row 5 + yield Static(t("Last Episode")) yield Input(id="input_last_episode", classes="two") - # 6 - yield Static("Season offset") + # Row 6 + yield Static(t("Season Offset")) yield Input(id="input_season_offset", classes="two") - # 7 - yield Static("Episode offset") + # Row 7 + yield Static(t("Episode offset")) yield Input(id="input_episode_offset", classes="two") - # 8 + # Row 8 yield Static(" ", classes="three") - # 9 - yield Button("Save", id="save_button") - yield Button("Cancel", id="cancel_button") + # Row 9 + yield Button(t("Save"), id="save_button") + yield Button(t("Cancel"), id="cancel_button") yield Static(" ") - # 10 + # Row 10 yield Static(" ", classes="three") yield Footer() @@ -196,6 +209,9 @@ class ShiftedSeasonDetailsScreen(Screen): return shiftedSeasonObj + def action_back(self): + go_back_or_exit(self) + # Event handler for button press def on_button_pressed(self, event: Button.Pressed) -> None: diff --git a/src/ffx/show_delete_screen.py b/src/ffx/show_delete_screen.py index c48b240..4622a50 100644 --- a/src/ffx/show_delete_screen.py +++ b/src/ffx/show_delete_screen.py @@ -2,20 +2,29 @@ from textual.screen import Screen from textual.widgets import Header, Footer, Static, Button from textual.containers import Grid +from .i18n import t from .show_controller import ShowController +from .screen_support import go_back_or_exit # Screen[dict[int, str, int]] class ShowDeleteScreen(Screen): + BINDINGS = [ + ("escape", "back", t("Back")), + ] + CSS = """ Grid { grid-size: 2; grid-rows: 2 auto; - grid-columns: 30 auto; + grid-columns: 18 4fr; height: 100%; width: 100%; + min-width: 80; padding: 1; + overflow-x: auto; + overflow-y: auto; } Input { @@ -59,20 +68,25 @@ class ShowDeleteScreen(Screen): yield Header() with Grid(): + # Row 1 + yield Static(t("Are you sure to delete the following show?"), id="toplabel", classes="two") - yield Static("Are you sure to delete the following show?", id="toplabel", classes="two") - + # Row 2 yield Static("", classes="two") + # Row 3 yield Static("", id="showlabel") yield Static("") + # Row 4 yield Static("", classes="two") + # Row 5 yield Static("", classes="two") - yield Button("Delete", id="delete_button") - yield Button("Cancel", id="cancel_button") + # Row 6 + yield Button(t("Delete"), id="delete_button") + yield Button(t("Cancel"), id="cancel_button") yield Footer() @@ -93,3 +107,6 @@ class ShowDeleteScreen(Screen): if event.button.id == "cancel_button": self.app.pop_screen() + + def action_back(self): + go_back_or_exit(self) diff --git a/src/ffx/show_details_screen.py b/src/ffx/show_details_screen.py index d174deb..983aaad 100644 --- a/src/ffx/show_details_screen.py +++ b/src/ffx/show_details_screen.py @@ -16,7 +16,13 @@ from .shifted_season_delete_screen import ShiftedSeasonDeleteScreen from ffx.model.shifted_season import ShiftedSeason from .helper import filterFilename -from .screen_support import build_screen_bootstrap, build_screen_controllers +from .i18n import t +from .screen_support import ( + add_auto_table_column, + build_screen_bootstrap, + build_screen_controllers, + go_back_or_exit, +) # Screen[dict[int, str, int]] @@ -25,12 +31,15 @@ class ShowDetailsScreen(Screen): CSS = """ Grid { - grid-size: 5 18; - grid-rows: 2 2 2 2 2 2 6 2 2 2 2 2 2 9 2 9 2 2; - grid-columns: 30 30 30 30 30; + grid-size: 5 19; + grid-rows: 2 2 2 2 2 2 6 2 2 2 2 2 2 2 9 2 9 2 2; + grid-columns: 25 20 20 20 1fr; height: 100%; width: 100%; + min-width: 110; padding: 1; + overflow-x: auto; + overflow-y: auto; } Input { @@ -43,6 +52,7 @@ class ShowDetailsScreen(Screen): DataTable { column-span: 2; min-height: 8; + width: 100%; } DataTable .datatable--cursor { @@ -84,9 +94,10 @@ class ShowDetailsScreen(Screen): """ BINDINGS = [ - ("a", "add_pattern", "Add Pattern"), - ("e", "edit_pattern", "Edit Pattern"), - ("r", "remove_pattern", "Remove Pattern"), + ("escape", "back", t("Back")), + ("a", "add_pattern", t("Add Pattern")), + ("e", "edit_pattern", t("Edit Pattern")), + ("r", "remove_pattern", t("Remove Pattern")), ] def __init__(self, showId = None): @@ -318,18 +329,18 @@ class ShowDetailsScreen(Screen): self.patternTable = DataTable(classes="five") # Define the columns with headers - self.column_key_pattern = self.patternTable.add_column("Pattern", width=150) + self.column_key_pattern = add_auto_table_column(self.patternTable, t("Pattern")) self.patternTable.cursor_type = 'row' self.shiftedSeasonsTable = DataTable(classes="five") - self.column_key_original_season = self.shiftedSeasonsTable.add_column("Source Season", width=30) - self.column_key_first_episode = self.shiftedSeasonsTable.add_column("First Episode", width=30) - self.column_key_last_episode = self.shiftedSeasonsTable.add_column("Last Episode", width=30) - self.column_key_season_offset = self.shiftedSeasonsTable.add_column("Season Offset", width=30) - self.column_key_episode_offset = self.shiftedSeasonsTable.add_column("Episode Offset", width=30) + self.column_key_original_season = add_auto_table_column(self.shiftedSeasonsTable, t("Source Season")) + self.column_key_first_episode = add_auto_table_column(self.shiftedSeasonsTable, t("First Episode")) + self.column_key_last_episode = add_auto_table_column(self.shiftedSeasonsTable, t("Last Episode")) + self.column_key_season_offset = add_auto_table_column(self.shiftedSeasonsTable, t("Season Offset")) + self.column_key_episode_offset = add_auto_table_column(self.shiftedSeasonsTable, t("Episode Offset")) self.shiftedSeasonsTable.cursor_type = 'row' @@ -338,82 +349,88 @@ class ShowDetailsScreen(Screen): with Grid(): - # 1 - yield Static("Show" if not self.__showDescriptor is None else "New Show", id="toplabel") - yield Button("Identify", id="identify_button") + # Row 1 + yield Static(t("Show") if not self.__showDescriptor is None else t("New Show"), id="toplabel") + yield Button(t("Identify"), id="identify_button") yield Static(" ", classes="three") - # 2 - yield Static("ID") + # Row 2 + yield Static(t("ID")) if not self.__showDescriptor is None: yield Static("", id="id_static", classes="four") else: yield Input(type="integer", id="id_input", classes="four") - # 3 - yield Static("Name") + # Row 3 + yield Static(t("Name")) yield Input(type="text", id="name_input", classes="four") - # 4 - yield Static("Year") + # Row 4 + yield Static(t("Year")) yield Input(type="integer", id="year_input", classes="four") - #5 - yield Static("Quality") + # Row 5 + yield Static(t("Quality")) yield Input(type="integer", id="quality_input", classes="four") - #6 - yield Static("Notes") + # Row 6 + yield Static(t("Notes")) yield Static(" ", classes="four") - #7 + # Row 7 yield TextArea(id="notes_textarea", classes="five note_box") - #8 - yield Static("Index Season Digits") - yield Input(type="integer", id="index_season_digits_input", classes="four") - - #9 - yield Static("Index Episode Digits") - yield Input(type="integer", id="index_episode_digits_input", classes="four") - - #10 - yield Static("Indicator Season Digits") - yield Input(type="integer", id="indicator_season_digits_input", classes="four") - - #11 - yield Static("Indicator Edisode Digits") - yield Input(type="integer", id="indicator_episode_digits_input", classes="four") - - # 12 + # Row 8 yield Static(" ", classes="five") - # 13 - yield Static("Shifted seasons", classes="two") + # Row 9 + yield Static(t("Index Season Digits")) + yield Input(type="integer", id="index_season_digits_input", classes="four") + + # Row 10 + yield Static(t("Index Episode Digits")) + yield Input(type="integer", id="index_episode_digits_input", classes="four") + + # Row 11 + yield Static(t("Indicator Season Digits")) + yield Input(type="integer", id="indicator_season_digits_input", classes="four") + + # Row 12 + yield Static(t("Indicator Edisode Digits")) + yield Input(type="integer", id="indicator_episode_digits_input", classes="four") + + # Row 13 + yield Static(" ", classes="five") + + # Row 14 + yield Static(t("Numbering Mapping")) if self.__showDescriptor is not None: - yield Button("Add", id="button_add_shifted_season") - yield Button("Edit", id="button_edit_shifted_season") - yield Button("Delete", id="button_delete_shifted_season") + yield Button(t("Add"), id="button_add_shifted_season") + yield Button(t("Edit"), id="button_edit_shifted_season") + yield Button(t("Delete"), id="button_delete_shifted_season") else: yield Static(" ") yield Static(" ") yield Static(" ") - # 14 + yield Static(" ") + + # Row 15 yield self.shiftedSeasonsTable - # 15 - yield Static("File patterns", classes="five") - # 16 + # Row 16 + yield Static(t("File patterns"), classes="five") + + # Row 17 yield self.patternTable - # 17 + # Row 18 yield Static(" ", classes="five") - # 18 - yield Button("Save", id="save_button") - yield Button("Cancel", id="cancel_button") + # Row 19 + yield Button(t("Save"), id="save_button") + yield Button(t("Cancel"), id="cancel_button") yield Footer() @@ -514,3 +531,6 @@ class ShowDetailsScreen(Screen): def handle_delete_shifted_season(self, screenResult): self.updateShiftedSeasons() + + def action_back(self): + go_back_or_exit(self) diff --git a/src/ffx/shows_screen.py b/src/ffx/shows_screen.py index 0853d94..66aa54d 100644 --- a/src/ffx/shows_screen.py +++ b/src/ffx/shows_screen.py @@ -1,8 +1,11 @@ from textual.screen import Screen from textual.widgets import Header, Footer, Static, DataTable from textual.containers import Grid +from rich.text import Text +from .i18n import t from .show_controller import ShowController +from .screen_support import add_auto_table_column, go_back_or_exit, update_table_column_label from .show_details_screen import ShowDetailsScreen from .show_delete_screen import ShowDeleteScreen @@ -21,7 +24,10 @@ class ShowsScreen(Screen): grid-rows: 2 auto; height: 100%; width: 100%; + min-width: 80; padding: 1; + overflow-x: auto; + overflow-y: auto; } DataTable .datatable--cursor { @@ -49,12 +55,17 @@ class ShowsScreen(Screen): height: 100%; border: solid green; } + + DataTable { + width: 100%; + } """ BINDINGS = [ - ("e", "edit_show", "Edit Show"), - ("n", "new_show", "New Show"), - ("d", "delete_show", "Delete Show"), + ("escape", "back", t("Back")), + ("e", "edit_show", t("Edit Show")), + ("n", "new_show", t("New Show")), + ("d", "delete_show", t("Delete Show")), ] @@ -67,6 +78,9 @@ class ShowsScreen(Screen): self.__sc = ShowController(context = self.context) self.__showRowData: dict[object, ShowDescriptor] = {} + self.__sortColumnKey = None + self.__sortReverse = False + self.__columnLabels: dict[object, str] = {} def _add_show_row(self, show_descriptor: ShowDescriptor): @@ -78,6 +92,64 @@ class ShowsScreen(Screen): self.__showRowData[row_key] = show_descriptor return row_key + def _get_selected_row_key(self): + try: + row_key, _ = self.table.coordinate_to_cell_key(self.table.cursor_coordinate) + return row_key + except CellDoesNotExist: + return None + + def _move_cursor_to_row_key(self, row_key): + if row_key is None: + return + + try: + row_index = int(self.table.get_row_index(row_key)) + except Exception: + return + + self.table.move_cursor(row=row_index) + + def _sort_key_for_column(self, column_key): + if column_key == self.column_key_id: + return lambda value: int(value) + if column_key == self.column_key_year: + return lambda value: int(value) + if column_key == self.column_key_name: + return lambda value: str(value).casefold() + return None + + def _update_header_labels(self): + if not hasattr(self, "table"): + return + + arrow_up = "▴" + arrow_down = "▾" + + for column_key, base_label in self.__columnLabels.items(): + column = self.table.columns.get(column_key) + if column is None: + continue + + label_text = base_label + if column_key == self.__sortColumnKey: + label_text = f"{base_label} {arrow_down if self.__sortReverse else arrow_up}" + + update_table_column_label(self.table, column_key, Text(label_text)) + + def _apply_sort(self, *, preserve_row_key=None): + if self.__sortColumnKey is None: + self._update_header_labels() + return + + self.table.sort( + self.__sortColumnKey, + key=self._sort_key_for_column(self.__sortColumnKey), + reverse=self.__sortReverse, + ) + self._move_cursor_to_row_key(preserve_row_key) + self._update_header_labels() + def getSelectedShowId(self): @@ -93,6 +165,23 @@ class ShowsScreen(Screen): except CellDoesNotExist: return None + def action_back(self): + go_back_or_exit(self) + + def on_data_table_header_selected(self, event: DataTable.HeaderSelected) -> None: + if event.data_table is not self.table: + return + + selected_row_key = self._get_selected_row_key() + + if self.__sortColumnKey == event.column_key: + self.__sortReverse = not self.__sortReverse + else: + self.__sortColumnKey = event.column_key + self.__sortReverse = False + + self._apply_sort(preserve_row_key=selected_row_key) + @@ -101,7 +190,8 @@ class ShowsScreen(Screen): def handle_new_screen(self, screenResult): if isinstance(screenResult, ShowDescriptor): - self._add_show_row(screenResult) + row_key = self._add_show_row(screenResult) + self._apply_sort(preserve_row_key=row_key) def action_edit_show(self): @@ -121,6 +211,7 @@ class ShowsScreen(Screen): self.table.update_cell(row_key, self.column_key_name, showDescriptor.getName()) self.table.update_cell(row_key, self.column_key_year, showDescriptor.getYear()) self.__showRowData[row_key] = showDescriptor + self._apply_sort(preserve_row_key=row_key) except CellDoesNotExist: pass @@ -151,6 +242,9 @@ class ShowsScreen(Screen): for show in self.__sc.getAllShows(): self._add_show_row(show.getDescriptor(self.context)) + self.__sortColumnKey = self.column_key_name + self._apply_sort() + def compose(self): @@ -158,18 +252,27 @@ class ShowsScreen(Screen): self.table = DataTable() # Define the columns with headers - self.column_key_id = self.table.add_column("ID", width=10) - self.column_key_name = self.table.add_column("Name", width=50) - self.column_key_year = self.table.add_column("Year", width=10) + idLabel = t("ID") + nameLabel = t("Name") + yearLabel = t("Year") + self.column_key_id = add_auto_table_column(self.table, idLabel) + self.column_key_name = add_auto_table_column(self.table, nameLabel) + self.column_key_year = add_auto_table_column(self.table, yearLabel) + self.__columnLabels = { + self.column_key_id: idLabel, + self.column_key_name: nameLabel, + self.column_key_year: yearLabel, + } self.table.cursor_type = 'row' yield Header() with Grid(): + # Row 1 + yield Static(t("Shows"), markup=False) - yield Static("Shows") - + # Row 2 yield self.table f = Footer() diff --git a/src/ffx/tag_delete_screen.py b/src/ffx/tag_delete_screen.py index 1956976..d0fc477 100644 --- a/src/ffx/tag_delete_screen.py +++ b/src/ffx/tag_delete_screen.py @@ -2,19 +2,29 @@ from textual.screen import Screen from textual.widgets import Header, Footer, Static, Button from textual.containers import Grid +from .i18n import t +from .screen_support import go_back_or_exit + # Screen[dict[int, str, int]] class TagDeleteScreen(Screen): + BINDINGS = [ + ("escape", "back", t("Back")), + ] + CSS = """ Grid { grid-size: 4 9; grid-rows: 2 2 2 2 2 2 2 2 2; - grid-columns: 30 30 30 30; + grid-columns: 18 1fr 1fr 1fr; height: 100%; width: 100%; + min-width: 90; padding: 1; + overflow-x: auto; + overflow-y: auto; } Input { @@ -64,23 +74,23 @@ class TagDeleteScreen(Screen): with Grid(): - #1 - yield Static(f"Are you sure to delete this tag ?", id="toplabel", classes="five") + # Row 1 + yield Static(t("Are you sure to delete this tag?"), id="toplabel", classes="five") - #2 - yield Static("Key") + # Row 2 + yield Static(t("Key")) yield Static(" ", id="keylabel", classes="four") - #3 - yield Static("Value") + # Row 3 + yield Static(t("Value")) yield Static(" ", id="valuelabel", classes="four") - #4 + # Row 4 yield Static(" ", classes="five") - #9 - yield Button("Delete", id="delete_button") - yield Button("Cancel", id="cancel_button") + # Row 5 + yield Button(t("Delete"), id="delete_button") + yield Button(t("Cancel"), id="cancel_button") yield Footer() @@ -96,3 +106,5 @@ class TagDeleteScreen(Screen): if event.button.id == "cancel_button": self.app.pop_screen() + def action_back(self): + go_back_or_exit(self) diff --git a/src/ffx/tag_details_screen.py b/src/ffx/tag_details_screen.py index 673735d..7feb3fc 100644 --- a/src/ffx/tag_details_screen.py +++ b/src/ffx/tag_details_screen.py @@ -2,19 +2,29 @@ from textual.screen import Screen from textual.widgets import Header, Footer, Static, Button, Input from textual.containers import Grid +from .i18n import t +from .screen_support import go_back_or_exit + # Screen[dict[int, str, int]] class TagDetailsScreen(Screen): + BINDINGS = [ + ("escape", "back", t("Back")), + ] + CSS = """ Grid { grid-size: 5 20; grid-rows: 2 2 2 2 2 3 2 2 2 2 2 6 2 2 6 2 2 2 2 6; - grid-columns: 25 25 25 25 225; + grid-columns: 18 1fr 1fr 1fr 5fr; height: 100%; width: 100%; + min-width: 100; padding: 1; + overflow-x: auto; + overflow-y: auto; } Input { @@ -90,24 +100,25 @@ class TagDetailsScreen(Screen): with Grid(): - # 8 - yield Static("Key") + # Row 1 + yield Static(t("Key")) yield Input(id="key_input", classes="four") - yield Static("Value") + # Row 2 + yield Static(t("Value")) yield Input(id="value_input", classes="four") - # 17 + # Row 3 yield Static(" ", classes="five") - # 18 - yield Button("Save", id="save_button") - yield Button("Cancel", id="cancel_button") + # Row 4 + yield Button(t("Save"), id="save_button") + yield Button(t("Cancel"), id="cancel_button") - # 19 + # Row 5 yield Static(" ", classes="five") - # 20 + # Row 6 yield Static(" ", classes="five", id="messagestatic") yield Footer(id="footer") @@ -120,6 +131,9 @@ class TagDetailsScreen(Screen): return (tagKey, tagValue) + def action_back(self): + go_back_or_exit(self) + # Event handler for button press def on_button_pressed(self, event: Button.Pressed) -> None: diff --git a/src/ffx/track_delete_screen.py b/src/ffx/track_delete_screen.py index 4743538..a681fec 100644 --- a/src/ffx/track_delete_screen.py +++ b/src/ffx/track_delete_screen.py @@ -5,20 +5,29 @@ from textual.widgets import Header, Footer, Static, Button from textual.containers import Grid from ffx.track_descriptor import TrackDescriptor +from .i18n import t +from .screen_support import go_back_or_exit # Screen[dict[int, str, int]] class TrackDeleteScreen(Screen): + BINDINGS = [ + ("escape", "back", t("Back")), + ] + CSS = """ Grid { grid-size: 4 9; grid-rows: 2 2 2 2 2 2 2 2 2; - grid-columns: 30 30 30 30; + grid-columns: 18 1fr 1fr 1fr; height: 100%; width: 100%; + min-width: 90; padding: 1; + overflow-x: auto; + overflow-y: auto; } Input { @@ -70,37 +79,44 @@ class TrackDeleteScreen(Screen): with Grid(): - #1 - yield Static(f"Are you sure to delete the following {self.__trackDescriptor.getType().label()} track?", id="toplabel", classes="four") + # Row 1 + yield Static( + t( + "Are you sure to delete the following {track_type} track?", + track_type=t(self.__trackDescriptor.getType().label()), + ), + id="toplabel", + classes="four", + ) - #2 - yield Static("sub index") + # Row 2 + yield Static(t("sub index")) yield Static(" ", id="subindexlabel", classes="three") - #3 - yield Static("from pattern") + # Row 3 + yield Static(t("from pattern")) yield Static(" ", id="patternlabel", classes="three") - #4 + # Row 4 yield Static(" ", classes="four") - #5 - yield Static("Language") + # Row 5 + yield Static(t("Language")) yield Static(" ", id="languagelabel", classes="three") - #6 - yield Static("Title") + # Row 6 + yield Static(t("Title")) yield Static(" ", id="titlelabel", classes="three") - #7 + # Row 7 yield Static(" ", classes="four") - #8 + # Row 8 yield Static(" ", classes="four") - #9 - yield Button("Delete", id="delete_button") - yield Button("Cancel", id="cancel_button") + # Row 9 + yield Button(t("Delete"), id="delete_button") + yield Button(t("Cancel"), id="cancel_button") yield Footer() @@ -113,3 +129,6 @@ class TrackDeleteScreen(Screen): if event.button.id == "cancel_button": self.app.pop_screen() + + def action_back(self): + go_back_or_exit(self) diff --git a/src/ffx/track_details_screen.py b/src/ffx/track_details_screen.py index 64f1644..a6df8ff 100644 --- a/src/ffx/track_details_screen.py +++ b/src/ffx/track_details_screen.py @@ -13,20 +13,28 @@ from .track_codec import TrackCodec from .track_descriptor import TrackDescriptor from .track_disposition import TrackDisposition from .track_type import TrackType -from .screen_support import build_screen_bootstrap, populate_tag_table +from .i18n import t +from .screen_support import add_auto_table_column, build_screen_bootstrap, go_back_or_exit, populate_tag_table class TrackDetailsScreen(Screen): + BINDINGS = [ + ("escape", "back", t("Back")), + ] + CSS = """ Grid { grid-size: 5 24; grid-rows: 2 2 2 2 2 3 3 2 2 3 2 2 2 2 2 6 2 2 6 2 2 2; - grid-columns: 25 25 25 25 125; + grid-columns: 18 1fr 1fr 1fr 4fr; height: 100%; width: 100%; + min-width: 115; padding: 1; + overflow-x: auto; + overflow-y: auto; } Input { @@ -45,6 +53,7 @@ class TrackDetailsScreen(Screen): DataTable { min-height: 6; + width: 100%; } DataTable .datatable--cursor { @@ -127,6 +136,8 @@ class TrackDetailsScreen(Screen): self.__index = index self.__subIndex = subIndex self.__draftTrackTags = {} + initial_language = IsoLanguage.UNDEFINED + initial_title = "" else: self.__trackType = trackDescriptor.getType() self.__trackCodec = trackDescriptor.getCodec() @@ -138,6 +149,14 @@ class TrackDetailsScreen(Screen): for key, value in trackDescriptor.getTags().items() if key not in ("language", "title") } + initial_language = trackDescriptor.getLanguage() + initial_title = trackDescriptor.getTitle() + + self.__titleAutoManaged = ( + initial_language == IsoLanguage.UNDEFINED and not str(initial_title).strip() + ) + self.__suppressTitleChanged = False + self.__lastAutoTitle = "" def _descriptor_refs_same_track(self, descriptor: TrackDescriptor) -> bool: if self.__trackDescriptor is None: @@ -158,6 +177,49 @@ class TrackDetailsScreen(Screen): remove_keys=self.__removeTrackKeys, ) + @staticmethod + def build_language_options(): + return [ + (language.label(), language) + for language in sorted( + [language for language in IsoLanguage if language != IsoLanguage.UNDEFINED], + key=lambda language: language.label().casefold(), + ) + ] + + @staticmethod + def language_select_value(language): + return Select.NULL if language == IsoLanguage.UNDEFINED else language + + def _apply_auto_title_for_language(self, language: IsoLanguage): + titleInput = self.query_one("#title_input", Input) + autoTitle = "" if language == IsoLanguage.UNDEFINED else language.label() + self.__suppressTitleChanged = True + titleInput.value = autoTitle + self.__suppressTitleChanged = False + self.__lastAutoTitle = autoTitle + + def _handle_language_selection_changed(self, language): + if not self.__titleAutoManaged: + return + + if not isinstance(language, IsoLanguage): + language = IsoLanguage.UNDEFINED + + self._apply_auto_title_for_language(language) + + def _handle_title_input_changed(self, titleValue: str): + if self.__suppressTitleChanged or not self.__titleAutoManaged: + return + + language = self.query_one("#language_select", Select).value + if not isinstance(language, IsoLanguage): + language = IsoLanguage.UNDEFINED + + expectedAutoTitle = "" if language == IsoLanguage.UNDEFINED else language.label() + if str(titleValue) != expectedAutoTitle: + self.__titleAutoManaged = False + def on_mount(self): self.query_one("#index_label", Static).update( @@ -181,7 +243,7 @@ class TrackDetailsScreen(Screen): ) dispositionOption = ( - disposition.label(), + t(disposition.label()), disposition.index(), dispositionIsSet, ) @@ -190,7 +252,9 @@ class TrackDetailsScreen(Screen): ) if self.__trackDescriptor is not None: - self.query_one("#language_select", Select).value = self.__trackDescriptor.getLanguage() + self.query_one("#language_select", Select).value = self.language_select_value( + self.__trackDescriptor.getLanguage() + ) self.query_one("#title_input", Input).value = self.__trackDescriptor.getTitle() self.updateTags() @@ -198,12 +262,20 @@ class TrackDetailsScreen(Screen): self.query_one("#type_select", Select).disabled = True self.query_one("#audio_layout_select", Select).disabled = True + def on_select_changed(self, event: Select.Changed) -> None: + if event.select.id == "language_select": + self._handle_language_selection_changed(event.value) + + def on_input_changed(self, event: Input.Changed) -> None: + if event.input.id == "title_input": + self._handle_title_input_changed(event.value) + def compose(self): self.trackTagsTable = DataTable(classes="five") - self.column_key_track_tag_key = self.trackTagsTable.add_column("Key", width=50) - self.column_key_track_tag_value = self.trackTagsTable.add_column("Value", width=100) + self.column_key_track_tag_key = add_auto_table_column(self.trackTagsTable, t("Key")) + self.column_key_track_tag_value = add_auto_table_column(self.trackTagsTable, t("Value")) self.trackTagsTable.cursor_type = "row" @@ -211,82 +283,108 @@ class TrackDetailsScreen(Screen): with Grid(): + # Row 1 yield Static( - "New stream" if self.__isNew else "Edit stream", + t("New stream") if self.__isNew else t("Edit stream"), id="toplabel", classes="five", ) - yield Static("for pattern") + # Row 2 + yield Static(t("for pattern")) yield Static("", id="pattern_label", classes="four", markup=False) + # Row 3 yield Static(" ", classes="five") - yield Static("Index / Subindex") + # Row 4 + yield Static(t("Index / Subindex")) yield Static("", id="index_label", classes="two") yield Static("", id="subindex_label", classes="two") + # Row 5 yield Static(" ", classes="five") - yield Static("Type") + # Row 6 + yield Static(t("Type")) yield Select( - [(trackType.label(), trackType) for trackType in TrackType], + [(t(trackType.label()), trackType) for trackType in TrackType], classes="four", id="type_select", ) - yield Static("Audio Layout") + # Row 7 + yield Static(t("Audio Layout")) yield Select( - [(layout.label(), layout) for layout in AudioLayout], + [(t(layout.label()), layout) for layout in AudioLayout], classes="four", id="audio_layout_select", ) + # Row 8 yield Static(" ", classes="five") + # Row 9 yield Static(" ", classes="five") - yield Static("Language") + # Row 10 + yield Static(t("Language")) yield Select( - [(language.label(), language) for language in IsoLanguage], + self.build_language_options(), + prompt=t("Select"), classes="four", id="language_select", ) + # Row 11 yield Static(" ", classes="five") - yield Static("Title") + # Row 12 + yield Static(t("Title")) yield Input(id="title_input", classes="four") + # Row 13 yield Static(" ", classes="five") + # Row 14 yield Static(" ", classes="five") - yield Static("Stream tags") + # Row 15 + yield Static(t("Stream tags")) yield Static(" ") - yield Button("Add", id="button_add_stream_tag") - yield Button("Edit", id="button_edit_stream_tag") - yield Button("Delete", id="button_delete_stream_tag") + yield Button(t("Add"), id="button_add_stream_tag") + yield Button(t("Edit"), id="button_edit_stream_tag") + yield Button(t("Delete"), id="button_delete_stream_tag") + # Row 16 yield self.trackTagsTable + # Row 17 yield Static(" ", classes="five") - yield Static("Stream dispositions", classes="five") + # Row 18 + yield Static(t("Stream dispositions"), classes="five") + # Row 19 yield SelectionList[int]( classes="five", id="dispositions_selection_list", ) - yield Static(" ", classes="five") + # Row 20 yield Static(" ", classes="five") - yield Button("Save", id="save_button") - yield Button("Cancel", id="cancel_button") - + # Row 21 yield Static(" ", classes="five") + # Row 22 + yield Button(t("Save"), id="save_button") + yield Button(t("Cancel"), id="cancel_button") + + # Row 23 + yield Static(" ", classes="five") + + # Row 24 yield Static(" ", classes="five", id="messagestatic") yield Footer(id="footer") @@ -348,6 +446,9 @@ class TrackDetailsScreen(Screen): return TrackDescriptor(**kwargs) + def action_back(self): + go_back_or_exit(self) + def getSelectedTag(self): try: @@ -408,7 +509,9 @@ class TrackDetailsScreen(Screen): ): self.query_one("#messagestatic", Static).update( - "Cannot add another stream with disposition flag 'default' or 'forced' set" + t( + "Cannot add another stream with disposition flag 'default' or 'forced' set" + ) ) else: self.query_one("#messagestatic", Static).update(" ") diff --git a/tests/unit/test_configure_workstation_script.py b/tests/unit/test_configure_workstation_script.py index cf5e76a..6ab35ad 100644 --- a/tests/unit/test_configure_workstation_script.py +++ b/tests/unit/test_configure_workstation_script.py @@ -49,6 +49,9 @@ class ConfigureWorkstationScriptTests(unittest.TestCase): "HOME": str(self.home_dir), "PATH": f"{self.stub_bin_dir}:{os.environ.get('PATH', '')}", "FFX_PYTHON": str(BUNDLE_PYTHON), + "LANG": "C.UTF-8", + "LC_ALL": "C.UTF-8", + "LC_MESSAGES": "C.UTF-8", **env_overrides, } @@ -76,6 +79,7 @@ class ConfigureWorkstationScriptTests(unittest.TestCase): self.assertEqual( { "databasePath": str(self.home_dir / ".local" / "var" / "ffx" / "ffx.db"), + "language": "de", "logDirectory": str(self.home_dir / ".local" / "var" / "log"), "subtitlesDirectory": str( self.home_dir / ".local" / "var" / "sync" / "subtitles" @@ -113,6 +117,24 @@ class ConfigureWorkstationScriptTests(unittest.TestCase): config_data, ) + def test_script_seeds_system_language_into_default_config(self): + completed = self.run_script( + LANG="fr_FR.UTF-8", + LC_ALL="fr_FR.UTF-8", + LC_MESSAGES="fr_FR.UTF-8", + ) + + self.assertEqual( + 0, + completed.returncode, + f"STDOUT:\n{completed.stdout}\nSTDERR:\n{completed.stderr}", + ) + + config_path = self.home_dir / ".local" / "etc" / "ffx.json" + config_data = json.loads(config_path.read_text(encoding="utf-8")) + + self.assertEqual("fr", config_data["language"]) + def test_script_honors_custom_template_override(self): custom_template_path = Path(self.tempdir.name) / "custom-config.j2" custom_template_path.write_text( diff --git a/tests/unit/test_i18n.py b/tests/unit/test_i18n.py new file mode 100644 index 0000000..223d2fb --- /dev/null +++ b/tests/unit/test_i18n.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from pathlib import Path +import json +import sys +import tempfile +import unittest + + +SRC_ROOT = Path(__file__).resolve().parents[2] / "src" + +if str(SRC_ROOT) not in sys.path: + sys.path.insert(0, str(SRC_ROOT)) + + +from ffx.i18n import ( # noqa: E402 + detect_system_language, + read_configured_language, + resolve_application_language, + set_current_language, + t, +) +from ffx.iso_language import IsoLanguage # noqa: E402 + + +class I18nTests(unittest.TestCase): + def tearDown(self): + set_current_language("de") + + def test_cli_language_takes_precedence_over_config_and_system(self): + self.assertEqual( + "es", + resolve_application_language( + cli_language="es", + config_language="fr", + system_language="ja", + ), + ) + + def test_config_language_takes_precedence_over_system(self): + self.assertEqual( + "fr", + resolve_application_language( + config_language="fr", + system_language="ja", + ), + ) + + def test_system_language_is_used_when_no_cli_or_config_is_present(self): + self.assertEqual("ja", resolve_application_language(system_language="ja")) + + def test_german_is_default_when_no_supported_language_is_available(self): + self.assertEqual( + "de", + resolve_application_language( + env={ + "LANG": "C.UTF-8", + "LC_ALL": "C.UTF-8", + "LC_MESSAGES": "C.UTF-8", + } + ), + ) + + def test_system_language_detection_normalizes_norwegian_bokmal(self): + self.assertEqual( + "nb", + detect_system_language({"LANG": "nb_NO.UTF-8"}), + ) + + def test_read_configured_language_normalizes_language_code(self): + with tempfile.TemporaryDirectory() as tempdir: + config_path = Path(tempdir) / "ffx.json" + config_path.write_text( + json.dumps({"language": "pt_BR.UTF-8"}), + encoding="utf-8", + ) + self.assertEqual("pt", read_configured_language(config_path)) + + def test_phrase_translation_uses_catalog_for_selected_language(self): + set_current_language("fr") + self.assertEqual("Ajouter", t("Add")) + + def test_iso_language_labels_use_catalog_for_selected_language(self): + set_current_language("de") + self.assertEqual("Deutsch", IsoLanguage.GERMAN.label()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/test_media_descriptor_change_set.py b/tests/unit/test_media_descriptor_change_set.py index 309ea12..8e196ec 100644 --- a/tests/unit/test_media_descriptor_change_set.py +++ b/tests/unit/test_media_descriptor_change_set.py @@ -15,6 +15,7 @@ from ffx.media_descriptor import MediaDescriptor # noqa: E402 from ffx.media_descriptor_change_set import MediaDescriptorChangeSet # noqa: E402 from ffx.track_descriptor import TrackDescriptor # noqa: E402 from ffx.track_type import TrackType # noqa: E402 +from ffx.i18n import set_current_language # noqa: E402 from ffx.logging_utils import get_ffx_logger # noqa: E402 @@ -27,6 +28,9 @@ class StaticConfig: class MediaDescriptorChangeSetTests(unittest.TestCase): + def tearDown(self): + set_current_language("de") + def test_non_primary_source_language_code_is_normalized_in_changed_track_metadata(self): context = { "logger": get_ffx_logger(), @@ -171,6 +175,111 @@ class MediaDescriptorChangeSetTests(unittest.TestCase): self.assertIn("language=deu", metadata_tokens) self.assertNotIn("language=ger", metadata_tokens) + def test_subtitle_without_title_gets_language_name_when_normalization_enabled(self): + set_current_language("de") + context = { + "logger": get_ffx_logger(), + "config": StaticConfig({}), + } + + source_track = TrackDescriptor( + index=0, + source_index=0, + sub_index=0, + track_type=TrackType.SUBTITLE, + tags={"language": "ger"}, + ) + target_track = TrackDescriptor( + index=0, + source_index=0, + sub_index=0, + track_type=TrackType.SUBTITLE, + tags={"language": "ger"}, + ) + + change_set = MediaDescriptorChangeSet( + context, + MediaDescriptor(track_descriptors=[target_track]), + MediaDescriptor(track_descriptors=[source_track]), + ) + + metadata_tokens = change_set.generateMetadataTokens() + change_set_obj = change_set.getChangeSetObj() + + self.assertIn("-metadata:s:s:0", metadata_tokens) + self.assertIn("language=deu", metadata_tokens) + self.assertIn("title=Deutsch", metadata_tokens) + self.assertEqual( + "Deutsch", + change_set_obj["tracks"]["changed"][0]["tags"]["added"]["title"], + ) + + def test_subtitle_without_title_uses_current_language_for_generated_title(self): + set_current_language("en") + context = { + "logger": get_ffx_logger(), + "config": StaticConfig({}), + } + + source_track = TrackDescriptor( + index=0, + source_index=0, + sub_index=0, + track_type=TrackType.SUBTITLE, + tags={"language": "ger"}, + ) + target_track = TrackDescriptor( + index=0, + source_index=0, + sub_index=0, + track_type=TrackType.SUBTITLE, + tags={"language": "ger"}, + ) + + change_set = MediaDescriptorChangeSet( + context, + MediaDescriptor(track_descriptors=[target_track]), + MediaDescriptor(track_descriptors=[source_track]), + ) + + metadata_tokens = change_set.generateMetadataTokens() + + self.assertIn("title=German", metadata_tokens) + self.assertNotIn("title=Deutsch", metadata_tokens) + + def test_non_subtitle_track_without_title_does_not_get_language_name(self): + context = { + "logger": get_ffx_logger(), + "config": StaticConfig({}), + } + + source_track = TrackDescriptor( + index=0, + source_index=0, + sub_index=0, + track_type=TrackType.AUDIO, + tags={"language": "ger"}, + ) + target_track = TrackDescriptor( + index=0, + source_index=0, + sub_index=0, + track_type=TrackType.AUDIO, + tags={"language": "ger"}, + ) + + change_set = MediaDescriptorChangeSet( + context, + MediaDescriptor(track_descriptors=[target_track]), + MediaDescriptor(track_descriptors=[source_track]), + ) + + metadata_tokens = change_set.generateMetadataTokens() + + self.assertIn("-metadata:s:a:0", metadata_tokens) + self.assertIn("language=deu", metadata_tokens) + self.assertNotIn("title=Deutsch", metadata_tokens) + def test_target_only_tracks_still_emit_remove_tokens_for_configured_stream_keys(self): context = { "logger": get_ffx_logger(), @@ -259,6 +368,32 @@ class MediaDescriptorChangeSetTests(unittest.TestCase): self.assertNotIn("creation_time=", metadata_tokens) self.assertNotIn("BPS=", metadata_tokens) + def test_normalization_can_be_disabled_per_context(self): + context = { + "logger": get_ffx_logger(), + "config": StaticConfig({}), + "apply_metadata_normalization": False, + } + + target_track = TrackDescriptor( + index=0, + source_index=0, + sub_index=0, + track_type=TrackType.AUDIO, + tags={"language": "ger", "title": "German Main"}, + ) + + change_set = MediaDescriptorChangeSet( + context, + MediaDescriptor(track_descriptors=[target_track]), + ) + + metadata_tokens = change_set.generateMetadataTokens() + + self.assertIn("-metadata:s:a:0", metadata_tokens) + self.assertIn("language=ger", metadata_tokens) + self.assertNotIn("language=deu", metadata_tokens) + if __name__ == "__main__": unittest.main() diff --git a/tests/unit/test_metadata_editor.py b/tests/unit/test_metadata_editor.py index 2c1bad1..753d544 100644 --- a/tests/unit/test_metadata_editor.py +++ b/tests/unit/test_metadata_editor.py @@ -38,6 +38,7 @@ def make_context(*, dry_run: bool = False) -> dict: "config": StaticConfig(), "dry_run": dry_run, "apply_metadata_cleanup": True, + "apply_metadata_normalization": True, } @@ -65,6 +66,8 @@ class MetadataEditorTests(unittest.TestCase): self.assertFalse(context["perform_cut"]) self.assertTrue(context["no_signature"]) self.assertEqual({}, context["encoding_metadata_tags"]) + self.assertTrue(context["apply_metadata_cleanup"]) + self.assertTrue(context["apply_metadata_normalization"]) def test_create_temporary_output_path_uses_same_directory_and_extension(self): with tempfile.TemporaryDirectory() as tmpdir: diff --git a/tests/unit/test_screen_support.py b/tests/unit/test_screen_support.py index 980da48..16f7984 100644 --- a/tests/unit/test_screen_support.py +++ b/tests/unit/test_screen_support.py @@ -13,6 +13,7 @@ if str(SRC_ROOT) not in sys.path: from ffx import screen_support # noqa: E402 +from ffx.i18n import set_current_language, t # noqa: E402 class StaticConfig: @@ -38,7 +39,28 @@ class FakeTagTable: return row_key +class FakeApp: + def __init__(self, screen_stack): + self.screen_stack = list(screen_stack) + self.pop_called = False + self.exit_called = False + + def pop_screen(self): + self.pop_called = True + + def exit(self): + self.exit_called = True + + +class FakeScreen: + def __init__(self, screen_stack): + self.app = FakeApp(screen_stack) + + class ScreenSupportTests(unittest.TestCase): + def tearDown(self): + set_current_language("de") + def make_context(self): return { "config": StaticConfig( @@ -122,6 +144,30 @@ class ScreenSupportTests(unittest.TestCase): table.rows["row-1"], ) + def test_go_back_or_exit_exits_from_first_pushed_screen(self): + screen = FakeScreen(screen_stack=["base", "shows"]) + + screen_support.go_back_or_exit(screen) + + self.assertFalse(screen.app.pop_called) + self.assertTrue(screen.app.exit_called) + + def test_go_back_or_exit_pops_nested_screen(self): + screen = FakeScreen(screen_stack=["base", "shows", "details"]) + + screen_support.go_back_or_exit(screen) + + self.assertTrue(screen.app.pop_called) + self.assertFalse(screen.app.exit_called) + + def test_localized_column_width_handles_combining_character_labels(self): + set_current_language("ta") + + translated = t("SubIndex") + self.assertEqual("துணைச்சுட்டி", translated) + self.assertGreater(len(translated), 8) + self.assertEqual(len(translated) + 2, screen_support.localized_column_width(translated, 8)) + if __name__ == "__main__": unittest.main() diff --git a/tests/unit/test_tag_table_screen_state.py b/tests/unit/test_tag_table_screen_state.py index 19b80ab..f3230a6 100644 --- a/tests/unit/test_tag_table_screen_state.py +++ b/tests/unit/test_tag_table_screen_state.py @@ -4,6 +4,8 @@ from pathlib import Path import sys import unittest +from textual.widgets import Select + SRC_ROOT = Path(__file__).resolve().parents[2] / "src" @@ -15,6 +17,7 @@ from ffx.audio_layout import AudioLayout # noqa: E402 from ffx.iso_language import IsoLanguage # noqa: E402 from ffx.logging_utils import get_ffx_logger # noqa: E402 from ffx.inspect_details_screen import InspectDetailsScreen # noqa: E402 +from ffx.i18n import set_current_language # noqa: E402 from ffx.media_edit_screen import MediaEditScreen # noqa: E402 from ffx.pattern_details_screen import PatternDetailsScreen # noqa: E402 from ffx.show_descriptor import ShowDescriptor # noqa: E402 @@ -29,15 +32,30 @@ from ffx.track_type import TrackType # noqa: E402 class FakeTagTable: def __init__(self): self.rows = {} + self.columns = [] self.cursor_coordinate = (0, 0) self._selected_row_key = None self._next_index = 0 self._row_order = [] - def clear(self): + def clear(self, columns=False): self.rows.clear() self._selected_row_key = None self._row_order.clear() + if columns: + self.columns.clear() + + def add_column(self, label, *, width=None, key=None, default=None): + column_key = key if key is not None else len(self.columns) + self.columns.append( + { + "key": column_key, + "label": label, + "width": width, + "default": default, + } + ) + return column_key def add_row(self, *values): row_key = f"row-{self._next_index}" @@ -112,6 +130,9 @@ def make_show_descriptor(show_id, name="Show", year=2000): class TagTableScreenStateTests(unittest.TestCase): + def tearDown(self): + set_current_language("de") + def test_track_details_screen_reads_selected_tag_from_raw_row_mapping(self): screen = object.__new__(TrackDetailsScreen) screen.trackTagsTable = FakeTagTable() @@ -161,6 +182,88 @@ class TagTableScreenStateTests(unittest.TestCase): self.assertEqual("German Audio", descriptor.getTitle()) self.assertEqual("value", descriptor.getTags()["KEEP"]) + def test_track_details_screen_auto_sets_localized_title_from_selected_language(self): + set_current_language("de") + screen = object.__new__(TrackDetailsScreen) + screen._TrackDetailsScreen__titleAutoManaged = True + screen._TrackDetailsScreen__suppressTitleChanged = False + screen._TrackDetailsScreen__lastAutoTitle = "" + + widgets = { + "#language_select": FakeValueWidget(IsoLanguage.UNDEFINED), + "#title_input": FakeInputWidget(""), + } + screen.query_one = lambda selector, _widget_type=None: widgets[selector] + + widgets["#language_select"].value = IsoLanguage.GERMAN + screen._handle_language_selection_changed(IsoLanguage.GERMAN) + self.assertEqual("Deutsch", widgets["#title_input"].value) + + widgets["#language_select"].value = IsoLanguage.ENGLISH + screen._handle_language_selection_changed(IsoLanguage.ENGLISH) + self.assertEqual("Englisch", widgets["#title_input"].value) + + def test_track_details_screen_auto_title_stops_after_manual_title_change(self): + set_current_language("de") + screen = object.__new__(TrackDetailsScreen) + screen._TrackDetailsScreen__titleAutoManaged = True + screen._TrackDetailsScreen__suppressTitleChanged = False + screen._TrackDetailsScreen__lastAutoTitle = "" + + widgets = { + "#language_select": FakeValueWidget(IsoLanguage.GERMAN), + "#title_input": FakeInputWidget(""), + } + screen.query_one = lambda selector, _widget_type=None: widgets[selector] + + screen._handle_language_selection_changed(IsoLanguage.GERMAN) + self.assertEqual("Deutsch", widgets["#title_input"].value) + + widgets["#title_input"].value = "Eigener Titel" + screen._handle_title_input_changed("Eigener Titel") + + widgets["#language_select"].value = IsoLanguage.ENGLISH + screen._handle_language_selection_changed(IsoLanguage.ENGLISH) + self.assertEqual("Eigener Titel", widgets["#title_input"].value) + + def test_track_details_screen_does_not_auto_set_title_when_helper_is_inactive(self): + set_current_language("de") + screen = object.__new__(TrackDetailsScreen) + screen._TrackDetailsScreen__titleAutoManaged = False + screen._TrackDetailsScreen__suppressTitleChanged = False + screen._TrackDetailsScreen__lastAutoTitle = "" + + widgets = { + "#language_select": FakeValueWidget(IsoLanguage.UNDEFINED), + "#title_input": FakeInputWidget("Preset"), + } + screen.query_one = lambda selector, _widget_type=None: widgets[selector] + + widgets["#language_select"].value = IsoLanguage.GERMAN + screen._handle_language_selection_changed(IsoLanguage.GERMAN) + + self.assertEqual("Preset", widgets["#title_input"].value) + + def test_track_details_screen_language_options_are_sorted_by_localized_label(self): + set_current_language("de") + + language_options = TrackDetailsScreen.build_language_options() + labels = [label for label, _language in language_options] + languages = [_language for _label, _language in language_options] + + self.assertEqual(labels, sorted(labels, key=str.casefold)) + self.assertNotIn(IsoLanguage.UNDEFINED, languages) + + def test_track_details_screen_uses_blank_select_value_for_undefined_language(self): + self.assertEqual( + TrackDetailsScreen.language_select_value(IsoLanguage.UNDEFINED), + Select.NULL, + ) + self.assertEqual( + IsoLanguage.GERMAN, + TrackDetailsScreen.language_select_value(IsoLanguage.GERMAN), + ) + def test_pattern_details_screen_reads_selected_track_from_row_mapping(self): first_track = make_track_descriptor(0, 0, TrackType.VIDEO) second_track = make_track_descriptor(1, 0, TrackType.SUBTITLE) @@ -215,6 +318,20 @@ class TagTableScreenStateTests(unittest.TestCase): self.assertIs(second_track, screen.getSelectedTrackDescriptor()) + def test_media_edit_screen_update_tracks_rebuilds_columns_for_auto_width_recalculation(self): + first_track = make_track_descriptor(0, 0, TrackType.VIDEO) + first_track.getTags()["title"] = "A much longer updated title" + + screen = object.__new__(MediaEditScreen) + screen.tracksTable = FakeTagTable() + screen._sourceMediaDescriptor = FakeMediaDescriptor([first_track]) + screen._trackRowData = {} + + screen.updateTracks() + + self.assertEqual(9, len(screen.tracksTable.columns)) + self.assertIn("A much longer updated title", screen.tracksTable.rows["row-0"]) + def test_pattern_details_screen_reads_selected_shifted_season_from_row_mapping(self): screen = object.__new__(PatternDetailsScreen) screen.shiftedSeasonsTable = FakeTagTable() diff --git a/tools/configure_workstation.sh b/tools/configure_workstation.sh index 30f56ab..e407a8f 100755 --- a/tools/configure_workstation.sh +++ b/tools/configure_workstation.sh @@ -369,6 +369,7 @@ from ffx.constants import ( DEFAULT_SHOW_INDICATOR_EPISODE_DIGITS, DEFAULT_SHOW_INDICATOR_SEASON_DIGITS, ) +from ffx.i18n import resolve_application_language template_path = Path(os.environ["FFX_CONFIG_TEMPLATE_FILE"]) environment = Environment( @@ -381,6 +382,7 @@ template = environment.get_template(template_path.name) sys.stdout.write( template.render( + language_json=json.dumps(resolve_application_language()), database_path_json=json.dumps(os.environ["FFX_DATABASE_PATH"]), log_directory_json=json.dumps(os.environ["FFX_LOG_DIRECTORY"]), subtitles_directory_json=json.dumps(os.environ["FFX_SUBTITLES_DIRECTORY"]), diff --git a/tools/generate_i18n_catalogs.py b/tools/generate_i18n_catalogs.py new file mode 100644 index 0000000..d9eadd1 --- /dev/null +++ b/tools/generate_i18n_catalogs.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import ast +import gettext +import json +from pathlib import Path + +from ffx.i18n import SUPPORTED_LANGUAGES +from ffx.iso_language import IsoLanguage + + +REPO_ROOT = Path(__file__).resolve().parents[1] +SOURCE_ROOT = REPO_ROOT / "src" / "ffx" +OUTPUT_ROOT = REPO_ROOT / "assets" / "i18n" +LANGUAGE_CODES = ("de", "en", "fr", "ja", "nb", "eo", "ta", "pt", "es") +TRANSLATED_LANGUAGE_CODES = ("de", "fr", "ja", "nb", "eo", "ta", "pt", "es") +EXTRA_PHRASES = { + "Differences", + "Differences (file->db/output)", + "Planned Changes (file->edited output)", + "video", + "audio", + "subtitle", + "attachment", + "unknown", + "default", + "forced", + "dub", + "original", + "comment", + "lyrics", + "karaoke", + "hearing_impaired", + "visual_impaired", + "clean_effects", + "attached_pic", + "timed_thumbnails", + "non_diegetic", + "captions", + "descriptions", + "metadata", + "dependent", + "still_image", + "stereo", + "5.1(side)", + "6.1", + "7.1", + "6ch", + "5.0(side)", + "undefined", +} + + +PHRASE_ROWS = [ + ("", "", "", "<新しい番組>", "", "", "<புதிய தொடர்>", "", ""), + ("Add", "Hinzufügen", "Ajouter", "追加", "Legg til", "Aldoni", "சேர்", "Adicionar", "Añadir"), + ("Add Pattern", "Muster hinzufügen", "Ajouter un modèle", "パターンを追加", "Legg til mønster", "Aldoni ŝablonon", "வடிவத்தை சேர்", "Adicionar padrão", "Añadir patrón"), + ("Apply", "Anwenden", "Appliquer", "適用", "Bruk", "Apliki", "பயன்படுத்து", "Aplicar", "Aplicar"), + ("Apply failed: {error}", "Anwenden fehlgeschlagen: {error}", "Échec de l'application : {error}", "適用に失敗しました: {error}", "Kunne ikke bruke endringene: {error}", "Apliko malsukcesis: {error}", "பயன்படுத்தல் தோல்வியடைந்தது: {error}", "Falha ao aplicar: {error}", "Error al aplicar: {error}"), + ("Are you sure to delete the following filename pattern?", "Möchtest du das folgende Dateinamensmuster wirklich löschen?", "Voulez-vous vraiment supprimer le modèle de nom de fichier suivant ?", "次のファイル名パターンを削除してもよろしいですか?", "Er du sikker på at du vil slette følgende filnavnmønster?", "Ĉu vi certe volas forigi la jenan dosiernoman ŝablonon?", "பின்வரும் கோப்பு பெயர் வடிவத்தை நீக்க விரும்புகிறீர்களா?", "Tem certeza de que deseja excluir o seguinte padrão de nome de arquivo?", "¿Seguro que quieres eliminar el siguiente patrón de nombre de archivo?"), + ("Are you sure to delete the following shifted season?", "Möchtest du die folgende verschobene Staffel wirklich löschen?", "Voulez-vous vraiment supprimer la saison décalée suivante ?", "次のシーズンシフト設定を削除してもよろしいですか?", "Er du sikker på at du vil slette følgende forskjøvede sesong?", "Ĉu vi certe volas forigi la jenan ŝovitan sezonon?", "பின்வரும் மாற்றிய சீசனை நீக்க விரும்புகிறீர்களா?", "Tem certeza de que deseja excluir a seguinte temporada deslocada?", "¿Seguro que quieres eliminar la siguiente temporada desplazada?"), + ("Are you sure to delete the following show?", "Möchtest du die folgende Serie wirklich löschen?", "Voulez-vous vraiment supprimer la série suivante ?", "次の番組を削除してもよろしいですか?", "Er du sikker på at du vil slette følgende serie?", "Ĉu vi certe volas forigi la jenan serion?", "பின்வரும் தொடரை நீக்க விரும்புகிறீர்களா?", "Tem certeza de que deseja excluir a seguinte série?", "¿Seguro que quieres eliminar la siguiente serie?"), + ("Are you sure to delete the following {track_type} track?", "Möchtest du den folgenden {track_type}-Stream wirklich löschen?", "Voulez-vous vraiment supprimer la piste {track_type} suivante ?", "次の{track_type}ストリームを削除してもよろしいですか?", "Er du sikker på at du vil slette følgende {track_type}-spor?", "Ĉu vi certe volas forigi la jenan {track_type}-trakon?", "பின்வரும் {track_type} ஸ்ட்ரீமை நீக்க விரும்புகிறீர்களா?", "Tem certeza de que deseja excluir a seguinte faixa {track_type}?", "¿Seguro que quieres eliminar la pista {track_type} siguiente?"), + ("Are you sure to delete this tag?", "Möchtest du dieses Tag wirklich löschen?", "Voulez-vous vraiment supprimer cette balise ?", "このタグを削除してもよろしいですか?", "Er du sikker på at du vil slette denne taggen?", "Ĉu vi certe volas forigi ĉi tiun etikedon?", "இந்த குறிச்சொல்லை நீக்க விரும்புகிறீர்களா?", "Tem certeza de que deseja excluir esta tag?", "¿Seguro que quieres eliminar esta etiqueta?"), + ("Audio Layout", "Audiolayout", "Disposition audio", "音声レイアウト", "Lydoppsett", "Aŭda aranĝo", "ஒலி அமைப்பு", "Layout de áudio", "Disposición de audio"), + ("Back", "Zurück", "Retour", "戻る", "Tilbake", "Reen", "பின்", "Voltar", "Volver"), + ("Cancel", "Abbrechen", "Annuler", "キャンセル", "Avbryt", "Nuligi", "ரத்து", "Cancelar", "Cancelar"), + ("Cannot add another stream with disposition flag 'default' or 'forced' set", "Es kann kein weiterer Stream mit gesetztem Dispositions-Flag 'default' oder 'forced' hinzugefügt werden", "Impossible d'ajouter un autre flux avec l'indicateur de disposition 'default' ou 'forced'", "default または forced の disposition が設定されたストリームはこれ以上追加できません", "Kan ikke legge til en ny strøm med disposisjonsflagget 'default' eller 'forced' satt", "Ne eblas aldoni alian fluon kun la dispozicia flago 'default' aŭ 'forced' aktiva", "'default' அல்லது 'forced' disposition கொடி அமைந்த மற்றொரு ஸ்ட்ரீமை சேர்க்க முடியாது", "Não é possível adicionar outro fluxo com a flag de disposição 'default' ou 'forced' definida", "No se puede añadir otro flujo con la marca de disposición 'default' o 'forced' activada"), + ("Changes applied and file reloaded.", "Änderungen angewendet und Datei neu geladen.", "Modifications appliquées et fichier rechargé.", "変更を適用し、ファイルを再読み込みしました。", "Endringene er brukt og filen er lastet inn på nytt.", "Ŝanĝoj aplikitaj kaj dosiero reŝargita.", "மாற்றங்கள் பயன்படுத்தப்பட்டு கோப்பு மீளேற்றப்பட்டது.", "Alterações aplicadas e arquivo recarregado.", "Cambios aplicados y archivo recargado."), + ("Cleanup", "Bereinigen", "Nettoyage", "クリーンアップ", "Rydd opp", "Purigado", "சுத்திகரிப்பு", "Limpeza", "Limpieza"), + ("Cleanup disabled.", "Bereinigung deaktiviert.", "Nettoyage désactivé.", "クリーンアップを無効にしました。", "Rydding deaktivert.", "Purigado malŝaltita.", "சுத்திகரிப்பு முடக்கப்பட்டது.", "Limpeza desativada.", "Limpieza desactivada."), + ("Cleanup enabled.", "Bereinigung aktiviert.", "Nettoyage activé.", "クリーンアップを有効にしました。", "Rydding aktivert.", "Purigado ŝaltita.", "சுத்திகரிப்பு இயக்கப்பட்டது.", "Limpeza ativada.", "Limpieza activada."), + ("Codec", "Codec", "Codec", "コーデック", "Kodek", "Kodeko", "கோடெக்", "Codec", "Códec"), + ("Continuing edit session.", "Bearbeitung wird fortgesetzt.", "Poursuite de la session d'édition.", "編集セッションを続行します。", "Fortsetter redigeringsøkten.", "Daŭrigante la redaktan seancon.", "திருத்த அமர்வு தொடர்கிறது.", "Continuando a sessão de edição.", "Continuando la sesión de edición."), + ("Default", "Standard", "Par défaut", "デフォルト", "Standard", "Defaŭlta", "இயல்புநிலை", "Padrão", "Predeterminado"), + ("Delete", "Löschen", "Supprimer", "削除", "Slett", "Forigi", "நீக்கு", "Excluir", "Eliminar"), + ("Delete Show", "Serie löschen", "Supprimer la série", "番組を削除", "Slett serie", "Forigi serion", "தொடரை நீக்கு", "Excluir série", "Eliminar serie"), + ("Deleted media tag {tag!r}.", "Medien-Tag {tag!r} gelöscht.", "Balise média {tag!r} supprimée.", "メディアタグ {tag!r} を削除しました。", "Mediataggen {tag!r} ble slettet.", "Forigis la aŭdvidan etikedon {tag!r}.", "மீடியா குறிச்சொல் {tag!r} நீக்கப்பட்டது.", "Tag de mídia {tag!r} excluída.", "Etiqueta de medios {tag!r} eliminada."), + ("Discard", "Verwerfen", "Ignorer", "破棄", "Forkast", "Forĵeti", "கைவிடு", "Descartar", "Descartar"), + ("Discard pending metadata changes and quit?", "Ausstehende Metadatenänderungen verwerfen und beenden?", "Ignorer les modifications de métadonnées en attente et quitter ?", "保留中のメタデータ変更を破棄して終了しますか?", "Forkaste ventende metadataendringer og avslutte?", "Ĉu forĵeti atendatajn metadatumajn ŝanĝojn kaj eliri?", "நிலுவையில் உள்ள மெட்டாடேட்டா மாற்றங்களை கைவிட்டு வெளியேறவா?", "Descartar alterações pendentes de metadados e sair?", "¿Descartar los cambios pendientes de metadatos y salir?"), + ("Discard pending metadata changes and reload the file state?", "Ausstehende Metadatenänderungen verwerfen und Dateistand neu laden?", "Ignorer les modifications de métadonnées en attente et recharger l'état du fichier ?", "保留中のメタデータ変更を破棄してファイル状態を再読み込みしますか?", "Forkaste ventende metadataendringer og laste filtilstanden på nytt?", "Ĉu forĵeti atendatajn metadatumajn ŝanĝojn kaj reŝargi la dosieran staton?", "நிலுவையில் உள்ள மெட்டாடேட்டா மாற்றங்களை கைவிட்டு கோப்பு நிலையை மீளேற்றவா?", "Descartar alterações pendentes de metadados e recarregar o estado do arquivo?", "¿Descartar los cambios pendientes de metadatos y recargar el estado del archivo?"), + ("Differences", "Unterschiede", "Différences", "差分", "Forskjeller", "Diferencoj", "வேறுபாடுகள்", "Diferenças", "Diferencias"), + ("Differences (file->db/output)", "Unterschiede (Datei->DB/Ausgabe)", "Différences (fichier->BD/sortie)", "差分 (ファイル->DB/出力)", "Forskjeller (fil->DB/utdata)", "Diferencoj (dosiero->DB/eligo)", "வேறுபாடுகள் (கோப்பு->DB/வெளியீடு)", "Diferenças (arquivo->BD/saída)", "Diferencias (archivo->BD/salida)"), + ("Down", "Runter", "Descendre", "下へ", "Ned", "Malsupren", "கீழ்", "Baixo", "Abajo"), + ("Dry-run: would rewrite via temporary file {target_path}", "Trockenlauf: würde über temporäre Datei {target_path} neu schreiben", "Simulation : réécrirait via le fichier temporaire {target_path}", "ドライラン: 一時ファイル {target_path} 経由で再書き込みします", "Tørrkjøring: ville skrevet om via midlertidig fil {target_path}", "Seka provo: reskribus per provizora dosiero {target_path}", "Dry-run: தற்காலிக கோப்பு {target_path} வழியாக மறுஎழுதப்படும்", "Execução simulada: regravaria via arquivo temporário {target_path}", "Simulación: reescribiría mediante el archivo temporal {target_path}"), + ("Edit", "Bearbeiten", "Modifier", "編集", "Rediger", "Redakti", "திருத்து", "Editar", "Editar"), + ("Edit Pattern", "Muster bearbeiten", "Modifier le modèle", "パターンを編集", "Rediger mønster", "Redakti ŝablonon", "வடிவத்தை திருத்து", "Editar padrão", "Editar patrón"), + ("Edit Show", "Serie bearbeiten", "Modifier la série", "番組を編集", "Rediger serie", "Redakti serion", "தொடரை திருத்து", "Editar série", "Editar serie"), + ("Edit filename pattern", "Dateinamensmuster bearbeiten", "Modifier le modèle de nom de fichier", "ファイル名パターンを編集", "Rediger filnavnmønster", "Redakti dosiernoman ŝablonon", "கோப்பு பெயர் வடிவத்தை திருத்து", "Editar padrão de nome de arquivo", "Editar patrón de nombre de archivo"), + ("Edit shifted season", "Verschobene Staffel bearbeiten", "Modifier la saison décalée", "シフト済みシーズンを編集", "Rediger forskjøvet sesong", "Redakti ŝovitan sezonon", "மாற்றிய சீசனை திருத்து", "Editar temporada deslocada", "Editar temporada desplazada"), + ("Edit stream", "Stream bearbeiten", "Modifier le flux", "ストリームを編集", "Rediger strøm", "Redakti fluon", "ஸ்ட்ரீமை திருத்து", "Editar fluxo", "Editar flujo"), + ("Episode Offset", "Episodenoffset", "Décalage d'épisode", "エピソードオフセット", "Episodeforskyvning", "Epizoda deŝovo", "அத்தியாய இடச்சரிவு", "Deslocamento de episódio", "Desplazamiento de episodio"), + ("Episode offset", "Episodenoffset", "Décalage d'épisode", "エピソードオフセット", "Episodeforskyvning", "Epizoda deŝovo", "அத்தியாய இடச்சரிவு", "Deslocamento de episódio", "Desplazamiento de episodio"), + ("File", "Datei", "Fichier", "ファイル", "Fil", "Dosiero", "கோப்பு", "Arquivo", "Archivo"), + ("File patterns", "Dateimuster", "Modèles de fichiers", "ファイルパターン", "Filmønstre", "Dosieraj ŝablonoj", "கோப்பு வடிவங்கள்", "Padrões de arquivo", "Patrones de archivo"), + ("First Episode", "Erste Episode", "Premier épisode", "最初のエピソード", "Første episode", "Unua epizodo", "முதல் அத்தியாயம்", "Primeiro episódio", "Primer episodio"), + ("First episode", "Erste Episode", "Premier épisode", "最初のエピソード", "Første episode", "Unua epizodo", "முதல் அத்தியாயம்", "Primeiro episódio", "Primer episodio"), + ("Forced", "Erzwungen", "Forcé", "強制", "Tvungen", "Devigita", "கட்டாயம்", "Forçado", "Forzado"), + ("Help", "Hilfe", "Aide", "ヘルプ", "Hjelp", "Helpo", "உதவி", "Ajuda", "Ayuda"), + ("Help Screen", "Hilfe-Bildschirm", "Écran d'aide", "ヘルプ画面", "Hjelpeskjerm", "Helpa ekrano", "உதவி திரை", "Tela de ajuda", "Pantalla de ayuda"), + ("ID", "ID", "ID", "ID", "ID", "ID", "அடையாளம்", "ID", "ID"), + ("Identify", "Identifizieren", "Identifier", "識別", "Identifiser", "Identigi", "அடையாளம் காட்டு", "Identificar", "Identificar"), + ("Index", "Index", "Index", "インデックス", "Indeks", "Indekso", "சுட்டி", "Índice", "Índice"), + ("Index / Subindex", "Index / Unterindex", "Index / Sous-index", "インデックス / サブインデックス", "Indeks / Underindeks", "Indekso / Subindekso", "சுட்டி / துணைச்சுட்டி", "Índice / Subíndice", "Índice / Subíndice"), + ("Index Episode Digits", "Index-Episodenziffern", "Chiffres d'épisode d'index", "インデックスのエピソード桁数", "Siffer for episodeindeks", "Ciferoj de epizoda indekso", "அத்தியாய சுட்டி இலக்கங்கள்", "Dígitos do índice do episódio", "Dígitos del índice de episodio"), + ("Index Season Digits", "Index-Staffelziffern", "Chiffres de saison d'index", "インデックスのシーズン桁数", "Siffer for sesongindeks", "Ciferoj de sezona indekso", "சீசன் சுட்டி இலக்கங்கள்", "Dígitos do índice da temporada", "Dígitos del índice de temporada"), + ("Indicator Edisode Digits", "Indikator-Episodenziffern", "Chiffres d'épisode de l'indicateur", "インジケーターのエピソード桁数", "Siffer for episodeindikator", "Ciferoj de epizoda indikilo", "அத்தியாய குறியீட்டு இலக்கங்கள்", "Dígitos do indicador do episódio", "Dígitos del indicador de episodio"), + ("Indicator Season Digits", "Indikator-Staffelziffern", "Chiffres de saison de l'indicateur", "インジケーターのシーズン桁数", "Siffer for sesongindikator", "Ciferoj de sezona indikilo", "சீசன் குறியீட்டு இலக்கங்கள்", "Dígitos do indicador da temporada", "Dígitos del indicador de temporada"), + ("Keep Editing", "Weiter bearbeiten", "Continuer l'édition", "編集を続ける", "Fortsett redigeringen", "Daŭrigi redaktadon", "திருத்தலை தொடரு", "Continuar editando", "Seguir editando"), + ("Keeping pending changes.", "Ausstehende Änderungen bleiben erhalten.", "Les modifications en attente sont conservées.", "保留中の変更を保持します。", "Beholder ventende endringer.", "Konservas atendatajn ŝanĝojn.", "நிலுவையில் உள்ள மாற்றங்கள் வைக்கப்படுகின்றன.", "Mantendo alterações pendentes.", "Se conservan los cambios pendientes."), + ("Key", "Schlüssel", "Clé", "キー", "Nøkkel", "Ŝlosilo", "சாவி", "Chave", "Clave"), + ("Language", "Sprache", "Langue", "言語", "Språk", "Lingvo", "மொழி", "Idioma", "Idioma"), + ("Last Episode", "Letzte Episode", "Dernier épisode", "最後のエピソード", "Siste episode", "Lasta epizodo", "கடைசி அத்தியாயம்", "Último episódio", "Último episodio"), + ("Last episode", "Letzte Episode", "Dernier épisode", "最後のエピソード", "Siste episode", "Lasta epizodo", "கடைசி அத்தியாயம்", "Último episódio", "Último episodio"), + ("Layout", "Layout", "Disposition", "レイアウト", "Oppsett", "Aranĝo", "அமைப்பு", "Layout", "Diseño"), + ("Media Tags", "Medien-Tags", "Balises média", "メディアタグ", "Mediatagger", "Aŭdvidaj etikedoj", "மீடியா குறிச்சொற்கள்", "Tags de mídia", "Etiquetas de medios"), + ("More than one default audio stream detected and no prompt set", "Mehr als ein Standard-Audiostream erkannt und keine Abfrage aktiviert", "Plus d'un flux audio par défaut détecté et aucune invite définie", "デフォルト音声ストリームが複数検出され、プロンプトも設定されていません", "Mer enn én standard lydstrøm funnet og ingen forespørsel satt", "Pli ol unu defaŭlta sonfluo detektita kaj neniu instigo agordita", "ஒருக்கும் மேற்பட்ட இயல்புநிலை ஒலி ஸ்ட்ரீம்கள் கண்டறியப்பட்டன, மேலும் எந்த prompt-வும் அமைக்கப்படவில்லை", "Mais de um fluxo de áudio padrão detectado e nenhum prompt definido", "Se detectó más de un flujo de audio predeterminado y no hay aviso configurado"), + ("More than one default audio stream detected! Please select stream", "Mehr als ein Standard-Audiostream erkannt! Bitte Stream auswählen", "Plus d'un flux audio par défaut détecté ! Veuillez sélectionner un flux", "デフォルト音声ストリームが複数検出されました。ストリームを選択してください", "Mer enn én standard lydstrøm funnet. Velg strøm", "Pli ol unu defaŭlta sonfluo detektita! Bonvolu elekti fluon", "ஒருக்கும் மேற்பட்ட இயல்புநிலை ஒலி ஸ்ட்ரீம்கள் கண்டறியப்பட்டன! ஸ்ட்ரீமைத் தேர்ந்தெடுக்கவும்", "Mais de um fluxo de áudio padrão detectado! Selecione o fluxo", "Se detectó más de un flujo de audio predeterminado. Selecciona el flujo"), + ("More than one default subtitle stream detected and no prompt set", "Mehr als ein Standard-Untertitelstream erkannt und keine Abfrage aktiviert", "Plus d'un flux de sous-titres par défaut détecté et aucune invite définie", "デフォルト字幕ストリームが複数検出され、プロンプトも設定されていません", "Mer enn én standard undertekststrøm funnet og ingen forespørsel satt", "Pli ol unu defaŭlta subtitola fluo detektita kaj neniu instigo agordita", "ஒருக்கும் மேற்பட்ட இயல்புநிலை வசன ஸ்ட்ரீம்கள் கண்டறியப்பட்டன, மேலும் எந்த prompt-வும் அமைக்கப்படவில்லை", "Mais de um fluxo de legenda padrão detectado e nenhum prompt definido", "Se detectó más de un flujo de subtítulos predeterminado y no hay aviso configurado"), + ("More than one default subtitle stream detected! Please select stream", "Mehr als ein Standard-Untertitelstream erkannt! Bitte Stream auswählen", "Plus d'un flux de sous-titres par défaut détecté ! Veuillez sélectionner un flux", "デフォルト字幕ストリームが複数検出されました。ストリームを選択してください", "Mer enn én standard undertekststrøm funnet. Velg strøm", "Pli ol unu defaŭlta subtitola fluo detektita! Bonvolu elekti fluon", "ஒருக்கும் மேற்பட்ட இயல்புநிலை வசன ஸ்ட்ரீம்கள் கண்டறியப்பட்டன! ஸ்ட்ரீமைத் தேர்ந்தெடுக்கவும்", "Mais de um fluxo de legenda padrão detectado! Selecione o fluxo", "Se detectó más de un flujo de subtítulos predeterminado. Selecciona el flujo"), + ("More than one default video stream detected and no prompt set", "Mehr als ein Standard-Videostream erkannt und keine Abfrage aktiviert", "Plus d'un flux vidéo par défaut détecté et aucune invite définie", "デフォルト映像ストリームが複数検出され、プロンプトも設定されていません", "Mer enn én standard videostrøm funnet og ingen forespørsel satt", "Pli ol unu defaŭlta videofluo detektita kaj neniu instigo agordita", "ஒருக்கும் மேற்பட்ட இயல்புநிலை வீடியோ ஸ்ட்ரீம்கள் கண்டறியப்பட்டன, மேலும் எந்த prompt-வும் அமைக்கப்படவில்லை", "Mais de um fluxo de vídeo padrão detectado e nenhum prompt definido", "Se detectó más de un flujo de vídeo predeterminado y no hay aviso configurado"), + ("More than one default video stream detected! Please select stream", "Mehr als ein Standard-Videostream erkannt! Bitte Stream auswählen", "Plus d'un flux vidéo par défaut détecté ! Veuillez sélectionner un flux", "デフォルト映像ストリームが複数検出されました。ストリームを選択してください", "Mer enn én standard videostrøm funnet. Velg strøm", "Pli ol unu defaŭlta videofluo detektita! Bonvolu elekti fluon", "ஒருக்கும் மேற்பட்ட இயல்புநிலை வீடியோ ஸ்ட்ரீம்கள் கண்டறியப்பட்டன! ஸ்ட்ரீமைத் தேர்ந்தெடுக்கவும்", "Mais de um fluxo de vídeo padrão detectado! Selecione o fluxo", "Se detectó más de un flujo de vídeo predeterminado. Selecciona el flujo"), + ("More than one forced audio stream detected and no prompt set", "Mehr als ein erzwungener Audiostream erkannt und keine Abfrage aktiviert", "Plus d'un flux audio forcé détecté et aucune invite définie", "強制音声ストリームが複数検出され、プロンプトも設定されていません", "Mer enn én tvungen lydstrøm funnet og ingen forespørsel satt", "Pli ol unu devigita sonfluo detektita kaj neniu instigo agordita", "ஒருக்கும் மேற்பட்ட கட்டாய ஒலி ஸ்ட்ரீம்கள் கண்டறியப்பட்டன, மேலும் எந்த prompt-வும் அமைக்கப்படவில்லை", "Mais de um fluxo de áudio forçado detectado e nenhum prompt definido", "Se detectó más de un flujo de audio forzado y no hay aviso configurado"), + ("More than one forced audio stream detected! Please select stream", "Mehr als ein erzwungener Audiostream erkannt! Bitte Stream auswählen", "Plus d'un flux audio forcé détecté ! Veuillez sélectionner un flux", "強制音声ストリームが複数検出されました。ストリームを選択してください", "Mer enn én tvungen lydstrøm funnet. Velg strøm", "Pli ol unu devigita sonfluo detektita! Bonvolu elekti fluon", "ஒருக்கும் மேற்பட்ட கட்டாய ஒலி ஸ்ட்ரீம்கள் கண்டறியப்பட்டன! ஸ்ட்ரீமைத் தேர்ந்தெடுக்கவும்", "Mais de um fluxo de áudio forçado detectado! Selecione o fluxo", "Se detectó más de un flujo de audio forzado. Selecciona el flujo"), + ("More than one forced subtitle stream detected and no prompt set", "Mehr als ein erzwungener Untertitelstream erkannt und keine Abfrage aktiviert", "Plus d'un flux de sous-titres forcé détecté et aucune invite définie", "強制字幕ストリームが複数検出され、プロンプトも設定されていません", "Mer enn én tvungen undertekststrøm funnet og ingen forespørsel satt", "Pli ol unu devigita subtitola fluo detektita kaj neniu instigo agordita", "ஒருக்கும் மேற்பட்ட கட்டாய வசன ஸ்ட்ரீம்கள் கண்டறியப்பட்டன, மேலும் எந்த prompt-வும் அமைக்கப்படவில்லை", "Mais de um fluxo de legenda forçada detectado e nenhum prompt definido", "Se detectó más de un flujo de subtítulos forzados y no hay aviso configurado"), + ("More than one forced subtitle stream detected! Please select stream", "Mehr als ein erzwungener Untertitelstream erkannt! Bitte Stream auswählen", "Plus d'un flux de sous-titres forcé détecté ! Veuillez sélectionner un flux", "強制字幕ストリームが複数検出されました。ストリームを選択してください", "Mer enn én tvungen undertekststrøm funnet. Velg strøm", "Pli ol unu devigita subtitola fluo detektita! Bonvolu elekti fluon", "ஒருக்கும் மேற்பட்ட கட்டாய வசன ஸ்ட்ரீம்கள் கண்டறியப்பட்டன! ஸ்ட்ரீமைத் தேர்ந்தெடுக்கவும்", "Mais de um fluxo de legenda forçada detectado! Selecione o fluxo", "Se detectó más de un flujo de subtítulos forzados. Selecciona el flujo"), + ("More than one forced video stream detected and no prompt set", "Mehr als ein erzwungener Videostream erkannt und keine Abfrage aktiviert", "Plus d'un flux vidéo forcé détecté et aucune invite définie", "強制映像ストリームが複数検出され、プロンプトも設定されていません", "Mer enn én tvungen videostrøm funnet og ingen forespørsel satt", "Pli ol unu devigita videofluo detektita kaj neniu instigo agordita", "ஒருக்கும் மேற்பட்ட கட்டாய வீடியோ ஸ்ட்ரீம்கள் கண்டறியப்பட்டன, மேலும் எந்த prompt-வும் அமைக்கப்படவில்லை", "Mais de um fluxo de vídeo forçado detectado e nenhum prompt definido", "Se detectó más de un flujo de vídeo forzado y no hay aviso configurado"), + ("More than one forced video stream detected! Please select stream", "Mehr als ein erzwungener Videostream erkannt! Bitte Stream auswählen", "Plus d'un flux vidéo forcé détecté ! Veuillez sélectionner un flux", "強制映像ストリームが複数検出されました。ストリームを選択してください", "Mer enn én tvungen videostrøm funnet. Velg strøm", "Pli ol unu devigita videofluo detektita! Bonvolu elekti fluon", "ஒருக்கும் மேற்பட்ட கட்டாய வீடியோ ஸ்ட்ரீம்கள் கண்டறியப்பட்டன! ஸ்ட்ரீமைத் தேர்ந்தெடுக்கவும்", "Mais de um fluxo de vídeo forçado detectado! Selecione o fluxo", "Se detectó más de un flujo de vídeo forzado. Selecciona el flujo"), + ("Name", "Name", "Nom", "名前", "Navn", "Nomo", "பெயர்", "Nome", "Nombre"), + ("New Pattern", "Neues Muster", "Nouveau modèle", "新しいパターン", "Nytt mønster", "Nova ŝablono", "புதிய வடிவம்", "Novo padrão", "Nuevo patrón"), + ("New Show", "Neue Serie", "Nouvelle série", "新しい番組", "Ny serie", "Nova serio", "புதிய தொடர்", "Nova série", "Nueva serie"), + ("New filename pattern", "Neues Dateinamensmuster", "Nouveau modèle de nom de fichier", "新しいファイル名パターン", "Nytt filnavnmønster", "Nova dosiernoma ŝablono", "புதிய கோப்பு பெயர் வடிவம்", "Novo padrão de nome de arquivo", "Nuevo patrón de nombre de archivo"), + ("New shifted season", "Neue verschobene Staffel", "Nouvelle saison décalée", "新しいシーズンシフト", "Ny forskjøvet sesong", "Nova ŝovita sezono", "புதிய மாற்றிய சீசன்", "Nova temporada deslocada", "Nueva temporada desplazada"), + ("New stream", "Neuer Stream", "Nouveau flux", "新しいストリーム", "Ny strøm", "Nova fluo", "புதிய ஸ்ட்ரீம்", "Novo fluxo", "Nuevo flujo"), + ("No", "Nein", "Non", "いいえ", "Nei", "Ne", "இல்லை", "Não", "No"), + ("No changes to apply.", "Keine Änderungen zum Anwenden.", "Aucune modification à appliquer.", "適用する変更はありません。", "Ingen endringer å bruke.", "Neniuj ŝanĝoj por apliki.", "பயன்படுத்த மாற்றங்கள் இல்லை.", "Nenhuma alteração para aplicar.", "No hay cambios para aplicar."), + ("No changes to revert.", "Keine Änderungen zum Zurücksetzen.", "Aucune modification à annuler.", "元に戻す変更はありません。", "Ingen endringer å tilbakestille.", "Neniuj ŝanĝoj por malfari.", "மீட்டெடுக்க மாற்றங்கள் இல்லை.", "Nenhuma alteração para reverter.", "No hay cambios para revertir."), + ("Normalization disabled.", "Normalisierung deaktiviert.", "Normalisation désactivée.", "正規化を無効にしました。", "Normalisering deaktivert.", "Normaligo malŝaltita.", "சீரமைப்பு முடக்கப்பட்டது.", "Normalização desativada.", "Normalización desactivada."), + ("Normalization enabled.", "Normalisierung aktiviert.", "Normalisation activée.", "正規化を有効にしました。", "Normalisering aktivert.", "Normaligo ŝaltita.", "சீரமைப்பு இயக்கப்பட்டது.", "Normalização ativada.", "Normalización activada."), + ("Normalize", "Normalisieren", "Normaliser", "正規化", "Normaliser", "Normaligi", "சீரமை", "Normalizar", "Normalizar"), + ("Notes", "Notizen", "Notes", "メモ", "Notater", "Notoj", "குறிப்புகள்", "Notas", "Notas"), + ("Pattern", "Muster", "Modèle", "パターン", "Mønster", "Ŝablono", "வடிவம்", "Padrão", "Patrón"), + ("Planned Changes (file->edited output)", "Geplante Änderungen (Datei->bearbeitete Ausgabe)", "Modifications prévues (fichier->sortie modifiée)", "予定された変更 (ファイル->編集後出力)", "Planlagte endringer (fil->redigert utdata)", "Planitaj ŝanĝoj (dosiero->redaktita eligo)", "திட்டமிட்ட மாற்றங்கள் (கோப்பு->திருத்திய வெளியீடு)", "Alterações planejadas (arquivo->saída editada)", "Cambios planificados (archivo->salida editada)"), + ("Quality", "Qualität", "Qualité", "品質", "Kvalitet", "Kvalito", "தரம்", "Qualidade", "Calidad"), + ("Quit", "Beenden", "Quitter", "終了", "Avslutt", "Eliri", "வெளியேறு", "Sair", "Salir"), + ("Remove Pattern", "Muster entfernen", "Supprimer le modèle", "パターンを削除", "Fjern mønster", "Forigi ŝablonon", "வடிவத்தை நீக்கு", "Remover padrão", "Eliminar patrón"), + ("Revert", "Zurücksetzen", "Annuler les modifications", "元に戻す", "Tilbakestill", "Malfari", "மீட்டு", "Reverter", "Revertir"), + ("Reverted pending changes.", "Ausstehende Änderungen verworfen.", "Modifications en attente annulées.", "保留中の変更を元に戻しました。", "Ventende endringer ble tilbakestilt.", "Malfaris atendatajn ŝanĝojn.", "நிலுவையில் உள்ள மாற்றங்கள் மீட்டெடுக்கப்பட்டன.", "Alterações pendentes revertidas.", "Se revirtieron los cambios pendientes."), + ("Save", "Speichern", "Enregistrer", "保存", "Lagre", "Konservi", "சேமி", "Salvar", "Guardar"), + ("Season Offset", "Staffeloffset", "Décalage de saison", "シーズンオフセット", "Sesongforskyvning", "Sezona deŝovo", "சீசன் இடச்சரிவு", "Deslocamento de temporada", "Desplazamiento de temporada"), + ("Select a stream first.", "Bitte zuerst einen Stream auswählen.", "Veuillez d'abord sélectionner un flux.", "まずストリームを選択してください。", "Velg en strøm først.", "Bonvolu unue elekti fluon.", "முதலில் ஒரு ஸ்ட்ரீமைத் தேர்ந்தெடுக்கவும்.", "Selecione um fluxo primeiro.", "Selecciona primero un flujo."), + ("Set Default", "Als Standard setzen", "Définir par défaut", "デフォルトに設定", "Sett som standard", "Agordi kiel defaŭltan", "இயல்புநிலையாக அமை", "Definir como padrão", "Establecer como predeterminado"), + ("Set Forced", "Als erzwungen setzen", "Définir comme forcé", "強制に設定", "Sett som tvungen", "Agordi kiel devigitan", "கட்டாயமாக அமை", "Definir como forçado", "Establecer como forzado"), + ("Settings Screen", "Einstellungsbildschirm", "Écran des paramètres", "設定画面", "Innstillingsskjerm", "Agorda ekrano", "அமைப்புகள் திரை", "Tela de configurações", "Pantalla de ajustes"), + ("Numbering Mapping", "Verschobene Staffeln", "Saisons décalées", "シフト済みシーズン", "Forskjøvne sesonger", "Ŝovitaj sezonoj", "மாற்றிய சீசன்கள்", "Temporadas deslocadas", "Temporadas desplazadas"), + ("Show", "Serie", "Série", "番組", "Serie", "Serio", "தொடர்", "Série", "Serie"), + ("Shows", "Serien", "Séries", "番組一覧", "Serier", "Serioj", "தொடர்கள்", "Séries", "Series"), + ("Source Season", "Quellstaffel", "Saison source", "元シーズン", "Kildesesong", "Fonta sezono", "மூல சீசன்", "Temporada de origem", "Temporada de origen"), + ("SrcIndex", "QuellIndex", "Index source", "元インデックス", "Kildeindeks", "Fontindekso", "மூலச் சுட்டி", "Índice de origem", "Índice origen"), + ("Status", "Status", "Statut", "状態", "Status", "Stato", "நிலை", "Status", "Estado"), + ("Stay", "Bleiben", "Rester", "このまま", "Bli", "Resti", "இரு", "Permanecer", "Permanecer"), + ("Stream dispositions", "Stream-Dispositionen", "Dispositions des flux", "ストリーム disposition", "Strømdisposisjoner", "Fluaj dispozicioj", "ஸ்ட்ரீம் disposition-கள்", "Disposições do fluxo", "Disposiciones del flujo"), + ("Stream tags", "Stream-Tags", "Balises du flux", "ストリームタグ", "Strømtagger", "Fluaj etikedoj", "ஸ்ட்ரீம் குறிச்சொற்கள்", "Tags do fluxo", "Etiquetas del flujo"), + ("Streams", "Streams", "Flux", "ストリーム", "Strømmer", "Fluoj", "ஸ்ட்ரீம்கள்", "Fluxos", "Flujos"), + ("SubIndex", "Unterindex", "Sous-index", "サブインデックス", "Underindeks", "Subindekso", "துணைச்சுட்டி", "Subíndice", "Subíndice"), + ("Substitute", "Ersetzen", "Remplacer", "置換", "Erstatt", "Anstataŭigi", "மாற்று", "Substituir", "Sustituir"), + ("Substitute pattern", "Muster ersetzen", "Remplacer le modèle", "パターンを置換", "Erstatt mønster", "Anstataŭigi ŝablonon", "வடிவத்தை மாற்று", "Substituir padrão", "Sustituir patrón"), + ("Title", "Titel", "Titre", "タイトル", "Tittel", "Titolo", "தலைப்பு", "Título", "Título"), + ("Type", "Typ", "Type", "タイプ", "Type", "Tipo", "வகை", "Tipo", "Tipo"), + ("Unable to update selected stream.", "Ausgewählten Stream konnte nicht aktualisiert werden.", "Impossible de mettre à jour le flux sélectionné.", "選択したストリームを更新できませんでした。", "Kunne ikke oppdatere valgt strøm.", "Ne eblis ĝisdatigi la elektitan fluon.", "தேர்ந்தெடுக்கப்பட்ட ஸ்ட்ரீமைப் புதுப்பிக்க முடியவில்லை.", "Não foi possível atualizar o fluxo selecionado.", "No se pudo actualizar el flujo seleccionado."), + ("Up", "Hoch", "Monter", "上へ", "Opp", "Supren", "மேல்", "Cima", "Arriba"), + ("Update Pattern", "Muster aktualisieren", "Mettre à jour le modèle", "パターンを更新", "Oppdater mønster", "Ĝisdatigi ŝablonon", "வடிவத்தை புதுப்பி", "Atualizar padrão", "Actualizar patrón"), + ("Updated media tag {tag!r}.", "Medien-Tag {tag!r} aktualisiert.", "Balise média {tag!r} mise à jour.", "メディアタグ {tag!r} を更新しました。", "Mediataggen {tag!r} ble oppdatert.", "Ĝisdatigis la aŭdvidan etikedon {tag!r}.", "மீடியா குறிச்சொல் {tag!r} புதுப்பிக்கப்பட்டது.", "Tag de mídia {tag!r} atualizada.", "Etiqueta de medios {tag!r} actualizada."), + ("Updated stream #{index} ({track_type}).", "Stream #{index} ({track_type}) aktualisiert.", "Flux #{index} ({track_type}) mis à jour.", "ストリーム #{index} ({track_type}) を更新しました。", "Strøm #{index} ({track_type}) oppdatert.", "Ĝisdatigis fluon #{index} ({track_type}).", "ஸ்ட்ரீம் #{index} ({track_type}) புதுப்பிக்கப்பட்டது.", "Fluxo #{index} ({track_type}) atualizado.", "Flujo #{index} ({track_type}) actualizado."), + ("Value", "Wert", "Valeur", "値", "Verdi", "Valoro", "மதிப்பு", "Valor", "Valor"), + ("Year", "Jahr", "Année", "年", "År", "Jaro", "ஆண்டு", "Ano", "Año"), + ("Yes", "Ja", "Oui", "はい", "Ja", "Jes", "ஆம்", "Sim", "Sí"), + ("add media tag: key='{key}' value='{value}'", "Medien-Tag hinzufügen: Schlüssel='{key}' Wert='{value}'", "ajouter une balise média : clé='{key}' valeur='{value}'", "メディアタグを追加: key='{key}' value='{value}'", "legg til mediatagg: nøkkel='{key}' verdi='{value}'", "aldoni aŭdvidan etikedon: ŝlosilo='{key}' valoro='{value}'", "மீடியா குறிச்சொல் சேர்: key='{key}' value='{value}'", "adicionar tag de mídia: chave='{key}' valor='{value}'", "añadir etiqueta de medios: clave='{key}' valor='{value}'"), + ("add {track_type} track: index={index} lang={language}", "{track_type}-Stream hinzufügen: Index={index} Sprache={language}", "ajouter une piste {track_type} : index={index} langue={language}", "{track_type}ストリームを追加: index={index} lang={language}", "legg til {track_type}-spor: indeks={index} språk={language}", "aldoni {track_type}-trakon: indekso={index} lingvo={language}", "{track_type} ஸ்ட்ரீம் சேர்: index={index} lang={language}", "adicionar faixa {track_type}: índice={index} idioma={language}", "añadir pista {track_type}: índice={index} idioma={language}"), + ("audio", "Audio", "audio", "音声", "lyd", "sono", "ஒலி", "áudio", "audio"), + ("attachment", "Anhang", "pièce jointe", "添付", "vedlegg", "aldonaĵo", "இணைப்பு", "anexo", "adjunto"), + ("captions", "Untertitel", "sous-titres", "キャプション", "teksting", "subtekstoj", "உரைப்பதிவுகள்", "legendas", "subtítulos"), + ("change media tag: key='{key}' value='{value}'", "Medien-Tag ändern: Schlüssel='{key}' Wert='{value}'", "modifier une balise média : clé='{key}' valeur='{value}'", "メディアタグを変更: key='{key}' value='{value}'", "endre mediatagg: nøkkel='{key}' verdi='{value}'", "ŝanĝi aŭdvidan etikedon: ŝlosilo='{key}' valoro='{value}'", "மீடியா குறிச்சொல் மாற்று: key='{key}' value='{value}'", "alterar tag de mídia: chave='{key}' valor='{value}'", "cambiar etiqueta de medios: clave='{key}' valor='{value}'"), + ("change stream #{index} ({track_type}:{sub_index}) add disposition={disposition}", "Stream #{index} ({track_type}:{sub_index}) Disposition hinzufügen={disposition}", "modifier le flux #{index} ({track_type}:{sub_index}) ajouter disposition={disposition}", "ストリーム #{index} ({track_type}:{sub_index}) disposition を追加={disposition}", "endre strøm #{index} ({track_type}:{sub_index}) legg til disposisjon={disposition}", "ŝanĝi fluon #{index} ({track_type}:{sub_index}) aldoni dispozicion={disposition}", "ஸ்ட்ரீம் #{index} ({track_type}:{sub_index}) disposition சேர்={disposition}", "alterar fluxo #{index} ({track_type}:{sub_index}) adicionar disposição={disposition}", "cambiar flujo #{index} ({track_type}:{sub_index}) añadir disposición={disposition}"), + ("change stream #{index} ({track_type}:{sub_index}) add key={key} value={value}", "Stream #{index} ({track_type}:{sub_index}) Schlüssel hinzufügen={key} Wert={value}", "modifier le flux #{index} ({track_type}:{sub_index}) ajouter clé={key} valeur={value}", "ストリーム #{index} ({track_type}:{sub_index}) key を追加={key} value={value}", "endre strøm #{index} ({track_type}:{sub_index}) legg til nøkkel={key} verdi={value}", "ŝanĝi fluon #{index} ({track_type}:{sub_index}) aldoni ŝlosilon={key} valoron={value}", "ஸ்ட்ரீம் #{index} ({track_type}:{sub_index}) key சேர்={key} value={value}", "alterar fluxo #{index} ({track_type}:{sub_index}) adicionar chave={key} valor={value}", "cambiar flujo #{index} ({track_type}:{sub_index}) añadir clave={key} valor={value}"), + ("change stream #{index} ({track_type}:{sub_index}) change key={key} value={value}", "Stream #{index} ({track_type}:{sub_index}) Schlüssel ändern={key} Wert={value}", "modifier le flux #{index} ({track_type}:{sub_index}) changer clé={key} valeur={value}", "ストリーム #{index} ({track_type}:{sub_index}) key を変更={key} value={value}", "endre strøm #{index} ({track_type}:{sub_index}) endre nøkkel={key} verdi={value}", "ŝanĝi fluon #{index} ({track_type}:{sub_index}) ŝanĝi ŝlosilon={key} valoron={value}", "ஸ்ட்ரீம் #{index} ({track_type}:{sub_index}) key மாற்று={key} value={value}", "alterar fluxo #{index} ({track_type}:{sub_index}) alterar chave={key} valor={value}", "cambiar flujo #{index} ({track_type}:{sub_index}) cambiar clave={key} valor={value}"), + ("change stream #{index} ({track_type}:{sub_index}) remove disposition={disposition}", "Stream #{index} ({track_type}:{sub_index}) Disposition entfernen={disposition}", "modifier le flux #{index} ({track_type}:{sub_index}) supprimer disposition={disposition}", "ストリーム #{index} ({track_type}:{sub_index}) disposition を削除={disposition}", "endre strøm #{index} ({track_type}:{sub_index}) fjern disposisjon={disposition}", "ŝanĝi fluon #{index} ({track_type}:{sub_index}) forigi dispozicion={disposition}", "ஸ்ட்ரீம் #{index} ({track_type}:{sub_index}) disposition நீக்கு={disposition}", "alterar fluxo #{index} ({track_type}:{sub_index}) remover disposição={disposition}", "cambiar flujo #{index} ({track_type}:{sub_index}) quitar disposición={disposition}"), + ("change stream #{index} ({track_type}:{sub_index}) remove key={key} value={value}", "Stream #{index} ({track_type}:{sub_index}) Schlüssel entfernen={key} Wert={value}", "modifier le flux #{index} ({track_type}:{sub_index}) supprimer clé={key} valeur={value}", "ストリーム #{index} ({track_type}:{sub_index}) key を削除={key} value={value}", "endre strøm #{index} ({track_type}:{sub_index}) fjern nøkkel={key} verdi={value}", "ŝanĝi fluon #{index} ({track_type}:{sub_index}) forigi ŝlosilon={key} valoron={value}", "ஸ்ட்ரீம் #{index} ({track_type}:{sub_index}) key நீக்கு={key} value={value}", "alterar fluxo #{index} ({track_type}:{sub_index}) remover chave={key} valor={value}", "cambiar flujo #{index} ({track_type}:{sub_index}) quitar clave={key} valor={value}"), + ("clean_effects", "Nur Effekte", "effets seuls", "効果音のみ", "bare effekter", "nur efektoj", "ஒலி விளைவுகள் மட்டும்", "apenas efeitos", "solo efectos"), + ("comment", "Kommentar", "commentaire", "コメント", "kommentar", "komento", "கருத்துரை", "comentário", "comentario"), + ("default", "Standard", "par défaut", "デフォルト", "standard", "defaŭlta", "இயல்புநிலை", "padrão", "predeterminado"), + ("dependent", "abhängig", "dépendant", "依存", "avhengig", "dependa", "சார்ந்த", "dependente", "dependiente"), + ("descriptions", "Beschreibungen", "descriptions", "解説", "beskrivelser", "priskriboj", "விளக்கங்கள்", "descrições", "descripciones"), + ("differences", "Unterschiede", "différences", "差分", "forskjeller", "diferencoj", "வேறுபாடுகள்", "diferenças", "diferencias"), + ("dub", "Synchronisiert", "doublage", "吹替", "dubbet", "dublado", "டப்", "dublado", "doblaje"), + ("for pattern", "für Muster", "pour le modèle", "パターン用", "for mønster", "por ŝablono", "வடிவத்திற்கு", "para o padrão", "para el patrón"), + ("forced", "erzwungen", "forcé", "強制", "tvungen", "devigita", "கட்டாயம்", "forçado", "forzado"), + ("from", "von", "de", "元", "fra", "de", "இருந்து", "de", "de"), + ("from pattern", "aus Muster", "depuis le modèle", "パターンから", "fra mønster", "de ŝablono", "வடிவத்திலிருந்து", "do padrão", "del patrón"), + ("from show", "aus Serie", "depuis la série", "番組から", "fra serie", "el serio", "தொடரிலிருந்து", "da série", "de la serie"), + ("hearing_impaired", "hörgeschädigt", "malentendants", "聴覚障害者向け", "hørselshemmet", "aŭdmalhelpita", "கேள்வித்திறன் குறைபாடு", "deficiência auditiva", "personas con discapacidad auditiva"), + ("karaoke", "Karaoke", "karaoké", "カラオケ", "karaoke", "karaokeo", "கரோக்கே", "karaokê", "karaoke"), + ("lyrics", "Liedtext", "paroles", "歌詞", "sangtekst", "kantoteksto", "பாடல்வரிகள்", "letra", "letra"), + ("metadata", "Metadaten", "métadonnées", "メタデータ", "metadata", "metadatenoj", "மெட்டாடேட்டா", "metadados", "metadatos"), + ("non_diegetic", "nicht-diegetisch", "non diégétique", "非ダイジェティック", "ikke-diegetisk", "nediĝeta", "அல்லாத-டைஜெடிக்", "não diegético", "no diegético"), + ("original", "Original", "original", "オリジナル", "original", "originala", "மூலம்", "original", "original"), + ("pattern #{id}", "Muster #{id}", "modèle #{id}", "パターン #{id}", "mønster #{id}", "ŝablono #{id}", "வடிவு #{id}", "padrão #{id}", "patrón #{id}"), + ("remove media tag: key='{key}' value='{value}'", "Medien-Tag entfernen: Schlüssel='{key}' Wert='{value}'", "supprimer une balise média : clé='{key}' valeur='{value}'", "メディアタグを削除: key='{key}' value='{value}'", "fjern mediatagg: nøkkel='{key}' verdi='{value}'", "forigi aŭdvidan etikedon: ŝlosilo='{key}' valoro='{value}'", "மீடியா குறிச்சொல் நீக்கு: key='{key}' value='{value}'", "remover tag de mídia: chave='{key}' valor='{value}'", "eliminar etiqueta de medios: clave='{key}' valor='{value}'"), + ("remove stream #{index}", "Stream #{index} entfernen", "supprimer le flux #{index}", "ストリーム #{index} を削除", "fjern strøm #{index}", "forigi fluon #{index}", "ஸ்ட்ரீம் #{index} நீக்கு", "remover fluxo #{index}", "eliminar flujo #{index}"), + ("show #{id}", "Serie #{id}", "série #{id}", "番組 #{id}", "serie #{id}", "serio #{id}", "தொடர் #{id}", "série #{id}", "serie #{id}"), + ("stereo", "Stereo", "stéréo", "ステレオ", "stereo", "stereo", "ஸ்டீரியோ", "estéreo", "estéreo"), + ("still_image", "Standbild", "image fixe", "静止画", "stillbilde", "senmova bildo", "நிலைப்படம்", "imagem estática", "imagen fija"), + ("sub index", "Unterindex", "sous-index", "サブインデックス", "underindeks", "subindekso", "துணைச்சுட்டி", "subíndice", "subíndice"), + ("subtitle", "Untertitel", "sous-titre", "字幕", "undertekst", "subtitolo", "வசனம்", "legenda", "subtítulo"), + ("timed_thumbnails", "zeitgesteuerte Vorschaubilder", "miniatures horodatées", "時間指定サムネイル", "tidsbestemte miniatyrer", "tempigitaj bildetoj", "நேர நிர்ணய சிறுபடங்கள்", "miniaturas temporizadas", "miniaturas temporizadas"), + ("undefined", "undefiniert", "indéfini", "未定義", "udefinert", "nedifinita", "வரையறுக்கப்படாத", "indefinido", "indefinido"), + ("unknown", "unbekannt", "inconnu", "不明", "ukjent", "nekonata", "தெரியாத", "desconhecido", "desconocido"), + ("video", "Video", "vidéo", "映像", "video", "video", "வீடியோ", "vídeo", "vídeo"), + ("visual_impaired", "sehgeschädigt", "malvoyants", "視覚障害者向け", "synshemmet", "vidmalhelpita", "பார்வைத்திறன் குறைபாடு", "deficiência visual", "personas con discapacidad visual"), +] + + +def extract_source_phrases() -> set[str]: + phrases: set[str] = set() + for path in SOURCE_ROOT.rglob("*.py"): + tree = ast.parse(path.read_text(encoding="utf-8")) + for node in ast.walk(tree): + if not isinstance(node, ast.Call) or not node.args: + continue + func = node.func + func_name = ( + func.id if isinstance(func, ast.Name) else func.attr if isinstance(func, ast.Attribute) else None + ) + if func_name != "t": + continue + first_arg = node.args[0] + if isinstance(first_arg, ast.Constant) and isinstance(first_arg.value, str): + phrases.add(first_arg.value) + return phrases + + +def build_translation_rows() -> dict[str, dict[str, str]]: + rows: dict[str, dict[str, str]] = {} + for row in PHRASE_ROWS: + source_phrase = row[0] + rows[source_phrase] = { + language_code: translated + for language_code, translated in zip(TRANSLATED_LANGUAGE_CODES, row[1:]) + } + return rows + + +def build_iso_language_catalog(language_code: str) -> dict[str, str]: + if language_code == "en": + return { + language.name: str(language.value["name"]) + for language in IsoLanguage + } + + translation = gettext.translation("iso_639", languages=[language_code], fallback=True) + iso_catalog: dict[str, str] = {} + for language in IsoLanguage: + english_name = str(language.value["name"]) + translated = translation.gettext(english_name) + iso_catalog[language.name] = translated if translated else english_name + return iso_catalog + + +def main() -> None: + source_phrases = extract_source_phrases() | EXTRA_PHRASES + row_translations = build_translation_rows() + OUTPUT_ROOT.mkdir(parents=True, exist_ok=True) + + for language_code in LANGUAGE_CODES: + if language_code not in SUPPORTED_LANGUAGES: + raise ValueError(f"Unsupported language code: {language_code}") + + phrases_catalog = { + phrase: ( + row_translations.get(phrase, {}).get(language_code, phrase) + if language_code != "en" + else phrase + ) + for phrase in sorted(source_phrases) + } + catalog = { + "phrases": phrases_catalog, + "iso_languages": build_iso_language_catalog(language_code), + } + (OUTPUT_ROOT / f"{language_code}.json").write_text( + json.dumps(catalog, ensure_ascii=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + + +if __name__ == "__main__": + main()