refactor: refactor manifest module
This commit is contained in:
+1
-1
@@ -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
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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] = {}
|
||||
|
||||
Reference in New Issue
Block a user