refactor: refactor language pack for future plist work
This commit is contained in:
@@ -4,8 +4,9 @@ from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from ...filters import register_appstream_filters, register_general_filters
|
||||
from ...langid import AppStreamLang, PoLang
|
||||
from ...langmap import appstream_lang_map
|
||||
from ...manifest import Manifest, load_manifest
|
||||
from ...manifest import load_manifest
|
||||
from ...pofile import (
|
||||
LanguagePack,
|
||||
build_default_pack,
|
||||
@@ -85,31 +86,85 @@ def parse(args: dict[str, Any]) -> AppStreamRenderOpts:
|
||||
)
|
||||
|
||||
|
||||
def _language_context(pack: LanguagePack, manifest: Manifest) -> dict:
|
||||
lang = appstream_lang_map.convert(pack.lang)
|
||||
return {
|
||||
"name": pack.lang.value,
|
||||
"lang": lang.value,
|
||||
"strings": pack.translations,
|
||||
}
|
||||
class _AppStreamStringsView:
|
||||
"""Route template string key lookups to their resolved text."""
|
||||
|
||||
__pack: LanguagePack
|
||||
"""The language pack the lookups route to."""
|
||||
|
||||
def __init__(self, pack: LanguagePack):
|
||||
self.__pack = pack
|
||||
|
||||
def __getitem__(self, key: str) -> str:
|
||||
return self.__pack.translations[key].text
|
||||
|
||||
|
||||
class _AppStreamLanguageView:
|
||||
"""The Liquid-facing view of one language for AppStream rendering."""
|
||||
|
||||
__pack: LanguagePack
|
||||
"""The wrapped language pack."""
|
||||
__strings: _AppStreamStringsView
|
||||
"""The view of the pack's translations."""
|
||||
__lang: AppStreamLang
|
||||
"""The BCP 47 language tag of the pack's language."""
|
||||
|
||||
def __init__(self, pack: LanguagePack):
|
||||
self.__pack = pack
|
||||
self.__strings = _AppStreamStringsView(pack)
|
||||
self.__lang = appstream_lang_map.convert(pack.lang)
|
||||
|
||||
def __getitem__(self, key: str) -> object:
|
||||
match key:
|
||||
case "name":
|
||||
return self.__pack.lang.value
|
||||
case "strings":
|
||||
return self.__strings
|
||||
case "lang":
|
||||
return self.__lang.value
|
||||
case _:
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
class _AppStreamRenderContext:
|
||||
"""The Liquid-facing top-level context for AppStream rendering."""
|
||||
|
||||
__languages: tuple[_AppStreamLanguageView, ...]
|
||||
"""The views of every translation language."""
|
||||
__default: _AppStreamLanguageView
|
||||
"""The view of the manifest's source language."""
|
||||
|
||||
def __init__(self, packs: dict[PoLang, LanguagePack], default: LanguagePack):
|
||||
self.__languages = tuple(
|
||||
_AppStreamLanguageView(pack) for pack in packs.values()
|
||||
)
|
||||
self.__default = _AppStreamLanguageView(default)
|
||||
|
||||
def keys(self) -> tuple[str, ...]:
|
||||
"""Return the names of the top-level template variables."""
|
||||
return ("languages", "default")
|
||||
|
||||
def __getitem__(self, key: str) -> object:
|
||||
match key:
|
||||
case "languages":
|
||||
return self.__languages
|
||||
case "default":
|
||||
return self.__default
|
||||
case _:
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
def run(opts: AppStreamRenderOpts) -> None:
|
||||
manifest = load_manifest(opts.in_manifest)
|
||||
packs = resolve_translations(manifest, resolve_glob_files(opts.in_po))
|
||||
default = _language_context(
|
||||
build_default_pack(manifest, manifest.source_language), manifest
|
||||
)
|
||||
entries = [_language_context(pack, manifest) for pack in packs.values()]
|
||||
default = build_default_pack(manifest, manifest.source_language)
|
||||
context = _AppStreamRenderContext(packs, default)
|
||||
logging.info(
|
||||
"rendering languages: %s (default %s)",
|
||||
", ".join(entry["name"] for entry in entries),
|
||||
default["name"],
|
||||
", ".join(str(pack.lang) for pack in packs.values()),
|
||||
default.lang,
|
||||
)
|
||||
env = create_environment()
|
||||
register_general_filters(env)
|
||||
register_appstream_filters(env)
|
||||
render(
|
||||
env, opts.in_template, {"languages": entries, "default": default},
|
||||
opts.out_appstream,
|
||||
)
|
||||
render(env, opts.in_template, context, opts.out_appstream)
|
||||
|
||||
@@ -4,8 +4,9 @@ from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from ...filters import register_desktop_filters, register_general_filters
|
||||
from ...langid import DesktopLang, PoLang
|
||||
from ...langmap import desktop_lang_map
|
||||
from ...manifest import Manifest, load_manifest
|
||||
from ...manifest import load_manifest
|
||||
from ...pofile import (
|
||||
LanguagePack,
|
||||
build_default_pack,
|
||||
@@ -85,31 +86,85 @@ def parse(args: dict[str, Any]) -> DesktopRenderOpts:
|
||||
)
|
||||
|
||||
|
||||
def _language_context(pack: LanguagePack, manifest: Manifest) -> dict:
|
||||
locale = desktop_lang_map.convert(pack.lang)
|
||||
return {
|
||||
"name": pack.lang.value,
|
||||
"locale": locale.value,
|
||||
"strings": pack.translations,
|
||||
}
|
||||
class _DesktopStringsView:
|
||||
"""Route template string key lookups to their resolved text."""
|
||||
|
||||
__pack: LanguagePack
|
||||
"""The language pack the lookups route to."""
|
||||
|
||||
def __init__(self, pack: LanguagePack):
|
||||
self.__pack = pack
|
||||
|
||||
def __getitem__(self, key: str) -> str:
|
||||
return self.__pack.translations[key].text
|
||||
|
||||
|
||||
class _DesktopLanguageView:
|
||||
"""The Liquid-facing view of one language for desktop entry rendering."""
|
||||
|
||||
__pack: LanguagePack
|
||||
"""The wrapped language pack."""
|
||||
__strings: _DesktopStringsView
|
||||
"""The view of the pack's translations."""
|
||||
__locale: DesktopLang
|
||||
"""The desktop entry locale of the pack's language."""
|
||||
|
||||
def __init__(self, pack: LanguagePack):
|
||||
self.__pack = pack
|
||||
self.__strings = _DesktopStringsView(pack)
|
||||
self.__locale = desktop_lang_map.convert(pack.lang)
|
||||
|
||||
def __getitem__(self, key: str) -> object:
|
||||
match key:
|
||||
case "name":
|
||||
return self.__pack.lang.value
|
||||
case "strings":
|
||||
return self.__strings
|
||||
case "locale":
|
||||
return self.__locale.value
|
||||
case _:
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
class _DesktopRenderContext:
|
||||
"""The Liquid-facing top-level context for desktop entry rendering."""
|
||||
|
||||
__languages: tuple[_DesktopLanguageView, ...]
|
||||
"""The views of every translation language."""
|
||||
__default: _DesktopLanguageView
|
||||
"""The view of the manifest's source language."""
|
||||
|
||||
def __init__(self, packs: dict[PoLang, LanguagePack], default: LanguagePack):
|
||||
self.__languages = tuple(
|
||||
_DesktopLanguageView(pack) for pack in packs.values()
|
||||
)
|
||||
self.__default = _DesktopLanguageView(default)
|
||||
|
||||
def keys(self) -> tuple[str, ...]:
|
||||
"""Return the names of the top-level template variables."""
|
||||
return ("languages", "default")
|
||||
|
||||
def __getitem__(self, key: str) -> object:
|
||||
match key:
|
||||
case "languages":
|
||||
return self.__languages
|
||||
case "default":
|
||||
return self.__default
|
||||
case _:
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
def run(opts: DesktopRenderOpts) -> None:
|
||||
manifest = load_manifest(opts.in_manifest)
|
||||
packs = resolve_translations(manifest, resolve_glob_files(opts.in_po))
|
||||
default = _language_context(
|
||||
build_default_pack(manifest, manifest.source_language), manifest
|
||||
)
|
||||
entries = [_language_context(pack, manifest) for pack in packs.values()]
|
||||
default = build_default_pack(manifest, manifest.source_language)
|
||||
context = _DesktopRenderContext(packs, default)
|
||||
logging.info(
|
||||
"rendering languages: %s (default %s)",
|
||||
", ".join(entry["name"] for entry in entries),
|
||||
default["name"],
|
||||
", ".join(str(pack.lang) for pack in packs.values()),
|
||||
default.lang,
|
||||
)
|
||||
env = create_environment()
|
||||
register_general_filters(env)
|
||||
register_desktop_filters(env)
|
||||
render(
|
||||
env, opts.in_template, {"languages": entries, "default": default},
|
||||
opts.out_desktop,
|
||||
)
|
||||
render(env, opts.in_template, context, opts.out_desktop)
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Any
|
||||
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 ...manifest import load_manifest
|
||||
from ...pofile import (
|
||||
LanguagePack,
|
||||
build_default_pack,
|
||||
@@ -100,17 +100,73 @@ def _lookup_langid(lang: PoLang) -> WinLcid:
|
||||
raise ValueError(f"language '{lang}' has no Windows language mapping") from None
|
||||
|
||||
|
||||
def _language_context(pack: LanguagePack, manifest: Manifest) -> dict:
|
||||
lcid = _lookup_langid(pack.lang)
|
||||
return {
|
||||
"name": pack.lang.value,
|
||||
"langid": f"0x{lcid.value:04X}",
|
||||
"primary": f"0x{lcid.primary:02X}",
|
||||
"sublanguage": f"0x{lcid.sublanguage:02X}",
|
||||
"block_key": f"{lcid.value:04X}{_CODE_PAGE:04X}",
|
||||
"code_page": f"{_CODE_PAGE}",
|
||||
"strings": pack.translations,
|
||||
}
|
||||
class _RcStringsView:
|
||||
"""Route template string key lookups to their resolved text."""
|
||||
|
||||
__pack: LanguagePack
|
||||
"""The language pack the lookups route to."""
|
||||
|
||||
def __init__(self, pack: LanguagePack):
|
||||
self.__pack = pack
|
||||
|
||||
def __getitem__(self, key: str) -> str:
|
||||
return self.__pack.translations[key].text
|
||||
|
||||
|
||||
class _RcLanguageView:
|
||||
"""The Liquid-facing view of one language for RC rendering."""
|
||||
|
||||
__pack: LanguagePack
|
||||
"""The wrapped language pack."""
|
||||
__strings: _RcStringsView
|
||||
"""The view of the pack's translations."""
|
||||
__lcid: WinLcid
|
||||
"""The Windows language identifier of the pack's language."""
|
||||
|
||||
def __init__(self, pack: LanguagePack):
|
||||
self.__pack = pack
|
||||
self.__strings = _RcStringsView(pack)
|
||||
self.__lcid = _lookup_langid(pack.lang)
|
||||
|
||||
def __getitem__(self, key: str) -> object:
|
||||
match key:
|
||||
case "name":
|
||||
return self.__pack.lang.value
|
||||
case "strings":
|
||||
return self.__strings
|
||||
case "langid":
|
||||
return f"0x{self.__lcid.value:04X}"
|
||||
case "primary":
|
||||
return f"0x{self.__lcid.primary:02X}"
|
||||
case "sublanguage":
|
||||
return f"0x{self.__lcid.sublanguage:02X}"
|
||||
case "block_key":
|
||||
return f"{self.__lcid.value:04X}{_CODE_PAGE:04X}"
|
||||
case "code_page":
|
||||
return f"{_CODE_PAGE}"
|
||||
case _:
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
class _RcRenderContext:
|
||||
"""The Liquid-facing top-level context for RC rendering."""
|
||||
|
||||
__languages: tuple[_RcLanguageView, ...]
|
||||
"""The views of every rendered language."""
|
||||
|
||||
def __init__(self, packs: dict[PoLang, LanguagePack]):
|
||||
self.__languages = tuple(_RcLanguageView(pack) for pack in packs.values())
|
||||
|
||||
def keys(self) -> tuple[str, ...]:
|
||||
"""Return the names of the top-level template variables."""
|
||||
return ("languages",)
|
||||
|
||||
def __getitem__(self, key: str) -> object:
|
||||
match key:
|
||||
case "languages":
|
||||
return self.__languages
|
||||
case _:
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
def run(opts: RcRenderOpts) -> None:
|
||||
@@ -122,12 +178,12 @@ def run(opts: RcRenderOpts) -> None:
|
||||
packs[manifest.source_language] = build_default_pack(
|
||||
manifest, manifest.source_language
|
||||
)
|
||||
languages = [_language_context(pack, manifest) for pack in packs.values()]
|
||||
context = _RcRenderContext(packs)
|
||||
logging.info(
|
||||
"rendering languages: %s",
|
||||
", ".join(language["name"] for language in languages),
|
||||
", ".join(str(pack.lang) for pack in packs.values()),
|
||||
)
|
||||
env = create_environment()
|
||||
register_general_filters(env)
|
||||
register_rc_filters(env)
|
||||
render(env, opts.in_template, {"languages": languages}, opts.out_rc)
|
||||
render(env, opts.in_template, context, opts.out_rc)
|
||||
|
||||
+24
-6
@@ -7,7 +7,7 @@ from pathlib import Path
|
||||
from typing import Iterable, Iterator, Optional
|
||||
import polib
|
||||
from .langid import PoLang
|
||||
from .manifest import Manifest
|
||||
from .manifest import Manifest, StringEntry
|
||||
|
||||
_POT_HEADER = {
|
||||
"Project-Id-Version": "PACKAGE VERSION",
|
||||
@@ -22,6 +22,18 @@ _POT_HEADER = {
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Translation:
|
||||
"""The resolution of one manifest entry in one language."""
|
||||
|
||||
entry: StringEntry
|
||||
"""The manifest entry this translation resolves."""
|
||||
|
||||
text: str
|
||||
"""The usable text of the entry. It falls back to the manifest source
|
||||
string when the entry is untranslated, missing or marked fuzzy."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LanguagePack:
|
||||
"""Resolved translations of one language against a manifest."""
|
||||
@@ -29,8 +41,9 @@ class LanguagePack:
|
||||
lang: PoLang
|
||||
"""The language of this pack."""
|
||||
|
||||
translations: dict[str, str]
|
||||
"""The resolved text of every manifest entry, keyed by manifest entry key."""
|
||||
translations: dict[str, Translation]
|
||||
"""The resolved translation of every manifest entry, keyed by manifest
|
||||
entry key."""
|
||||
|
||||
|
||||
def resolve_glob_files(patterns: Iterable[Path]) -> Iterator[Path]:
|
||||
@@ -108,7 +121,7 @@ def resolve_translations(
|
||||
"""Resolve the translations of the given PO files against a manifest.
|
||||
|
||||
Each PO file is loaded lazily and resolved to a :class:`LanguagePack`
|
||||
holding the text of every manifest entry. Entries that are missing,
|
||||
holding the resolution of every manifest entry. Entries that are missing,
|
||||
untranslated or marked fuzzy fall back to their source string.
|
||||
|
||||
:param manifest: The manifest describing all translatable entries.
|
||||
@@ -158,7 +171,10 @@ def resolve_translations(
|
||||
# Resolve every manifest entry against the index. Entries that are
|
||||
# missing, untranslated or fuzzy fall back to their source string.
|
||||
translations = {
|
||||
key: _translated_text(index.get((entry.msgid, entry.context)), entry.msgid)
|
||||
key: Translation(
|
||||
entry,
|
||||
_translated_text(index.get((entry.msgid, entry.context)), entry.msgid),
|
||||
)
|
||||
for key, entry in manifest.strings.items()
|
||||
}
|
||||
packs[lang] = LanguagePack(lang, translations)
|
||||
@@ -177,7 +193,9 @@ def build_default_pack(manifest: Manifest, lang: PoLang) -> LanguagePack:
|
||||
: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()}
|
||||
translations = {
|
||||
key: Translation(entry, entry.msgid) for key, entry in manifest.strings.items()
|
||||
}
|
||||
return LanguagePack(lang, translations)
|
||||
|
||||
|
||||
|
||||
+26
-3
@@ -1,10 +1,30 @@
|
||||
import logging
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
from typing import Protocol
|
||||
|
||||
from liquid import Environment, StrictUndefined
|
||||
from liquid.exceptions import LiquidError
|
||||
|
||||
|
||||
class RenderContext(Protocol):
|
||||
"""The interface required of a top-level rendering context.
|
||||
|
||||
A render context routes template variable lookups dynamically (Liquid
|
||||
performs item access only), and enumerates its variable names the same
|
||||
way the ``dict`` constructor does, so an instance can be passed to
|
||||
``BoundTemplate.render`` directly.
|
||||
"""
|
||||
|
||||
def keys(self) -> Iterable[str]:
|
||||
"""Return the names of the top-level template variables."""
|
||||
...
|
||||
|
||||
def __getitem__(self, key: str) -> object:
|
||||
"""Return the value of the named top-level template variable."""
|
||||
...
|
||||
|
||||
|
||||
def create_environment() -> Environment:
|
||||
"""Create a Liquid environment for rendering.
|
||||
|
||||
@@ -18,7 +38,10 @@ def create_environment() -> Environment:
|
||||
|
||||
|
||||
def render(
|
||||
env: Environment, template_path: Path, context: dict, output_path: Path
|
||||
env: Environment,
|
||||
template_path: Path,
|
||||
context: RenderContext,
|
||||
output_path: Path,
|
||||
) -> None:
|
||||
"""Render a Liquid template and write the result to a file.
|
||||
|
||||
@@ -27,7 +50,7 @@ def render(
|
||||
|
||||
: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 context: The top-level render context 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.
|
||||
@@ -43,7 +66,7 @@ def render(
|
||||
template = env.from_string(
|
||||
template_source, name=str(template_path), path=template_path
|
||||
)
|
||||
content = template.render(**context)
|
||||
content = template.render(context)
|
||||
except LiquidError as exc:
|
||||
raise RuntimeError(
|
||||
f"failed to render template '{template_path}': {exc}"
|
||||
|
||||
Reference in New Issue
Block a user