refactor: refactor render module
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# Liquid Filters
|
||||
|
||||
MetaGlot renders output files through [Liquid](https://shopify.github.io/liquid/)
|
||||
templates. The filters MetaGlot provides are grouped by rendering target and
|
||||
described in this document.
|
||||
|
||||
The rendering environment runs in strict mode: referencing a variable or a property
|
||||
that does not exist fails the render.
|
||||
|
||||
## General filters
|
||||
|
||||
The following filters are available in every rendering.
|
||||
|
||||
| Filter | Description |
|
||||
|--------------|-------------|
|
||||
| `to_string` | Convert the value to its string representation. |
|
||||
| `format` | Treat the value as a Python format template and apply `str.format` with the arguments passed to the filter. |
|
||||
| `to_integer` | Convert the value to an integer. |
|
||||
| `to_float` | Convert the value to a float. |
|
||||
| `is_integer` | Whether the value can be parsed as an integer. |
|
||||
| `is_float` | Whether the value can be parsed as a float. |
|
||||
|
||||
Examples:
|
||||
|
||||
```liquid
|
||||
{{ 1000 | to_string }} {% comment %}renders "1000"{% endcomment %}
|
||||
{{ "ID: {0}" | format: 1001 }} {% comment %}renders "ID: 1001"{% endcomment %}
|
||||
```
|
||||
|
||||
## RC filters
|
||||
|
||||
The following filters are available only when rendering Windows RC files.
|
||||
|
||||
| Filter | Description |
|
||||
|-------------|-------------|
|
||||
| `rc_escape` | Escapes a string for use inside a double-quoted C-style string literal, such as an RC string table entry: `\` becomes `\\` and `"` becomes `\"`. |
|
||||
+27
-20
@@ -3,42 +3,49 @@
|
||||
This document describes the data that `metaglot rc render` provides to a user-provided
|
||||
[Liquid](https://shopify.github.io/liquid/) template when rendering Windows RC files.
|
||||
|
||||
MetaGlot reads templates as UTF-8 and writes the rendered file as UTF-8 (without BOM)
|
||||
using the system's native line endings. This encoding is a built-in constraint of the
|
||||
tool and is not configurable. Because the generated file is UTF-8, templates should
|
||||
declare the encoding with `#pragma code_page(65001)` so that the RC compiler parses
|
||||
the file correctly (see the `example` folder for a complete sample).
|
||||
|
||||
The renderer runs in strict mode: referencing a variable or a property that does not
|
||||
exist fails the render. Output is written as UTF-8 (without BOM) using LF line endings.
|
||||
exist fails the render.
|
||||
|
||||
## Top-level variables
|
||||
|
||||
| Variable | Type | Description |
|
||||
|-------------|-------------------|-------------|
|
||||
| `languages` | array of language | One entry per language, see below. The English (`en`) entry is always present and comes first; remaining entries follow in the order their PO files are matched. |
|
||||
| `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 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.
|
||||
|
||||
## Language object
|
||||
|
||||
| Property | Type | Description |
|
||||
|---------------|--------|-------------|
|
||||
| `name` | string | Human-readable language name, e.g. `English (United States)`. |
|
||||
| `locale` | string | Gettext-style locale code, e.g. `en`, `zh_CN`. |
|
||||
| `langid` | string | Windows language identifier in hexadecimal, e.g. `0x409`. |
|
||||
| `primary` | string | Windows primary language identifier in hexadecimal, e.g. `0x09`. |
|
||||
| `sublanguage` | string | Windows sublanguage identifier in hexadecimal, e.g. `0x01`. |
|
||||
| `block_key` | string | Eight hex digits combining `langid` and code page 1200 (`0x4B0`), suitable as a `StringFileInfo` block key, e.g. `040904B0`. |
|
||||
| `strings` | array of string entry | All manifest entries, in manifest order, see below. |
|
||||
| `named` | mapping of string to string | Maps every entry key to its translated text, convenient for direct access such as `lang.named.file_description`. |
|
||||
| `block_key` | string | Eight hex digits combining `langid` and the Unicode code page, suitable as a `StringFileInfo` block key, e.g. `040904B0`. |
|
||||
| `code_page` | string | The version resource code page in decimal, always `1200` (Unicode, `0x04B0`). It is fixed by MetaGlot's UTF-8 read/write pipeline and shares its source with `block_key`; templates must not attempt to use another code page. |
|
||||
| `strings` | mapping of string to string | Maps every manifest entry key to its resolved text, e.g. `lang.strings.file_description`. |
|
||||
|
||||
The locale of a PO file is resolved from its `Language:` header, falling back to the
|
||||
file name without extension. Locales without a Windows language mapping are rejected.
|
||||
|
||||
## String entry object
|
||||
|
||||
| Property | Type | Description |
|
||||
|-----------|--------|-------------|
|
||||
| `key` | string | The entry key from the manifest, e.g. `100` or `file_description`. Numeric resource IDs are represented as strings. |
|
||||
| `msgid` | string | The untranslated source string. |
|
||||
| `text` | string | The translated text for this language. Falls back to `msgid` when the entry is untranslated or marked fuzzy. Provided raw, without any escaping. |
|
||||
| `context` | string | The entry context from the manifest, e.g. `File type name for: jpg, jpeg, jfif`. |
|
||||
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.
|
||||
|
||||
## Filters
|
||||
|
||||
| Filter | Description |
|
||||
|-------------|-------------|
|
||||
| `rc_escape` | Escapes a string for use inside a double-quoted RC string literal: `\` becomes `\\` and `"` becomes `\"`. |
|
||||
All filters described in [filters.md](filters.md) are available when rendering RC
|
||||
files: the general filters available in every rendering, and the RC filters such
|
||||
as `rc_escape`.
|
||||
|
||||
@@ -10,8 +10,7 @@ STRINGTABLE
|
||||
BEGIN
|
||||
{% for i in (1000..1002) %}
|
||||
{% assign key = i | to_string %}
|
||||
{% assign s = lang.named[key] %}
|
||||
{{ s.key }} "{{ s.text | rc_escape }}" // {{ s.comment }}
|
||||
{{ i }} "{{ lang.strings[key] | rc_escape }}"
|
||||
{% endfor %}
|
||||
END
|
||||
{% endfor %}
|
||||
@@ -35,18 +34,18 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "Comments", "https://github.com/SarasasChipWorkshop/metaglot"
|
||||
VALUE "CompanyName", "Sarasa's Chip Workshop"
|
||||
VALUE "FileDescription", "{{ lang.named.file_description | rc_escape }}"
|
||||
VALUE "FileDescription", "{{ lang.strings.file_description | rc_escape }}"
|
||||
VALUE "FileVersion", "1.0.0.0"
|
||||
VALUE "InternalName", "example"
|
||||
VALUE "LegalCopyright", "MIT/Expat License - Copyright (C) 2026 Sarasa's Chip Workshop"
|
||||
VALUE "OriginalFilename", "example.exe"
|
||||
VALUE "ProductName", "{{ lang.named.product_name | rc_escape }}"
|
||||
VALUE "ProductName", "{{ lang.strings.product_name | rc_escape }}"
|
||||
VALUE "ProductVersion", "1.0.0.0"
|
||||
END
|
||||
{% endfor %}
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", {% for lang in languages %}{{ lang.langid }}, 1200{% unless forloop.last %}, {% endunless %}{% endfor %}
|
||||
VALUE "Translation", {% for lang in languages %}{{ lang.langid }}, {{ lang.code_page }}{% unless forloop.last %}, {% endunless %}{% endfor %}
|
||||
END
|
||||
END
|
||||
|
||||
@@ -1,11 +1,50 @@
|
||||
import logging
|
||||
|
||||
from ... import winlang
|
||||
from ...cli import RcRenderOpts
|
||||
from ...render import render
|
||||
from ...filters import register_general_filters, register_rc_filters
|
||||
from ...langid import PoLang
|
||||
from ...manifest import Manifest, load_manifest
|
||||
from ...pofile import LanguagePack, resolve_glob_files, resolve_translations
|
||||
from ...render import create_environment, render
|
||||
|
||||
_CODE_PAGE = 1200
|
||||
"""The Unicode (UTF-16) code page used by VERSIONINFO resources.
|
||||
|
||||
MetaGlot reads and writes everything as UTF-8, so this code page is a fixed
|
||||
constraint of the pipeline, not a configuration point.
|
||||
"""
|
||||
|
||||
|
||||
def _lookup_windows_language(lang: PoLang) -> winlang.WindowsLanguage:
|
||||
try:
|
||||
return winlang.lookup(lang.value)
|
||||
except KeyError:
|
||||
raise ValueError(f"language '{lang}' has no Windows language mapping") from None
|
||||
|
||||
|
||||
def _language_context(pack: LanguagePack, manifest: Manifest) -> dict:
|
||||
language = _lookup_windows_language(pack.lang)
|
||||
return {
|
||||
"name": language.name,
|
||||
"langid": f"0x{language.langid:X}",
|
||||
"primary": f"0x{language.primary:02X}",
|
||||
"sublanguage": f"0x{language.sublanguage:02X}",
|
||||
"block_key": f"{language.langid:04X}{_CODE_PAGE:04X}",
|
||||
"code_page": f"{_CODE_PAGE}",
|
||||
"strings": pack.translations,
|
||||
}
|
||||
|
||||
|
||||
def run(opts: RcRenderOpts) -> None:
|
||||
render(
|
||||
opts.in_manifest,
|
||||
opts.in_po,
|
||||
opts.in_template,
|
||||
opts.out_rc,
|
||||
manifest = load_manifest(opts.in_manifest)
|
||||
packs = resolve_translations(manifest, resolve_glob_files(opts.in_po))
|
||||
languages = [_language_context(pack, manifest) for pack in packs.values()]
|
||||
logging.info(
|
||||
"rendering languages: %s",
|
||||
", ".join(language["name"] for language in languages),
|
||||
)
|
||||
env = create_environment()
|
||||
register_general_filters(env)
|
||||
register_rc_filters(env)
|
||||
render(env, opts.in_template, {"languages": languages}, opts.out_rc)
|
||||
|
||||
@@ -18,6 +18,10 @@ def _is_float(s: str) -> bool:
|
||||
|
||||
|
||||
def register_general_filters(env: Environment) -> None:
|
||||
"""Register the general-purpose Liquid filters available in every rendering.
|
||||
|
||||
:param env: The environment to register the filters on.
|
||||
"""
|
||||
env.add_filter("to_string", str)
|
||||
env.add_filter("format", str.format)
|
||||
env.add_filter("to_integer", int)
|
||||
@@ -30,5 +34,9 @@ def _rc_escape(value: str) -> str:
|
||||
return value.replace("\\", "\\\\").replace('"', '\\"')
|
||||
|
||||
|
||||
def register_rc_specific_filters(env: Environment) -> None:
|
||||
def register_rc_filters(env: Environment) -> None:
|
||||
"""Register the Liquid filters specific to Windows RC file rendering.
|
||||
|
||||
:param env: The environment to register the filters on.
|
||||
"""
|
||||
env.add_filter("rc_escape", _rc_escape)
|
||||
|
||||
@@ -49,7 +49,7 @@ def resolve_glob_files(patterns: Iterable[Path]) -> Iterator[Path]:
|
||||
|
||||
|
||||
def resolve_translations(
|
||||
manifest: Manifest, po_paths: Iterator[Path]
|
||||
manifest: Manifest, po_paths: Iterable[Path]
|
||||
) -> dict[PoLang, LanguagePack]:
|
||||
"""Resolve the translations of the given PO files against a manifest.
|
||||
|
||||
@@ -141,7 +141,7 @@ def _load_po(path: Path) -> polib.POFile:
|
||||
"""
|
||||
try:
|
||||
return polib.pofile(str(path))
|
||||
except (OSError, IOError) as exc:
|
||||
except OSError as exc:
|
||||
raise RuntimeError(f"failed to read PO file '{path}': {exc}") from exc
|
||||
|
||||
|
||||
|
||||
+22
-80
@@ -4,86 +4,34 @@ from pathlib import Path
|
||||
from liquid import Environment, StrictUndefined
|
||||
from liquid.exceptions import LiquidError
|
||||
|
||||
from . import winlang
|
||||
from .langid import PoLang
|
||||
from .manifest import Manifest, load_manifest
|
||||
from .pofile import LanguagePack, resolve_glob_files, resolve_translations
|
||||
|
||||
def create_environment() -> Environment:
|
||||
"""Create a Liquid environment for rendering.
|
||||
|
||||
def _rc_escape(value: str) -> str:
|
||||
return value.replace("\\", "\\\\").replace('"', '\\"')
|
||||
The environment runs in strict mode: referencing a variable or a property
|
||||
that does not exist fails the render. No filters are registered; callers
|
||||
register what they need with the functions of the ``filters`` module.
|
||||
|
||||
|
||||
def _create_environment() -> Environment:
|
||||
env = Environment(undefined=StrictUndefined)
|
||||
env.add_filter("rc_escape", _rc_escape)
|
||||
return env
|
||||
|
||||
|
||||
def _language_context(
|
||||
locale: str,
|
||||
language: winlang.WindowsLanguage,
|
||||
pack: LanguagePack,
|
||||
manifest: Manifest,
|
||||
) -> dict:
|
||||
return {
|
||||
"name": language.name,
|
||||
"locale": locale,
|
||||
"langid": f"0x{language.langid:X}",
|
||||
"primary": f"0x{language.primary:02X}",
|
||||
"sublanguage": f"0x{language.sublanguage:02X}",
|
||||
"block_key": f"{language.langid:04X}04B0",
|
||||
"strings": [
|
||||
{
|
||||
"key": key,
|
||||
"msgid": entry.msgid,
|
||||
"text": pack.translations[key],
|
||||
"context": entry.context,
|
||||
}
|
||||
for key, entry in manifest.strings.items()
|
||||
],
|
||||
"named": pack.translations,
|
||||
}
|
||||
|
||||
|
||||
def _lookup_windows_language(lang: PoLang) -> winlang.WindowsLanguage:
|
||||
try:
|
||||
return winlang.lookup(lang.value)
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
f"language '{lang}' has no Windows language mapping"
|
||||
) from None
|
||||
:return: The created environment.
|
||||
"""
|
||||
return Environment(undefined=StrictUndefined)
|
||||
|
||||
|
||||
def render(
|
||||
manifest_path: Path,
|
||||
po_patterns: list[Path],
|
||||
template_path: Path,
|
||||
output_path: Path,
|
||||
env: Environment, template_path: Path, context: dict, output_path: Path
|
||||
) -> None:
|
||||
manifest = load_manifest(manifest_path)
|
||||
packs = resolve_translations(manifest, resolve_glob_files(po_patterns))
|
||||
"""Render a Liquid template and write the result to a file.
|
||||
|
||||
english = winlang.lookup("en")
|
||||
resolved = [(pack, _lookup_windows_language(pack.lang)) for pack in packs.values()]
|
||||
english_item = next(
|
||||
(item for item in resolved if item[1].langid == english.langid), None
|
||||
)
|
||||
others = [item for item in resolved if item[1].langid != english.langid]
|
||||
if english_item is None:
|
||||
english_pack = LanguagePack(
|
||||
PoLang("en"),
|
||||
{key: entry.msgid for key, entry in manifest.strings.items()},
|
||||
)
|
||||
ordered = [(english_pack, english), *others]
|
||||
else:
|
||||
ordered = [english_item, *others]
|
||||
|
||||
languages = [
|
||||
_language_context(pack.lang.value, language, pack, manifest)
|
||||
for pack, language in ordered
|
||||
]
|
||||
The template is read as UTF-8. The rendered output is written as UTF-8
|
||||
using the system's native line endings.
|
||||
|
||||
:param env: The environment to render with.
|
||||
:param template_path: The path of the template file.
|
||||
:param context: The top-level variables passed to the template.
|
||||
:param output_path: The path of the rendered file to write.
|
||||
:raises ValueError: If the template file does not exist.
|
||||
:raises RuntimeError: If the template cannot be read or rendered.
|
||||
"""
|
||||
if not template_path.is_file():
|
||||
raise ValueError(f"template file not found: {template_path}")
|
||||
try:
|
||||
@@ -91,21 +39,15 @@ def render(
|
||||
except OSError as exc:
|
||||
raise RuntimeError(f"failed to read template '{template_path}': {exc}") from exc
|
||||
|
||||
env = _create_environment()
|
||||
try:
|
||||
template = env.from_string(
|
||||
template_source, name=str(template_path), path=template_path
|
||||
)
|
||||
content = template.render(languages=languages)
|
||||
content = template.render(**context)
|
||||
except LiquidError as exc:
|
||||
raise RuntimeError(
|
||||
f"failed to render template '{template_path}': {exc}"
|
||||
) from exc
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(content, encoding="utf-8", newline="\n")
|
||||
logging.info(
|
||||
"wrote %s with languages: %s",
|
||||
output_path,
|
||||
", ".join(language["name"] for language in languages),
|
||||
)
|
||||
output_path.write_text(content, encoding="utf-8")
|
||||
logging.info("Wrote rendered output to %s", output_path)
|
||||
|
||||
Reference in New Issue
Block a user