fix: add source language field in manifest

add source language field in manifest to fix the bug that source string is not included in render context
This commit is contained in:
2026-09-17 10:50:28 +08:00
parent 93c69f764d
commit 7986c72ff0
6 changed files with 70 additions and 4 deletions
+3
View File
@@ -9,6 +9,7 @@ The manifest is platform-neutral. It only describes *what* can be translated, no
| Field | Type | Description |
|---|---|---|
| `version` | int | The manifest format version. Currently `1`; any other value is rejected. |
| `source_language` | string | Required. The language of the source strings (`msgid`). Must be a valid Gettext `Language` field value, e.g. `en`, `en_US`. |
| `strings` | table | All translatable strings. Each item maps a string entry key to a string entry, see below. |
## String Entry
@@ -26,6 +27,7 @@ The string entry key identifies the entry inside the manifest and in generated o
A manifest is rejected when:
- `version` is not matched with current value introduced above.
- `source_language` is missing or not a valid language tag.
- Any string entry key is empty.
- Two entries share both `msgid` and `context`.
- An entry or the document root contains unknown fields.
@@ -34,6 +36,7 @@ A manifest is rejected when:
```toml
version = 1
source_language = "en"
[strings.panel_title]
msgid = "Example - Image Viewer"
+6 -1
View File
@@ -12,7 +12,12 @@ The renderer runs in strict mode: referencing a variable or a property that does
|---|---|---|
| `languages` | array of language | One entry per language, see below. |
The order of the entries is not guaranteed: they appear in the order their PO files are matched. The presence of an English entry is not guaranteed either. An English entry exists only if an English PO file is provided; otherwise it is simply absent, and templates must handle that themselves (the text of every entry already falls back to the manifest source string, which is typically English).
The order of the entries is not guaranteed: they appear in the order their PO files
are matched, with a synthesized entry (see below) appended last. The manifest's
source language is always present: it comes from a PO file when one provides it,
and is otherwise synthesized from the manifest's source strings — a manifest
declaring `source_language = "en"` yields a neutral `en` entry (`0x0009`), while
`"en_US"` yields `0x0409`.
The language of a PO file is resolved from its `Language:` header, falling back to the file name without extension; only values valid as Gettext `Language` fields are accepted. Languages without a Windows language mapping, two PO files describing the same language, and PO entries unknown to the manifest are rejected. Languages without a country resolve to a neutral language identifier (sublanguage `0x00`), e.g. `en` resolves to `0x0009`.
+1
View File
@@ -1,4 +1,5 @@
version = 1
source_language = "en"
[strings.1000]
msgid = "Example - Image Viewer"
+12 -1
View File
@@ -5,7 +5,12 @@ from ...filters import register_general_filters, register_rc_filters
from ...langid import PoLang, WinLcid
from ...langmap import win_lcid_map
from ...manifest import Manifest, load_manifest
from ...pofile import LanguagePack, resolve_glob_files, resolve_translations
from ...pofile import (
LanguagePack,
build_default_pack,
resolve_glob_files,
resolve_translations,
)
from ...render import create_environment, render
_CODE_PAGE = 1200
@@ -39,6 +44,12 @@ def _language_context(pack: LanguagePack, manifest: Manifest) -> dict:
def run(opts: RcRenderOpts) -> None:
manifest = load_manifest(opts.in_manifest)
packs = resolve_translations(manifest, resolve_glob_files(opts.in_po))
# The manifest's source language is always rendered: taken from a PO file
# when available, synthesized from the source strings otherwise.
if manifest.source_language not in packs:
packs[manifest.source_language] = build_default_pack(
manifest, manifest.source_language
)
languages = [_language_context(pack, manifest) for pack in packs.values()]
logging.info(
"rendering languages: %s",
+32 -2
View File
@@ -1,7 +1,16 @@
import tomllib
from typing import Optional
from typing import Annotated, Optional
from pathlib import Path
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
from pydantic import (
BaseModel,
ConfigDict,
GetPydanticSchema,
field_validator,
model_validator,
)
from pydantic_core import core_schema
from .langid import PoLang
MANIFEST_VERSION = 1
"""The manifest format version this build of MetaGlot understands."""
@@ -18,11 +27,32 @@ class StringEntry(BaseModel, frozen=True):
"""Translator-oriented comment of this entry. ``None`` if nothing."""
def _validate_po_lang(value) -> PoLang:
if isinstance(value, PoLang):
return value
return PoLang(value)
def _po_lang_schema(source_type, handler) -> core_schema.CoreSchema:
"""Describe how pydantic validates a ``PoLang`` field.
The whole validation is delegated to :func:`_validate_po_lang`, which
accepts any value parseable by ``PoLang``. This keeps the field typed
as ``PoLang`` without enabling ``arbitrary_types_allowed`` on the model.
"""
return core_schema.no_info_plain_validator_function(
_validate_po_lang,
serialization=core_schema.to_string_ser_schema(),
)
class Manifest(BaseModel, frozen=True):
model_config = ConfigDict(extra="forbid", strict=True)
version: int
"""The version of this manifest file."""
source_language: Annotated[PoLang, GetPydanticSchema(_po_lang_schema)]
"""The language of the source strings (``msgid``)."""
strings: dict[str, StringEntry]
"""The list holding all strings to be translated."""
+16
View File
@@ -165,6 +165,22 @@ def resolve_translations(
return packs
def build_default_pack(manifest: Manifest, lang: PoLang) -> LanguagePack:
"""Build the default language pack of a manifest.
The pack is built directly from the manifest without reading any PO
file: every entry resolves to its own source string. The language of
the pack is not derived from a PO file either; callers pass whatever
value fits their use.
:param manifest: The manifest describing all translatable entries.
:param lang: The language stamped on the returned pack.
:return: The default language pack.
"""
translations = {key: entry.msgid for key, entry in manifest.strings.items()}
return LanguagePack(lang, translations)
def generate_pot(manifest: Manifest, output_path: Path) -> None:
"""Generate a POT template file from a manifest.