feat: expose Inno Setup language name officiality as is_official

This commit is contained in:
yyc12345 committed 2026-10-12 17:33:57 +08:00
1 parent 3393f1a63e
commit 574f112957
4 files changed
+122 -51

No files matched your search

+13 -5
View File
@@ -12,10 +12,11 @@ Every component below renders with its **own environment**, so the filters avail
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`.
MetaGlot therefore exposes **two** language identifiers per language:
MetaGlot therefore exposes **two** language identifiers and **one** provenance flag per language:
- `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`.
- `is_official` — whether `lang` belongs to an official translation bundled with Inno Setup (`true`) or to a community provided translation (`false`). It is also `false` when `lang` is `null`, since there is no official file to reference then. Only the official files come with an Inno Setup installation; the community files must be downloaded from the [translations page](https://jrsoftware.org/files/istrans/) and shipped with the installer yourself — or the base file falls back to `compiler:Default.isl` for users with a bare Inno Setup installation.
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`.
@@ -26,13 +27,18 @@ Caveats:
- 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`:
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) — keeps two special cases: the location of `Default.isl`, and the official-only fallback for community translations:
```liquid
{% for lang in langs -%}
{%- if lang.lang and lang.lang != 'Default' -%}
{%- if lang.lang == 'Default' -%}
{% assign base = 'compiler:Default.isl' -%}
{%- elsif lang.is_official -%}
{% assign base = 'compiler:Languages\' | append: lang.lang | append: '.isl' -%}
{%- else -%}
{%- comment -%}
unofficial or unknown: not bundled with Inno Setup, fall back for bare installs
{%- endcomment -%}
{% assign base = 'compiler:Default.isl' -%}
{%- endif -%}
{% assign incremental = lang.lang | default: lang.name -%}
@@ -40,7 +46,7 @@ Name: "{{ lang.name }}"; MessagesFile: "{{ base }},Strings\{{ incremental }}.isl
{% endfor -%}
```
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.
The `else` branch serves languages whose `lang` is `null` or unofficial: they fall back to `compiler:Default.isl` and inherit the English language identity. If you prefer keeping the community localization, download its `.isl` from the translations page, ship it next to your installer, and reference it in that branch instead. Keep the fallback aligned with the languages you ship PO files for, and 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.
@@ -62,6 +68,7 @@ The template is rendered once. Referencing any variable fails the render.
|---|---|---|
| `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`. |
| `is_official` | boolean | Whether the conventional name belongs to an official translation bundled with Inno Setup; `false` for community provided names and when `lang` is `null`. |
The template is rendered once per language, for example:
@@ -78,7 +85,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 language | Every language, source language first. Each entry exposes `name` (the PO language tag) and `lang` (the conventional Inno Setup language name, or null). |
| `langs` | array of language | Every language, source language first. Each entry exposes `name` (the PO language tag), `lang` (the conventional Inno Setup language name, or null) and `is_official` (whether the name belongs to an official translation). |
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:
@@ -96,6 +103,7 @@ The members of the current language are exposed at the top level. The structure
|---|---|---|
| `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. |
| `is_official` | boolean | Whether the conventional name belongs to an official translation bundled with Inno Setup; `false` for community provided names and when `lang` is `null`. |
| `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.
+6 -1
View File
@@ -10,9 +10,14 @@ SolidCompression=yes
[Languages]
{% for lang in langs -%}
{%- if lang.lang and lang.lang != 'Default' -%}
{%- if lang.lang == 'Default' -%}
{% assign base = 'compiler:Default.isl' -%}
{%- elsif lang.is_official -%}
{% assign base = 'compiler:Languages\' | append: lang.lang | append: '.isl' -%}
{%- else -%}
{%- comment -%}
unofficial or unknown: not bundled with Inno Setup, fall back for bare installs
{%- endcomment -%}
{% assign base = 'compiler:Default.isl' -%}
{%- endif -%}
{% assign incremental = lang.lang | default: lang.name -%}
+39 -14
View File
@@ -161,29 +161,46 @@ class _InnoIssPathContext:
raise KeyError(key)
def _resolve_inno_lang(pack: LanguagePack) -> tuple[Optional[str], bool]:
"""Resolve a language pack to its conventional Inno Setup language
name and officiality.
:param pack: The language pack to resolve.
:return: A pair of the conventional name (``None`` when no
conventional name exists) and whether the name belongs to an
official translation bundled with Inno Setup (always ``False``
when the name is ``None``).
"""
result = inno_lang_map.convert(pack.lang)
name = result.name.value if result.name is not None else None
return name, result.official
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 ``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 }}``.
messages file being placed exposed as ``name``, the best-effort
conventional Inno Setup language name exposed as ``lang`` and its
officiality exposed as ``is_official``. The conversion is not
normative and may yield ``None``, so path templates typically render
``{{ lang | default: name }}``.
"""
__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."""
__official: bool
"""Whether the conventional name is an official translation."""
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
self.__lang, self.__official = _resolve_inno_lang(pack)
def keys(self) -> tuple[str, ...]:
"""Return the names of the top-level template variables."""
return ("lang", "name")
return ("lang", "name", "is_official")
def __getitem__(self, key: str) -> object:
match key:
@@ -191,6 +208,8 @@ class _InnoIslPathContext:
return self.__lang
case "name":
return self.__name
case "is_official":
return self.__official
case _:
raise KeyError(key)
@@ -199,20 +218,21 @@ 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
It exposes the PO language tag as ``name``, the best-effort
conventional Inno Setup language name as ``lang`` (``None`` when no
conventional name exists).
conventional name exists) and its officiality as ``is_official``.
"""
__name: str
"""The PO language tag."""
__lang: Optional[str]
"""The conventional Inno Setup language name, or None if unknown."""
__official: bool
"""Whether the conventional name is an official translation."""
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
self.__lang, self.__official = _resolve_inno_lang(pack)
def __getitem__(self, key: str) -> object:
match key:
@@ -220,6 +240,8 @@ class _InnoLanguageView:
return self.__name
case "lang":
return self.__lang
case "is_official":
return self.__official
case _:
raise KeyError(key)
@@ -240,16 +262,17 @@ class _InnoIslTemplateContext:
"""The view of the pack's translations."""
__lang: Optional[str]
"""The conventional Inno Setup language name, or None if unknown."""
__official: bool
"""Whether the conventional name is an official translation."""
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
self.__lang, self.__official = _resolve_inno_lang(pack)
def keys(self) -> tuple[str, ...]:
"""Return the names of the exposed template variables."""
return ("name", "lang", "strings")
return ("name", "lang", "is_official", "strings")
def __getitem__(self, key: str) -> object:
match key:
@@ -257,6 +280,8 @@ class _InnoIslTemplateContext:
return self.__pack.lang.value
case "lang":
return self.__lang
case "is_official":
return self.__official
case "strings":
return self.__strings
case _:
+64 -31
View File
@@ -1,9 +1,11 @@
from dataclasses import dataclass
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*
# freely by the packager. The tables below transcribe 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.
@@ -11,19 +13,22 @@ from ..langid import InnoLang, PoLang
# 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+.
# 2026-10-08, reflecting Inno Setup 6.5.0+. The official/unofficial split
# is kept as two separate seed tables, so it stays machine-readable: the
# officiality of a conversion is exposed to templates as 'is_official',
# letting them serve users with a bare Inno Setup installation.
#
# 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.
# The tables are authored name-oriented (_OFFICIAL_NAME_KEYS and
# _UNOFFICIAL_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 them, 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
@@ -31,11 +36,11 @@ from ..langid import InnoLang, PoLang
# - 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
# Hindi, Kazakh, Marathi, Nepali, Sinhala, Uyghur); the tables record
# base names only, and the extension is the template author's concern.
# fmt: off
_NAME_KEYS: dict[InnoLang, tuple[str, ...]] = {
_OFFICIAL_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",),
@@ -71,7 +76,10 @@ _NAME_KEYS: dict[InnoLang, tuple[str, ...]] = {
InnoLang("Thai"): ("th",),
InnoLang("Turkish"): ("tr",),
InnoLang("Ukrainian"): ("uk",),
# Unofficial translations, up to date with 6.5.0+.
}
_UNOFFICIAL_NAME_KEYS: dict[InnoLang, tuple[str, ...]] = {
# Community maintained translations, up to date with 6.5.0+.
InnoLang("Afrikaans"): ("af",),
InnoLang("Basque"): ("eu",),
InnoLang("Croatian"): ("hr",),
@@ -84,7 +92,7 @@ _NAME_KEYS: dict[InnoLang, tuple[str, ...]] = {
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.
# Community maintained 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+
@@ -120,16 +128,38 @@ _NAME_KEYS: dict[InnoLang, tuple[str, ...]] = {
}
# 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.
# The key-oriented lookup table used at runtime, derived from both seed
# tables. 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
key: name
for names in (_OFFICIAL_NAME_KEYS, _UNOFFICIAL_NAME_KEYS)
for name, keys in names.items()
for key in keys
}
if len(_INNO_NAMES) != sum(len(keys) for keys in _NAME_KEYS.values()):
if len(_INNO_NAMES) != sum(
len(keys) for names in (_OFFICIAL_NAME_KEYS, _UNOFFICIAL_NAME_KEYS) for keys in names.values()
):
raise ValueError("duplicate PO language key(s) in the Inno Setup name table")
# The conventional names that belong to official translations, i.e. the
# messages files bundled with the Inno Setup compiler itself.
_OFFICIAL_NAMES: frozenset[InnoLang] = frozenset(_OFFICIAL_NAME_KEYS)
@dataclass(frozen=True)
class InnoConversion:
"""The result of a best-effort PO language conversion for Inno Setup."""
name: InnoLang | None
"""The conventional Inno Setup language name, or ``None`` if no
conventional name exists for the language."""
official: bool
"""Whether the name belongs to an official translation bundled with
Inno Setup. Always ``False`` when ``name`` is ``None``."""
# 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
@@ -141,7 +171,7 @@ _SCRIPT_WORDS: dict[str, str] = {
}
def convert(lang: PoLang) -> InnoLang | None:
def convert(lang: PoLang) -> InnoConversion:
"""
Convert a PO file language to a conventional Inno Setup language name.
@@ -151,12 +181,14 @@ def convert(lang: PoLang) -> InnoLang | None:
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.
language without a conventional name converts to an empty result
(``name`` is ``None``, ``official`` is ``False``), 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.
:return: The conversion result holding the conventional Inno Setup
language name (``None`` if none exists) and whether it belongs to
an official translation bundled with Inno Setup.
"""
candidates: list[str] = []
if lang.variant is not None:
@@ -173,6 +205,7 @@ def convert(lang: PoLang) -> InnoLang | 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
name = _INNO_NAMES.get(candidate)
if name is not None:
return InnoConversion(name, name in _OFFICIAL_NAMES)
return InnoConversion(None, False)