Compare commits

...
2 Commits
Author SHA1 Message Date
yyc12345 cea485f85c refactor: refactor manifest module 2026-09-14 16:07:59 +08:00
yyc12345 a0a48b1865 doc: add developer notes 2026-09-14 16:07:45 +08:00
5 changed files with 53 additions and 31 deletions
+12
View File
@@ -0,0 +1,12 @@
# Developer Notes
# Bump Version Up
## Bump Application Version Up
TODO...
## Bump Manifest Version Up
- Update version value in document `doc/manifest.md`.
- Update version value in example `doc/example.*.toml`.
+1 -1
View File
@@ -31,7 +31,7 @@ outputs. It must be non-empty; both numeric IDs (e.g. `1000`) and names
A manifest is rejected when:
- `version` is not `1`.
- `version` is not matched with current value introduced above.
- Any string entry key is empty.
- Two entries share both `msgid` and `context`.
- An entry or the document root contains unknown fields.
+37 -27
View File
@@ -1,29 +1,33 @@
import tomllib
from typing import Optional
from pathlib import Path
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
MANIFEST_VERSION = 1
"""The manifest format version this build of MetaGlot understands."""
class StringEntry(BaseModel):
model_config = ConfigDict(extra="forbid")
class StringEntry(BaseModel, frozen=True):
model_config = ConfigDict(extra="forbid", strict=True)
msgid: str
context: str = ""
comment: str = ""
"""The string to be translated."""
context: Optional[str] = None
"""The context of this entry. ``None`` if no context."""
comment: Optional[str] = None
"""Translator-oriented comment of this entry. ``None`` if nothing."""
class Manifest(BaseModel):
model_config = ConfigDict(extra="forbid")
class Manifest(BaseModel, frozen=True):
model_config = ConfigDict(extra="forbid", strict=True)
version: int
"""The version of this manifest file."""
strings: dict[str, StringEntry]
"""The list holding all strings to be translated."""
@field_validator("version")
@classmethod
def _check_version(cls, value: int) -> int:
def validate_version(cls, value: int) -> int:
if value != MANIFEST_VERSION:
raise ValueError(
f"unsupported manifest version: {value} (expected {MANIFEST_VERSION})"
@@ -31,34 +35,40 @@ class Manifest(BaseModel):
return value
@model_validator(mode="after")
def _check_entries(self) -> "Manifest":
empty_keys = sorted(key for key in self.strings if not key)
if empty_keys:
def validate_string_entries(self) -> "Manifest":
has_empty_key = any(key == "" for key in self.strings.keys())
if has_empty_key:
raise ValueError("string key(s) must not be empty")
pairs = [(entry.msgid, entry.context) for entry in self.strings.values()]
duplicated = sorted({pair for pair in pairs if pairs.count(pair) > 1})
if duplicated:
has_empty_value = any(value.msgid == "" for value in self.strings.values())
if has_empty_value:
raise ValueError("string value(s) must not be empty")
seen: set[tuple[str, Optional[str]]] = set()
duplicates: set[tuple[str, Optional[str]]] = set()
for entry in self.strings.values():
pair = (entry.msgid, entry.context)
if pair in seen:
duplicates.add(pair)
seen.add(pair)
if duplicates:
rendered = ", ".join(
f"{msgid!r}" if not context else f"{msgid!r} (context {context!r})"
for msgid, context in duplicated
f"{msgid!r}" if context is None else f"{msgid!r} (context {context!r})"
for msgid, context in sorted(
duplicates, key=lambda pair: (pair[0], pair[1] or "")
)
)
raise ValueError(f"duplicated msgid and context combination(s): {rendered}")
return self
@property
def entries(self) -> list[tuple[str, StringEntry]]:
return list(self.strings.items())
@property
def msgids(self) -> set[str]:
return {entry.msgid for entry in self.strings.values()}
def load_manifest(path: Path) -> Manifest:
try:
raw = tomllib.loads(path.read_text(encoding="utf-8"))
with open(path, "rb") as f:
raw = tomllib.load(f)
return Manifest.model_validate(raw)
except tomllib.TOMLDecodeError as exc:
raise ValueError(f"failed to parse manifest '{path}': {exc}") from exc
except OSError as exc:
raise RuntimeError(f"failed to read manifest '{path}': {exc}") from exc
return Manifest.model_validate(raw)
+1 -1
View File
@@ -51,7 +51,7 @@ def generate(manifest_path: Path, output_path: Path) -> None:
manifest: Manifest = load_manifest(manifest_path)
po = polib.POFile()
po.metadata = _POT_HEADER
for _, entry in manifest.entries:
for _, entry in manifest.strings.items():
po.append(
polib.POEntry(
msgid=entry.msgid,
+2 -2
View File
@@ -25,7 +25,7 @@ def _language_context(
translations: dict[str, str],
manifest: Manifest,
) -> dict:
entries = manifest.entries
entries = manifest.strings.items()
return {
"name": language.name,
"locale": locale,
@@ -51,7 +51,7 @@ def _language_context(
def render(manifest_path: Path, po_patterns: list[Path], template_path: Path, output_path: Path) -> None:
manifest = load_manifest(manifest_path)
known = manifest.msgids
known = {entry.msgid for entry in manifest.strings.values()}
collected = []
locale_sources: dict[str, Path] = {}