From 3393f1a63ec5dd5aa41225bb17d06ac10b22f215 Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Thu, 8 Oct 2026 21:38:06 +0800 Subject: [PATCH] feat: add best-effort PO language to Inno Setup language name conversion --- DEVNOTE.md | 7 +- doc/render-context/inno.md | 52 +++++--- example/example.iss.liquid | 14 +- src/metaglot/cmds/render/inno.py | 92 +++++++++---- src/metaglot/langid.py | 62 +++++++++ src/metaglot/langmap/inno_lang_map.py | 178 ++++++++++++++++++++++++++ 6 files changed, 353 insertions(+), 52 deletions(-) create mode 100644 src/metaglot/langmap/inno_lang_map.py diff --git a/DEVNOTE.md b/DEVNOTE.md index 797d3ba..e4755e2 100644 --- a/DEVNOTE.md +++ b/DEVNOTE.md @@ -61,7 +61,12 @@ InnoSetup installation involves multiple file formats. All formats of them can b Based on these document, we create corresponding Liquid filters for both files, since they share the same message value format. -Unlike the other targets, InnoSetup has no standard representation of language identifiers: both the internal language names declared in the `[Languages]` section and the message file names are chosen freely by the packager, as its official document says. So there is no language representation or convertion code for this target. The PO language tag carries over verbatim, and the pairing between a language and its base `.isl` file is expressed explicitly by the template author. +Unlike the other targets, InnoSetup has no standard representation of language identifiers: both the internal language names declared in the `[Languages]` section and the message file names are chosen freely by the packager, as its official document says. So there is no normative conversion for this target - the PO language tag carries over verbatim as the canonical identity. What MetaGlot adds on top is a best-effort conversion to the conventional language file names distributed by the InnoSetup project. We transcribe the name list from its [unofficial translations page](https://jrsoftware.org/files/istrans/), which covers both the official translations bundled with the compiler and the community provided ones (including the outdated ones). The list was fetched on **2026-10-08** and reflects Inno Setup **6.5.0+**. The names are represented by `InnoLang`, which constrains them to ASCII letters and spaces. Notes kept during the transcription: + +- The conventional name is the file base name, not the `LanguageName` field inside the file: `ChineseSimplified` vs `Chinese (Simplified)`. +- English has no dedicated file: `en` converts to `Default`, because `Default.isl` sits at the compiler root, not in the `Languages` subfolder. +- Seven legacy community files ship with a `.islu` extension (Bengali, Hindi, Kazakh, Marathi, Nepali, Sinhala, Uyghur); the table records base names only. +- The pairing between a language and its base `.isl` file remains knowledge of the template author for languages outside the transcription table (the conversion yields nothing, and the PO language tag is the fallback). ## Bump Version Up diff --git a/doc/render-context/inno.md b/doc/render-context/inno.md index 9ac0002..3a97f08 100644 --- a/doc/render-context/inno.md +++ b/doc/render-context/inno.md @@ -10,31 +10,39 @@ Every component below renders with its **own environment**, so the filters avail ## Language Identification -Inno Setup has **no standard representation of language identifiers**: the `[Languages]` internal `Name:` and the message file names are both chosen freely by the packager. MetaGlot therefore performs **no conversion** for this target - the PO language tag (`zh_CN`, `en`) carries over verbatim, and the same tag serves as the single join key across everything the pipeline generates: +Inno Setup has **no standard representation of language identifiers**: the `[Languages]` internal `Name:` and the message file names are both chosen freely by the packager. What does exist is a set of **conventional file names** — the base names of the messages files distributed by the Inno Setup project itself, such as `ChineseSimplified` or `Japanese`. -- the `Name:` value of a `[Languages]` entry, -- the incremental `.isl` file name (e.g. `Strings\zh_CN.isl`), -- and the matching branch of the base file pairing in the `.iss` template (see below). +MetaGlot therefore exposes **two** language identifiers per language: -The pairing between a language and its base `.isl` - the official full translation whose `[LangOptions]` provides the language identity (the Select Language dialog name and the auto-detection identifier) - is knowledge of the template author and is expressed explicitly, for example: +- `name` — the PO language tag (`zh_CN`, `en`), carried over verbatim. It is the canonical join key across everything the pipeline generates: the `Name:` value of a `[Languages]` entry, the language ordering, and the fallback identifier when the conversion below fails. +- `lang` — the best-effort conversion of the PO tag to a conventional Inno Setup language name (`zh_CN` → `ChineseSimplified`, `ja`/`ja_JP` → `Japanese`). It is `null` when no conventional name is known for the language; handle those languages with `{{ lang | default: name }}`, or with a `case`/`when` block on `name`. + +The conversion table transcribes the official translations bundled with Inno Setup together with the community translations listed on the [unofficial translations page](https://jrsoftware.org/files/istrans/) — including the outdated ones — so adding a new language with a known name requires no template edits. Country-sensitive languages are disambiguated: `zh`/`zh_SG` → `ChineseSimplified` but `zh_TW`/`zh_HK`/`zh_MO` → `ChineseTraditional`; `pt` → `Portuguese` but `pt_BR` → `BrazilianPortuguese`; `en` → `Default` but `en_GB` → `EnglishBritish`; `no`/`nb` → `Norwegian` but `nn` → `NorwegianNynorsk`; `sr` → `SerbianCyrillic` but `sr_RS@latin` → `SerbianLatin`; `ca_ES@valencia` → `Valencian`. + +Caveats: + +- English has no dedicated file: `en` converts to `Default`, because `Default.isl` lives at the compiler root instead of the `Languages` subfolder. +- The converted name is the file **base name** (`ChineseSimplified`), not the `LanguageName` field inside the file (`Chinese (Simplified)`). +- Seven legacy community files ship with a `.islu` extension (Bengali, Hindi, Kazakh, Marathi, Nepali, Sinhala, Uyghur); the conversion returns base names only, and the extension is the template author's concern. +- Outdated community files predate the current Inno Setup message set; they still load, but their missing messages fall back to English. + +With the conversion available, the base file pairing between a language and its base `.isl` — the official full translation whose `[LangOptions]` provides the language identity (the Select Language dialog name and the auto-detection identifier) — shrinks to one special case, the location of `Default.isl`: ```liquid -{% for lang in langs %} -{% case lang %} -{% when 'zh_CN' %} -{% assign base = 'compiler:Languages\ChineseSimplified.isl' %} -{% when 'ja' %} -{% assign base = 'compiler:Languages\Japanese.isl' %} -{% else %} -{% assign base = 'compiler:Default.isl' %} -{% endcase %} -Name: "{{ lang }}"; MessagesFile: "{{ base }},Strings\{{ lang }}.isl" -{% endfor %} +{% for lang in langs -%} +{%- if lang.lang and lang.lang != 'Default' -%} +{% assign base = 'compiler:Languages\' | append: lang.lang | append: '.isl' -%} +{%- else -%} +{% assign base = 'compiler:Default.isl' -%} +{%- endif -%} +{% assign incremental = lang.lang | default: lang.name -%} +Name: "{{ lang.name }}"; MessagesFile: "{{ base }},Strings\{{ incremental }}.isl" +{% endfor -%} ``` -The multi-line decision block renders some blank lines into the INI output, which is harmless; Liquid's whitespace control markers (`{%-`/`-%}`) can remove them once used consistently across templates. +Languages whose `lang` is `null` fall to the `else` base (usually `compiler:Default.isl`) in the snippet above and inherit the English language identity. Keep that fallback (or a `case`/`when` on `name`) aligned with the languages you ship PO files for, and review the rendered `[Languages]` section after adding a new language. -Keep the `case` branches aligned with the languages you ship PO files for: an unmatched language falls to the `{% else %}` base (usually `compiler:Default.isl`) and inherits the English language identity. Review the rendered `[Languages]` section after adding a new language. +The whitespace control markers (`{%-`/`-%}`) keep the decision block from rendering blank lines into the INI output; use them consistently across the block. ## Path Template Contexts @@ -52,12 +60,13 @@ The template is rendered once. Referencing any variable fails the render. | Variable | Type | Description | |---|---|---| -| `lang` | string | The PO language tag of the `.isl` messages file being placed, e.g. `en`, `zh_CN`. | +| `lang` | string or null | The conventional Inno Setup language name of the `.isl` messages file being placed, e.g. `ChineseSimplified`; `null` when no conventional name exists. | +| `name` | string | The PO language tag of the `.isl` messages file being placed, e.g. `en`, `zh_CN`. | The template is rendered once per language, for example: ```liquid -Strings/{{ lang }}.isl +Strings/{{ lang | default: name }}.isl ``` ## File Render Contexts @@ -69,7 +78,7 @@ The `--iss-template` and `--isl-template` arguments point to template files. Bes | Variable | Type | Description | |---|---|---| | `default` | language | The manifest's source language entry, see below. | -| `langs` | array of string | The PO language tag of every language, source language first. | +| `langs` | array of language | Every language, source language first. Each entry exposes `name` (the PO language tag) and `lang` (the conventional Inno Setup language name, or null). | The `.iss` script references the localized text through `{cm:...}` constants (e.g. `AppName={cm:app_name}`) and enumerates `[Languages]` from `langs` with the base file pairing shown above. `default` is available for values written directly instead of via constants: @@ -86,6 +95,7 @@ The members of the current language are exposed at the top level. The structure | Variable | Type | Description | |---|---|---| | `name` | string | The PO language tag of this `.isl` messages file, e.g. `en_US`, `zh_CN`. | +| `lang` | string or null | The conventional Inno Setup language name of this `.isl` messages file, e.g. `Japanese`; `null` when no conventional name exists. | | `strings` | mapping of string to string | Maps every manifest entry key to its resolved text, e.g. `strings.app_name`. | The resolved text of an entry falls back to the manifest source string when the entry is untranslated, missing from the PO file, or marked fuzzy. The template is rendered once per language. diff --git a/example/example.iss.liquid b/example/example.iss.liquid index d059b1d..6378749 100644 --- a/example/example.iss.liquid +++ b/example/example.iss.liquid @@ -10,15 +10,13 @@ SolidCompression=yes [Languages] {% for lang in langs -%} -{% case lang -%} -{% when 'zh_CN' -%} -{% assign base = 'compiler:Languages\ChineseSimplified.isl' -%} -{% when 'ja' -%} -{% assign base = 'compiler:Languages\Japanese.isl' -%} -{% else -%} +{%- if lang.lang and lang.lang != 'Default' -%} +{% assign base = 'compiler:Languages\' | append: lang.lang | append: '.isl' -%} +{%- else -%} {% assign base = 'compiler:Default.isl' -%} -{% endcase -%} -Name: "{{ lang }}"; MessagesFile: "{{ base }},Strings\{{ lang }}.isl" +{%- endif -%} +{% assign incremental = lang.lang | default: lang.name -%} +Name: "{{ lang.name }}"; MessagesFile: "{{ base }},Strings\{{ incremental }}.isl" {% endfor -%} [Tasks] Name: "desktopicon"; Description: "{cm:task_desktop_icon}" diff --git a/src/metaglot/cmds/render/inno.py b/src/metaglot/cmds/render/inno.py index 09f31c5..9b303e9 100644 --- a/src/metaglot/cmds/render/inno.py +++ b/src/metaglot/cmds/render/inno.py @@ -2,8 +2,9 @@ import logging from argparse import ArgumentParser from dataclasses import dataclass from pathlib import Path -from typing import Any +from typing import Any, Optional from ...filters import register_general_filters, register_inno_filters +from ...langmap import inno_lang_map from ...manifest import load_manifest from ...pofile import ( LanguagePack, @@ -164,25 +165,63 @@ class _InnoIslPathContext: """The Liquid-facing context of one .isl path template rendering. It is rendered once per language, with the PO language tag of the - messages file being placed exposed as ``lang``. Inno Setup has no - standard representation of language identifiers, so no conversion - happens: the tag is the Gettext PO tag verbatim. + messages file being placed exposed as ``name`` and the best-effort + conventional Inno Setup language name exposed as ``lang``. The + conversion is not normative and may yield ``None``, so path templates + typically render ``{{ lang | default: name }}``. """ - __lang: str + __name: str """The PO language tag of the .isl messages file being placed.""" + __lang: Optional[str] + """The conventional Inno Setup language name, or None if unknown.""" - def __init__(self, lang: str): - self.__lang = lang + def __init__(self, pack: LanguagePack): + inno = inno_lang_map.convert(pack.lang) + self.__name = pack.lang.value + self.__lang = inno.value if inno is not None else None def keys(self) -> tuple[str, ...]: """Return the names of the top-level template variables.""" - return ("lang",) + return ("lang", "name") def __getitem__(self, key: str) -> object: - if key == "lang": - return self.__lang - raise KeyError(key) + match key: + case "lang": + return self.__lang + case "name": + return self.__name + case _: + raise KeyError(key) + + +class _InnoLanguageView: + """The Liquid-facing view of one language inside the ``langs`` array of + the .iss template context. + + It exposes the PO language tag as ``name`` and the best-effort + conventional Inno Setup language name as ``lang`` (``None`` when no + conventional name exists). + """ + + __name: str + """The PO language tag.""" + __lang: Optional[str] + """The conventional Inno Setup language name, or None if unknown.""" + + def __init__(self, pack: LanguagePack): + inno = inno_lang_map.convert(pack.lang) + self.__name = pack.lang.value + self.__lang = inno.value if inno is not None else None + + def __getitem__(self, key: str) -> object: + match key: + case "name": + return self.__name + case "lang": + return self.__lang + case _: + raise KeyError(key) class _InnoIslTemplateContext: @@ -199,19 +238,25 @@ class _InnoIslTemplateContext: """The wrapped language pack.""" __strings: _InnoStringsView """The view of the pack's translations.""" + __lang: Optional[str] + """The conventional Inno Setup language name, or None if unknown.""" def __init__(self, pack: LanguagePack): self.__pack = pack self.__strings = _InnoStringsView(pack) + inno = inno_lang_map.convert(pack.lang) + self.__lang = inno.value if inno is not None else None def keys(self) -> tuple[str, ...]: """Return the names of the exposed template variables.""" - return ("name", "strings") + return ("name", "lang", "strings") def __getitem__(self, key: str) -> object: match key: case "name": return self.__pack.lang.value + case "lang": + return self.__lang case "strings": return self.__strings case _: @@ -224,12 +269,12 @@ class _InnoIssTemplateContext: __default: _InnoIslTemplateContext """The view of the manifest's source language.""" - __langs: tuple[str, ...] - """The PO language tags of every language, source language first.""" + __langs: tuple[_InnoLanguageView, ...] + """The views of every language, source language first.""" - def __init__(self, source: LanguagePack, langs: tuple[str, ...]): + def __init__(self, source: LanguagePack, langs: tuple[LanguagePack, ...]): self.__default = _InnoIslTemplateContext(source) - self.__langs = langs + self.__langs = tuple(_InnoLanguageView(pack) for pack in langs) def keys(self) -> tuple[str, ...]: """Return the names of the top-level template variables.""" @@ -256,9 +301,12 @@ def run(opts: InnoRenderOpts) -> None: source = build_default_pack(manifest, manifest.source_language) ordered = [source, *packs.values()] # Inno Setup has no standard language identifier representation, so - # the PO language tags carry over verbatim. - langs = tuple(pack.lang.value for pack in ordered) - logging.info("rendering languages: %s", ", ".join(langs)) + # the PO language tags carry over verbatim as the canonical identity. + # The conventional Inno Setup language names are derived per language + # by the context views, best effort. + logging.info( + "rendering languages: %s", ", ".join(str(pack.lang) for pack in ordered) + ) # Every component renders with its own environment, so their filters # never interfere with each other. @@ -279,13 +327,13 @@ def run(opts: InnoRenderOpts) -> None: render( iss_template_env, opts.in_iss_template, - _InnoIssTemplateContext(source, langs), + _InnoIssTemplateContext(source, tuple(ordered)), iss_out, ) - for pack, lang in zip(ordered, langs): + for pack in ordered: isl_out = opts.out_dir / render_path( - isl_path_env, opts.in_isl_path, _InnoIslPathContext(lang) + isl_path_env, opts.in_isl_path, _InnoIslPathContext(pack) ) render( isl_template_env, diff --git a/src/metaglot/langid.py b/src/metaglot/langid.py index d0bdd31..6805231 100644 --- a/src/metaglot/langid.py +++ b/src/metaglot/langid.py @@ -470,3 +470,65 @@ class WinLcid: return hash(self.__value) # endregion + + +class InnoLang: + """ + Represents the human-readable name of an Inno Setup language, i.e. the + conventional base name of a messages (``.isl``) file such as ``Japanese`` + or ``ChineseSimplified``. + + Inno Setup itself defines no grammar for these names: the official and + community translations simply use CamelCase English words. This class + constrains them conservatively: a non-empty run of ASCII letters and + spaces that starts with a letter. + + Examples: + InnoLang("Japanese") # valid + InnoLang("Chinese Simplified") # valid (spaces allowed) + InnoLang(" Japanese") # invalid (must start with a letter) + InnoLang("") # invalid (empty) + InnoLang("日本語") # invalid (non-ASCII characters) + """ + + __PATTERN: ClassVar[re.Pattern] = re.compile(r"^[A-Za-z][A-Za-z ]*$") + + __value: str + """The language name.""" + + def __init__(self, value: str): + if not self.__PATTERN.match(value): + raise ValueError( + f"Invalid Inno Setup language name: {value!r}. " + f"Expected only ASCII letters and spaces" + ) + self.__value = value + + # region: Properties + + @property + def value(self) -> str: + """The language name.""" + return self.__value + + # endregion + + # region: Object methods + + def __str__(self) -> str: + return self.__value + + def __repr__(self) -> str: + return f"InnoLang({self.__value!r})" + + def __eq__(self, other) -> bool: + if self is other: + return True + if not isinstance(other, InnoLang): + return NotImplemented + return self.__value == other.__value + + def __hash__(self) -> int: + return hash(self.__value) + + # endregion diff --git a/src/metaglot/langmap/inno_lang_map.py b/src/metaglot/langmap/inno_lang_map.py new file mode 100644 index 0000000..85ad5be --- /dev/null +++ b/src/metaglot/langmap/inno_lang_map.py @@ -0,0 +1,178 @@ +from ..langid import InnoLang, PoLang + +# YYC MARK: +# Inno Setup language files have no standardized names: both the internal +# names of the [Languages] section and the messages file names are chosen +# freely by the packager. The table below transcribes the *conventional* +# base names of the messages files distributed by the Inno Setup project +# itself, so a PO language can be paired with them without template-side +# case blocks. +# +# The official translations are bundled with Inno Setup (in the compiler's +# 'Languages' subfolder), and the unofficial ones are community maintained. +# Both lists were fetched from https://jrsoftware.org/files/istrans/ on +# 2026-10-08, reflecting Inno Setup 6.5.0+. +# +# The table is authored name-oriented (_NAME_KEYS below, one row per +# conventional name holding every PO language key converting to it) and +# the key-oriented lookup used at runtime (_INNO_NAMES) is derived from +# it, so a name shared by several keys - like ChineseSimplified ('zh', +# 'zh-SG') - is declared exactly once. The keys are hyphenated PO language +# components ('ll', 'll-CC', or variant-aware forms). Keys needing country +# disambiguation are spelled out; every other language is reachable +# through the bare 'll' fallback. Outdated unofficial translations are +# still transcribed - an outdated base file is better than an English-only +# installer - with their minimum supported Inno Setup version noted in a +# trailing comment. +# +# Transcription notes: +# - English has no dedicated file: 'Default.isl' lives at the compiler +# root, not in the 'Languages' subfolder, so 'en' maps to 'Default'. +# - The conventional name is the file BASE name, not the 'LanguageName' +# field inside the file ('ChineseSimplified' vs 'Chinese (Simplified)'). +# - Seven legacy unofficial files ship with a '.islu' extension (Bengali, +# Hindi, Kazakh, Marathi, Nepali, Sinhala, Uyghur); the table records +# base names only, and the extension is the template author's concern. + +# fmt: off +_NAME_KEYS: dict[InnoLang, tuple[str, ...]] = { + # Official translations (bundled with Inno Setup). + InnoLang("Default"): ("en",), # compiler:Default.isl, at the compiler root. + InnoLang("Arabic"): ("ar",), + InnoLang("Armenian"): ("hy",), + InnoLang("Bulgarian"): ("bg",), + InnoLang("Catalan"): ("ca",), + InnoLang("ChineseSimplified"): ("zh", "zh-SG"), + InnoLang("ChineseTraditional"): ("zh-TW", "zh-HK", "zh-MO"), + InnoLang("Corsican"): ("co",), + InnoLang("Czech"): ("cs",), + InnoLang("Danish"): ("da",), + InnoLang("Dutch"): ("nl",), + InnoLang("Finnish"): ("fi",), + InnoLang("French"): ("fr",), + InnoLang("German"): ("de",), + InnoLang("Hebrew"): ("he",), + InnoLang("Hungarian"): ("hu",), + InnoLang("Icelandic"): ("is",), + InnoLang("Italian"): ("it",), + InnoLang("Japanese"): ("ja",), + InnoLang("Korean"): ("ko",), + InnoLang("Lithuanian"): ("lt",), + InnoLang("Norwegian"): ("no", "nb"), + InnoLang("Polish"): ("pl",), + InnoLang("Portuguese"): ("pt",), + InnoLang("BrazilianPortuguese"): ("pt-BR",), + InnoLang("Russian"): ("ru",), + InnoLang("Slovak"): ("sk",), + InnoLang("Slovenian"): ("sl",), + InnoLang("Spanish"): ("es",), + InnoLang("Swedish"): ("sv",), + InnoLang("Tamil"): ("ta",), + InnoLang("Thai"): ("th",), + InnoLang("Turkish"): ("tr",), + InnoLang("Ukrainian"): ("uk",), + # Unofficial translations, up to date with 6.5.0+. + InnoLang("Afrikaans"): ("af",), + InnoLang("Basque"): ("eu",), + InnoLang("Croatian"): ("hr",), + InnoLang("EnglishBritish"): ("en-GB",), + InnoLang("Galician"): ("gl",), + InnoLang("Greek"): ("el",), + InnoLang("Esperanto"): ("eo",), + InnoLang("Indonesian"): ("id",), + InnoLang("Farsi"): ("fa",), + InnoLang("Vietnamese"): ("vi",), + InnoLang("SerbianCyrillic"): ("sr",), # Cyrillic is the default script. + InnoLang("SerbianLatin"): ("sr-Latn",), # e.g. sr_RS@latin. + # Unofficial translations, outdated for 6.5.0. Minimum versions noted. + InnoLang("Abkhazian"): ("ab",), # outdated: 6.4.0+ + InnoLang("Albanian"): ("sq",), # outdated: 6.4.0+ + InnoLang("Azerbaijan"): ("az",), # outdated: 6.4.0+ + InnoLang("Bosnian"): ("bs",), # outdated: 6.4.0+ + InnoLang("Ewe"): ("ee",), # outdated: 6.4.0+ + InnoLang("Belarusian"): ("be",), # outdated: 6.1.0+ + InnoLang("Georgian"): ("ka",), # outdated: 6.1.0+ + InnoLang("Latvian"): ("lv",), # outdated: 6.1.0+ + InnoLang("Montenegrin"): ("cnr",), # outdated: 6.1.0+ + InnoLang("Romanian"): ("ro",), # outdated: 6.1.0+ + InnoLang("Urdu"): ("ur",), # outdated: 6.1.0+ + InnoLang("Uyghur"): ("ug",), # outdated: 6.1.0+, ships as .islu + InnoLang("ScottishGaelic"): ("gd",), # outdated: 6.0.0+ + InnoLang("Bengali"): ("bn",), # outdated: 5.5.3+, ships as .islu + InnoLang("Estonian"): ("et",), # outdated: 5.5.3+ + InnoLang("Hindi"): ("hi",), # outdated: 5.5.3+, ships as .islu + InnoLang("Marathi"): ("mr",), # outdated: 5.5.3+, ships as .islu + InnoLang("Nepali"): ("ne",), # outdated: 5.5.3+, ships as .islu + InnoLang("Sinhala"): ("si",), # outdated: 5.5.3+, ships as .islu + InnoLang("Uzbek"): ("uz",), # outdated: 5.5.3+ + InnoLang("Valencian"): ("ca-ES-valencia",), # outdated: 5.5.3+ + InnoLang("Kazakh"): ("kk",), # outdated: 5.5.0+, ships as .islu + InnoLang("Kurdish"): ("ku",), # outdated: 5.5.0+ + InnoLang("Tatar"): ("tt",), # outdated: 5.5.0+ + InnoLang("Ligurian"): ("lij",), # outdated: 5.1.11+ + InnoLang("Luxemburgish"): ("lb",), # outdated: 5.1.11+ + InnoLang("Macedonian"): ("mk",), # outdated: 5.1.11+ + InnoLang("Mongolian"): ("mn",), # outdated: 5.1.11+ + InnoLang("Occitan"): ("oc",), # outdated: 5.1.11+ + InnoLang("Malaysian"): ("ms",), # outdated: 5.1.0+ + InnoLang("Asturian"): ("ast",), # outdated: 4.0.5+ + InnoLang("NorwegianNynorsk"): ("nn",), # outdated: 4.0.5+ +} +# fmt: on + +# The key-oriented lookup table used at runtime, derived from _NAME_KEYS. +# The duplicate check makes a transcription mistake - the same PO language +# key attached to two names - fail at import time instead of silently +# overriding in the comprehension below. +_INNO_NAMES: dict[str, InnoLang] = { + key: name for name, keys in _NAME_KEYS.items() for key in keys +} +if len(_INNO_NAMES) != sum(len(keys) for keys in _NAME_KEYS.values()): + raise ValueError("duplicate PO language key(s) in the Inno Setup name table") + + +# Gettext-style script designator words mapped to ISO 15924 script codes. +# This is not the full ISO 15924 table - it only contains the words that can +# hit entries in _INNO_NAMES above. Extend it as needed. +_SCRIPT_WORDS: dict[str, str] = { + "latin": "Latn", + "cyrillic": "Cyrl", + "arabic": "Arab", +} + + +def convert(lang: PoLang) -> InnoLang | None: + """ + Convert a PO file language to a conventional Inno Setup language name. + + The conversion is a best effort: the language, country and variant + components are hyphenated into candidate keys, variant-aware forms + first (the PO variant slot may hold a script designator, interpreted + via the script word table), then the country form, then the bare + language form. The first candidate hitting the transcription table + wins. Unlike the other targets, no exception is ever raised - a + language without a conventional name converts to ``None``, and the + caller falls back to the PO language tag. + + :param lang: The PO file language. + :return: The conventional Inno Setup language name, or ``None`` if no + conventional name exists for the language. + """ + candidates: list[str] = [] + if lang.variant is not None: + script = _SCRIPT_WORDS.get(lang.variant) + if script is not None: + # Script interpretation: 'sr_RS@latin' -> 'sr-Latn-RS'. + candidates.append(f"{lang.language}-{script}-{lang.country}") + candidates.append(f"{lang.language}-{script}") + else: + # Variant interpretation: 'ca_ES@valencia' -> 'ca-ES-valencia'. + candidates.append(f"{lang.language}-{lang.country}-{lang.variant}") + candidates.append(f"{lang.language}-{lang.variant}") + if lang.country is not None: + candidates.append(f"{lang.language}-{lang.country}") + candidates.append(lang.language) + for candidate in candidates: + if candidate in _INNO_NAMES: + return _INNO_NAMES[candidate] + return None