feat: add update command for rc render
This commit is contained in:
+46
-1
@@ -26,15 +26,24 @@ class RcRenderOpts:
|
||||
"""The path to the output rendered Windows RC file."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RcUpdateOpts:
|
||||
in_pot: Path
|
||||
"""The path to the input POT template file."""
|
||||
in_po: list[Path]
|
||||
"""The path to input PO files to update, each may be a glob pattern."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RcOpts:
|
||||
opts: RcPotOpts | RcRenderOpts
|
||||
opts: RcPotOpts | RcRenderOpts | RcUpdateOpts
|
||||
"""The option of one of subcommand."""
|
||||
|
||||
|
||||
class RcCommand(enum.StrEnum):
|
||||
Pot = "pot"
|
||||
Render = "render"
|
||||
Update = "update"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -65,6 +74,9 @@ _RC_RDR_PO_DEST = "po"
|
||||
_RC_RDR_TEMPLATE_DEST = "template"
|
||||
_RC_RDR_OUTPUT_DEST = "output"
|
||||
|
||||
_RC_UPD_POT_DEST = "pot"
|
||||
_RC_UPD_PO_DEST = "po"
|
||||
|
||||
|
||||
def _register_rc_pot_param(parser: ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
@@ -133,6 +145,30 @@ def _register_rc_render_param(parser: ArgumentParser) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _register_rc_update_param(parser: ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--pot",
|
||||
dest=_RC_UPD_POT_DEST,
|
||||
action="store",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="The path to the input POT template file.",
|
||||
metavar="FILE",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--po",
|
||||
dest=_RC_UPD_PO_DEST,
|
||||
action="extend",
|
||||
nargs="+",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="The PO files to update, each may be a glob pattern.",
|
||||
metavar="GLOB",
|
||||
)
|
||||
|
||||
|
||||
def _register_rc_param(parser: ArgumentParser) -> None:
|
||||
cmds = parser.add_subparsers(dest=_RC_SUBCMD_DEST, required=True)
|
||||
pot_subcmd = cmds.add_parser(
|
||||
@@ -144,6 +180,10 @@ def _register_rc_param(parser: ArgumentParser) -> None:
|
||||
help="Render a user-provided template with manifest data and PO translations.",
|
||||
)
|
||||
_register_rc_render_param(render_subcmd)
|
||||
update_subcmd = cmds.add_parser(
|
||||
RcCommand.Update.value, help="Update PO files against a POT template."
|
||||
)
|
||||
_register_rc_update_param(update_subcmd)
|
||||
|
||||
|
||||
def _register_param(parser: ArgumentParser) -> None:
|
||||
@@ -187,6 +227,11 @@ def parse() -> Opts:
|
||||
args[_RC_RDR_OUTPUT_DEST],
|
||||
)
|
||||
rc_opts = RcOpts(rc_rdr_opts)
|
||||
case RcCommand.Update:
|
||||
rc_upd_opts = RcUpdateOpts(
|
||||
args[_RC_UPD_POT_DEST], args[_RC_UPD_PO_DEST]
|
||||
)
|
||||
rc_opts = RcOpts(rc_upd_opts)
|
||||
case _:
|
||||
raise ValueError(f"unhandled rc command: {args[_RC_SUBCMD_DEST]}")
|
||||
opts = Opts(rc_opts)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from ...cli import RcOpts, RcPotOpts, RcRenderOpts
|
||||
from . import pot, render
|
||||
from ...cli import RcOpts, RcPotOpts, RcRenderOpts, RcUpdateOpts
|
||||
from . import pot, render, update
|
||||
|
||||
def run(opts: RcOpts) -> None:
|
||||
match opts.opts:
|
||||
@@ -7,6 +7,7 @@ def run(opts: RcOpts) -> None:
|
||||
pot.run(pot_opts)
|
||||
case RcRenderOpts() as render_opts:
|
||||
render.run(render_opts)
|
||||
case RcUpdateOpts() as update_opts:
|
||||
update.run(update_opts)
|
||||
case _:
|
||||
raise RuntimeError(f"unhandled rc options: {opts.opts!r}")
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from ...cli import RcUpdateOpts
|
||||
from ...pofile import resolve_glob_files, update_po_files
|
||||
|
||||
|
||||
def run(opts: RcUpdateOpts) -> None:
|
||||
update_po_files(opts.in_pot, resolve_glob_files(opts.in_po))
|
||||
+77
-49
@@ -1,4 +1,6 @@
|
||||
import glob
|
||||
import os
|
||||
import subprocess
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
@@ -48,6 +50,58 @@ def resolve_glob_files(patterns: Iterable[Path]) -> Iterator[Path]:
|
||||
yield name.resolve()
|
||||
|
||||
|
||||
def _load_po(path: Path) -> polib.POFile:
|
||||
"""Load and parse a PO file.
|
||||
|
||||
:param path: The path of the PO file to load.
|
||||
:return: The parsed PO file.
|
||||
:raises RuntimeError: If the file cannot be read or parsed.
|
||||
"""
|
||||
try:
|
||||
return polib.pofile(str(path))
|
||||
except OSError as exc:
|
||||
raise RuntimeError(f"failed to read PO file '{path}': {exc}") from exc
|
||||
|
||||
|
||||
def _extract_lang(path: Path, po: polib.POFile) -> PoLang:
|
||||
"""Extract the language of a loaded PO file.
|
||||
|
||||
The ``Language`` metadata field is tried first, then the file name
|
||||
without extension.
|
||||
|
||||
:param path: The path of the PO file, used for fallback and reporting.
|
||||
:param po: The loaded PO file.
|
||||
:return: The language of the PO file.
|
||||
:raises RuntimeError: If no valid language can be extracted.
|
||||
"""
|
||||
metadata_language = po.metadata.get("Language")
|
||||
if metadata_language is not None:
|
||||
try:
|
||||
return PoLang(metadata_language)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return PoLang(path.stem)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
raise RuntimeError(f"can't fetch valid language id in PO file '{path}'")
|
||||
|
||||
|
||||
def _translated_text(entry: Optional[polib.POEntry], fallback: str) -> str:
|
||||
"""Return the usable translation of an entry.
|
||||
|
||||
:param entry: The PO entry to read, or ``None`` when absent.
|
||||
:param fallback: The source string used when the entry is missing,
|
||||
untranslated or marked fuzzy.
|
||||
:return: The usable translation.
|
||||
"""
|
||||
if entry is None or entry.msgstr == "" or entry.fuzzy:
|
||||
return fallback
|
||||
return entry.msgstr
|
||||
|
||||
|
||||
def resolve_translations(
|
||||
manifest: Manifest, po_paths: Iterable[Path]
|
||||
) -> dict[PoLang, LanguagePack]:
|
||||
@@ -129,56 +183,30 @@ def generate_pot(manifest: Manifest, output_path: Path) -> None:
|
||||
)
|
||||
)
|
||||
po.save(str(output_path))
|
||||
logging.info("Wrote %d entries to POT file '%s'.", len(po), output_path)
|
||||
logging.info("Wrote %d entries to POT file %s.", len(po), output_path)
|
||||
|
||||
|
||||
def _load_po(path: Path) -> polib.POFile:
|
||||
"""Load and parse a PO file.
|
||||
def update_po_files(pot_path: Path, po_paths: Iterable[Path]) -> None:
|
||||
"""Update the given PO files in place against a POT template.
|
||||
|
||||
:param path: The path of the PO file to load.
|
||||
:return: The parsed PO file.
|
||||
:raises RuntimeError: If the file cannot be read or parsed.
|
||||
Each PO file is merged with the POT using the ``msgmerge`` tool from GNU
|
||||
gettext: entries are added, removed and marked fuzzy as needed, and the
|
||||
result is written back over the PO file without keeping a backup.
|
||||
|
||||
The msgmerge binary is looked up on PATH; set the ``METAGLOT_MSGMERGE``
|
||||
environment variable to use a specific executable instead.
|
||||
|
||||
:param pot_path: The path of the POT template file.
|
||||
:param po_paths: The PO file paths to update.
|
||||
:raises RuntimeError: If msgmerge fails on a PO file.
|
||||
"""
|
||||
try:
|
||||
return polib.pofile(str(path))
|
||||
except OSError as exc:
|
||||
raise RuntimeError(f"failed to read PO file '{path}': {exc}") from exc
|
||||
|
||||
|
||||
def _extract_lang(path: Path, po: polib.POFile) -> PoLang:
|
||||
"""Extract the language of a loaded PO file.
|
||||
|
||||
The ``Language`` metadata field is tried first, then the file name
|
||||
without extension.
|
||||
|
||||
:param path: The path of the PO file, used for fallback and reporting.
|
||||
:param po: The loaded PO file.
|
||||
:return: The language of the PO file.
|
||||
:raises RuntimeError: If no valid language can be extracted.
|
||||
"""
|
||||
metadata_language = po.metadata.get("Language")
|
||||
if metadata_language is not None:
|
||||
try:
|
||||
return PoLang(metadata_language)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return PoLang(path.stem)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
raise RuntimeError(f"can't fetch valid language id in PO file '{path}'")
|
||||
|
||||
|
||||
def _translated_text(entry: Optional[polib.POEntry], fallback: str) -> str:
|
||||
"""Return the usable translation of an entry.
|
||||
|
||||
:param entry: The PO entry to read, or ``None`` when absent.
|
||||
:param fallback: The source string used when the entry is missing,
|
||||
untranslated or marked fuzzy.
|
||||
:return: The usable translation.
|
||||
"""
|
||||
if entry is None or entry.msgstr == "" or entry.fuzzy:
|
||||
return fallback
|
||||
return entry.msgstr
|
||||
msgmerge_bin = os.getenv("METAGLOT_MSGMERGE", "msgmerge")
|
||||
for po_path in po_paths:
|
||||
logging.info("Updating PO by POT: %s -> %s", pot_path, po_path)
|
||||
cmd = [msgmerge_bin, "-U", str(po_path), str(pot_path), "--backup=none"]
|
||||
proc = subprocess.run(cmd, capture_output=False)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"failed to update PO file '{po_path}' with msgmerge "
|
||||
f"(return code {proc.returncode})"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user