1 changed files with 0 additions and 215 deletions
@ -1,215 +0,0 @@ |
|||||||
#!/usr/bin/env python3 |
|
||||||
|
|
||||||
from __future__ import annotations |
|
||||||
|
|
||||||
import argparse |
|
||||||
import re |
|
||||||
import struct |
|
||||||
from pathlib import Path |
|
||||||
|
|
||||||
|
|
||||||
_ESCAPE_RE = re.compile(r"\\(n|t|r|\\|\"|[0-7]{1,3}|x[0-9a-fA-F]{2})") |
|
||||||
|
|
||||||
|
|
||||||
def _unescape(value: str) -> str: |
|
||||||
def repl(match: re.Match[str]) -> str: |
|
||||||
escape = match.group(1) |
|
||||||
if escape == "n": |
|
||||||
return "\n" |
|
||||||
if escape == "t": |
|
||||||
return "\t" |
|
||||||
if escape == "r": |
|
||||||
return "\r" |
|
||||||
if escape == "\\": |
|
||||||
return "\\" |
|
||||||
if escape == '"': |
|
||||||
return '"' |
|
||||||
if escape.startswith("x"): |
|
||||||
return chr(int(escape[1:], 16)) |
|
||||||
return chr(int(escape, 8)) |
|
||||||
|
|
||||||
return _ESCAPE_RE.sub(repl, value) |
|
||||||
|
|
||||||
|
|
||||||
def _parse_quoted(rest: str) -> str: |
|
||||||
rest = rest.strip() |
|
||||||
if not (rest.startswith('"') and rest.endswith('"')): |
|
||||||
raise ValueError(f"Invalid PO string: {rest!r}") |
|
||||||
return _unescape(rest[1:-1]) |
|
||||||
|
|
||||||
|
|
||||||
def parse_po(path: Path) -> dict[str, str]: |
|
||||||
messages: list[tuple[str | None, str, str | None, dict[int, str], set[str]]] = [] |
|
||||||
|
|
||||||
msgctxt: str | None = None |
|
||||||
msgid: str | None = None |
|
||||||
msgid_plural: str | None = None |
|
||||||
msgstr: dict[int, str] = {} |
|
||||||
flags: set[str] = set() |
|
||||||
active: tuple[str, int | None] | None = None |
|
||||||
|
|
||||||
def flush() -> None: |
|
||||||
nonlocal msgctxt, msgid, msgid_plural, msgstr, flags, active |
|
||||||
if msgid is None: |
|
||||||
msgctxt = None |
|
||||||
msgid_plural = None |
|
||||||
msgstr = {} |
|
||||||
flags = set() |
|
||||||
active = None |
|
||||||
return |
|
||||||
|
|
||||||
messages.append((msgctxt, msgid, msgid_plural, dict(msgstr), set(flags))) |
|
||||||
|
|
||||||
msgctxt = None |
|
||||||
msgid = None |
|
||||||
msgid_plural = None |
|
||||||
msgstr = {} |
|
||||||
flags = set() |
|
||||||
active = None |
|
||||||
|
|
||||||
with path.open("r", encoding="utf-8", errors="replace", newline="") as file: |
|
||||||
for raw_line in file: |
|
||||||
line = raw_line.rstrip("\n") |
|
||||||
|
|
||||||
if not line.strip(): |
|
||||||
flush() |
|
||||||
continue |
|
||||||
|
|
||||||
if line.startswith("#,"): |
|
||||||
for flag in line[2:].split(","): |
|
||||||
flag = flag.strip() |
|
||||||
if flag: |
|
||||||
flags.add(flag) |
|
||||||
continue |
|
||||||
|
|
||||||
if line.startswith("#"): |
|
||||||
continue |
|
||||||
|
|
||||||
if line.startswith("msgctxt"): |
|
||||||
msgctxt = _parse_quoted(line[len("msgctxt") :]) |
|
||||||
active = ("msgctxt", None) |
|
||||||
continue |
|
||||||
|
|
||||||
if line.startswith("msgid_plural"): |
|
||||||
msgid_plural = _parse_quoted(line[len("msgid_plural") :]) |
|
||||||
active = ("msgid_plural", None) |
|
||||||
continue |
|
||||||
|
|
||||||
if line.startswith("msgid"): |
|
||||||
msgid = _parse_quoted(line[len("msgid") :]) |
|
||||||
active = ("msgid", None) |
|
||||||
continue |
|
||||||
|
|
||||||
if line.startswith("msgstr["): |
|
||||||
close = line.find("]") |
|
||||||
index = int(line[len("msgstr[") : close]) |
|
||||||
msgstr[index] = _parse_quoted(line[close + 1 :]) |
|
||||||
active = ("msgstr", index) |
|
||||||
continue |
|
||||||
|
|
||||||
if line.startswith("msgstr"): |
|
||||||
msgstr[0] = _parse_quoted(line[len("msgstr") :]) |
|
||||||
active = ("msgstr", 0) |
|
||||||
continue |
|
||||||
|
|
||||||
if line.lstrip().startswith('"'): |
|
||||||
value = _parse_quoted(line) |
|
||||||
if active is None: |
|
||||||
continue |
|
||||||
kind, index = active |
|
||||||
if kind == "msgctxt": |
|
||||||
msgctxt = (msgctxt or "") + value |
|
||||||
elif kind == "msgid": |
|
||||||
msgid = (msgid or "") + value |
|
||||||
elif kind == "msgid_plural": |
|
||||||
msgid_plural = (msgid_plural or "") + value |
|
||||||
elif kind == "msgstr": |
|
||||||
assert index is not None |
|
||||||
msgstr[index] = msgstr.get(index, "") + value |
|
||||||
continue |
|
||||||
|
|
||||||
flush() |
|
||||||
|
|
||||||
catalog: dict[str, str] = {} |
|
||||||
for msgctxt, msgid, msgid_plural, msgstrs, flags in messages: |
|
||||||
if "fuzzy" in flags: |
|
||||||
continue |
|
||||||
|
|
||||||
if msgid_plural is not None: |
|
||||||
key = msgid + "\x00" + msgid_plural |
|
||||||
max_index = max(msgstrs.keys(), default=0) |
|
||||||
value = "\x00".join(msgstrs.get(i, "") for i in range(max_index + 1)) |
|
||||||
else: |
|
||||||
key = msgid |
|
||||||
value = msgstrs.get(0, "") |
|
||||||
|
|
||||||
if msgctxt: |
|
||||||
key = msgctxt + "\x04" + key |
|
||||||
|
|
||||||
catalog[key] = value |
|
||||||
|
|
||||||
catalog.setdefault("", "") |
|
||||||
return catalog |
|
||||||
|
|
||||||
|
|
||||||
def write_mo(catalog: dict[str, str], out_file: Path) -> None: |
|
||||||
entries = sorted(catalog.items(), key=lambda kv: kv[0]) |
|
||||||
ids = [key.encode("utf-8") for key, _ in entries] |
|
||||||
strs = [value.encode("utf-8") for _, value in entries] |
|
||||||
|
|
||||||
count = len(entries) |
|
||||||
header_size = 7 * 4 |
|
||||||
table_size = count * 8 |
|
||||||
originals_offset = header_size |
|
||||||
translations_offset = originals_offset + table_size |
|
||||||
string_offset = translations_offset + table_size |
|
||||||
|
|
||||||
offsets_ids: list[tuple[int, int]] = [] |
|
||||||
offsets_strs: list[tuple[int, int]] = [] |
|
||||||
pool = bytearray() |
|
||||||
|
|
||||||
for value in ids: |
|
||||||
offsets_ids.append((len(value), string_offset + len(pool))) |
|
||||||
pool.extend(value) |
|
||||||
pool.append(0) |
|
||||||
|
|
||||||
for value in strs: |
|
||||||
offsets_strs.append((len(value), string_offset + len(pool))) |
|
||||||
pool.extend(value) |
|
||||||
pool.append(0) |
|
||||||
|
|
||||||
out_file.parent.mkdir(parents=True, exist_ok=True) |
|
||||||
with out_file.open("wb") as file: |
|
||||||
file.write( |
|
||||||
struct.pack( |
|
||||||
"<Iiiiiii", |
|
||||||
0x950412DE, # magic |
|
||||||
0, # version |
|
||||||
count, |
|
||||||
originals_offset, |
|
||||||
translations_offset, |
|
||||||
0, # hash table size |
|
||||||
0, # hash table offset |
|
||||||
) |
|
||||||
) |
|
||||||
for length, offset in offsets_ids: |
|
||||||
file.write(struct.pack("<II", length, offset)) |
|
||||||
for length, offset in offsets_strs: |
|
||||||
file.write(struct.pack("<II", length, offset)) |
|
||||||
file.write(pool) |
|
||||||
|
|
||||||
|
|
||||||
def main() -> int: |
|
||||||
parser = argparse.ArgumentParser(description="Compile a .po file into a GNU .mo/.gmo file.") |
|
||||||
parser.add_argument("input", type=Path, help="Input .po file") |
|
||||||
parser.add_argument("-o", "--output", type=Path, required=True, help="Output .mo/.gmo file") |
|
||||||
args = parser.parse_args() |
|
||||||
|
|
||||||
catalog = parse_po(args.input) |
|
||||||
write_mo(catalog, args.output) |
|
||||||
return 0 |
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": |
|
||||||
raise SystemExit(main()) |
|
||||||
|
|
||||||
Loading…
Reference in new issue