Skip to content

Commit 1a174a9

Browse files
committed
lib: rewrite manifest2po in Python
Extracting translatable strings from manifests is easy enough to do in pure Python and removes an dependency on node_modules. The differences in the Python implementation versus the JavaScript version: - no longer relies on global state to gather the msgid's - doesn't encode duplicate filenames for the same msgid - no longer replaces @foo@ placeholders which where removed in 473db0e
1 parent 36c4411 commit 1a174a9

File tree

3 files changed

+109
-181
lines changed

3 files changed

+109
-181
lines changed

pkg/lib/manifest2po

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#!/usr/bin/python3
2+
# This file is part of Cockpit.
3+
#
4+
# Copyright (C) 2025 Red Hat, Inc.
5+
#
6+
# This program is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU General Public License as published by
8+
# the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
19+
import argparse
20+
import collections
21+
import collections.abc
22+
import json
23+
import pathlib
24+
from typing import Any
25+
26+
PO_HEADER = r"""msgid ""
27+
msgstr ""
28+
"Project-Id-Version: PACKAGE_VERSION\n"
29+
"MIME-Version: 1.0\n"
30+
"Content-Type: text/plain; charset=UTF-8\n"
31+
"Content-Transfer-Encoding: 8bit\n"
32+
"X-Generator: Cockpit manifest2po\n"
33+
"""
34+
35+
36+
def get_docs_strings(docs: list[dict[str, str]]) -> collections.abc.Iterable[str]:
37+
for doc in docs:
38+
assert 'label' in doc, 'doc entry without label'
39+
yield doc['label']
40+
41+
42+
def get_keyword_strings(keywords: list[dict[str, list[str]]]) -> collections.abc.Iterable[str]:
43+
for keyword in keywords:
44+
assert 'matches' in keyword, 'keywords entry without matches'
45+
for match in keyword['matches']:
46+
yield match
47+
48+
49+
def get_menu_strings(menu: dict[str, Any]) -> collections.abc.Iterable[str]:
50+
for entry in menu.values():
51+
if label := entry.get('label'):
52+
yield label
53+
if keywords := entry.get('keywords'):
54+
yield from get_keyword_strings(keywords)
55+
if docs := entry.get('docs'):
56+
yield from get_docs_strings(docs)
57+
58+
59+
def get_bridges_strings(bridges: list[dict[str, str]]) -> collections.abc.Iterable[str]:
60+
for bridge in bridges:
61+
if label := bridge.get('label'):
62+
yield label
63+
64+
65+
def get_manifest_strings(manifest: dict[str, Any]) -> collections.abc.Iterable[str]:
66+
if menu := manifest.get('menu'):
67+
yield from get_menu_strings(menu)
68+
if tools := manifest.get('tools'):
69+
yield from get_menu_strings(tools)
70+
if bridges := manifest.get('bridges'):
71+
yield from get_bridges_strings(bridges)
72+
73+
74+
def main() -> None:
75+
parser = argparse.ArgumentParser(prog='manifest2po',
76+
description='Extracts translatable strings from manifest.json files')
77+
parser.add_argument('-d', '--directory', help='Base directory for input files')
78+
parser.add_argument('-o', '--output', help='Output files', required=True)
79+
parser.add_argument('files', nargs='+', help='One or more input files', type=pathlib.Path, metavar='FILE')
80+
81+
args = parser.parse_args()
82+
strings = collections.defaultdict[str, set[str]](set)
83+
84+
files: collections.abc.Iterable[pathlib.Path] = args.files
85+
for file in files:
86+
if file.name != 'manifest.json':
87+
continue
88+
89+
# Qualify the filename if necessary
90+
full_path = args.directory / file if args.directory else file
91+
with open(full_path, 'r') as fp:
92+
manifest = json.load(fp)
93+
for msgid in get_manifest_strings(manifest):
94+
strings[msgid].add(str(file))
95+
96+
# Write PO file
97+
with open(args.output, 'w') as fp:
98+
fp.write(PO_HEADER)
99+
for msgid in strings:
100+
manifest_filenames = ' '.join([f'{fname}:0' for fname in strings[msgid]])
101+
fp.write(f"\n#: {manifest_filenames}\n")
102+
fp.write(f'msgid "{msgid}"\n')
103+
fp.write('msgstr ""\n')
104+
105+
106+
if __name__ == "__main__":
107+
main()

pkg/lib/manifest2po.js

Lines changed: 0 additions & 179 deletions
This file was deleted.

po/Makefile.am

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ po/cockpit.js.pot:
2424
$$( cd $(srcdir) && find pkg/ ! -name 'test-*' -name '*.[jt]s' -o -name '*.[jt]sx') | \
2525
sed '/^#/ s/, c-format//' > $@
2626

27-
po/cockpit.manifest.pot: $(srcdir)/package-lock.json
27+
po/cockpit.manifest.pot:
2828
$(AM_V_GEN) mkdir -p $(dir $@) && \
29-
$(srcdir)/pkg/lib/manifest2po.js -d $(srcdir) -o $@ \
29+
$(srcdir)/pkg/lib/manifest2po -d $(srcdir) -o $@ \
3030
$$(cd $(srcdir) && find pkg/ -name 'manifest.json')
3131

3232
po/cockpit.appstream.pot:

0 commit comments

Comments
 (0)