Skip to content

Commit 65e9d93

Browse files
committed
use python based msgfmt
1 parent d959ef1 commit 65e9d93

File tree

9 files changed

+259
-30
lines changed

9 files changed

+259
-30
lines changed

.github/actions/deps/external/action.yml

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,6 @@ inputs:
1212
- riscv
1313
- none
1414

15-
apt:
16-
required: false
17-
default: true
18-
type: boolean
19-
20-
python:
21-
required: false
22-
default: true
23-
type: boolean
24-
2515
runs:
2616
using: composite
2717
steps:
@@ -85,15 +75,10 @@ runs:
8575

8676
# common
8777
- name: Cache python dependencies
88-
if: inputs.python == 'true' && inputs.platform != 'espressif'
78+
if: inputs.platform != 'espressif'
8979
uses: ./.github/actions/deps/python
9080
with:
9181
action: ${{ fromJSON('["restore", "cache"]')[github.job == 'scheduler'] }}
9282
- name: Install python dependencies
93-
if: inputs.python == 'true'
9483
run: pip install -r requirements-dev.txt
9584
shell: bash
96-
- name: Install dependencies
97-
if: inputs.apt == 'true'
98-
run: sudo apt-get install -y gettext
99-
shell: bash

.github/actions/deps/python/action.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,29 @@ runs:
1414
using: composite
1515
steps:
1616
- name: Cache python dependencies
17-
if: ${{ inputs.action == 'cache' }}
17+
id: cache-python-deps
18+
if: inputs.action == 'cache'
1819
uses: actions/cache@v3
1920
with:
2021
path: .cp_tools
2122
key: ${{ runner.os }}-${{ env.pythonLocation }}-tools-cp-${{ hashFiles('requirements-dev.txt') }}
2223

2324
- name: Restore python dependencies
24-
if: ${{ inputs.action == 'restore' }}
25+
id: restore-python-deps
26+
if: inputs.action == 'restore'
2527
uses: actions/cache/restore@v3
2628
with:
2729
path: .cp_tools
2830
key: ${{ runner.os }}-${{ env.pythonLocation }}-tools-cp-${{ hashFiles('requirements-dev.txt') }}
2931

3032
- name: Set up venv
33+
if: inputs.action == 'cache' && !steps.cache-python-deps.outputs.cache-hit
34+
run: python -m venv .cp_tools
35+
shell: bash
36+
37+
- name: Activate venv
38+
if: inputs.action == 'cache' || (inputs.action == 'restore' && steps.restore-python-deps.outputs.cache-hit)
3139
run: |
32-
python -m venv .cp_tools
3340
source .cp_tools/bin/activate
3441
echo >> $GITHUB_PATH "$PATH"
3542
shell: bash

.github/workflows/build-mpy-cross.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,6 @@ jobs:
3535
uses: ./.github/actions/deps/submodules
3636
with:
3737
target: mpy-cross
38-
- name: Set up external
39-
uses: ./.github/actions/deps/external
40-
with:
41-
python: false
4238

4339
- name: Install toolchain (aarch64)
4440
if: matrix.mpy-cross == 'static-aarch64'

.github/workflows/build.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,7 @@ jobs:
179179
uses: ./.github/actions/deps/submodules
180180
- name: Install dependencies
181181
run: |
182-
sudo apt-get update
183-
sudo apt-get install -y eatmydata
184-
sudo eatmydata apt-get install -y latexmk librsvg2-bin texlive-fonts-recommended texlive-latex-recommended texlive-latex-extra
182+
sudo apt-get install -y latexmk librsvg2-bin texlive-fonts-recommended texlive-latex-recommended texlive-latex-extra
185183
pip install -r requirements-doc.txt
186184
- name: Build and Validate Stubs
187185
run: make check-stubs -j2

.github/workflows/create_website_pr.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ jobs:
3131
version: true
3232
- name: Set up external
3333
uses: ./.github/actions/deps/external
34-
with:
35-
apt: false
3634
- name: Versions
3735
run: |
3836
gcc --version

.github/workflows/pre-commit.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
- name: Set up external
3131
uses: ./.github/actions/deps/external
3232
- name: Install dependencies
33-
run: sudo apt-get install -y uncrustify
33+
run: sudo apt-get install -y gettext uncrustify
3434
- name: Run pre-commit
3535
uses: pre-commit/[email protected]
3636
- name: Make patch

.github/workflows/run-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ jobs:
3737
with:
3838
target: tests
3939
- name: Set up external
40+
if: matrix.test == 'all'
4041
uses: ./.github/actions/deps/external
4142
- name: Set up mpy-cross
4243
uses: ./.github/actions/mpy_cross

py/py.mk

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ $(HEADER_BUILD)/mpversion.h: FORCE | $(HEADER_BUILD)
243243
MPCONFIGPORT_MK = $(wildcard mpconfigport.mk)
244244

245245
$(HEADER_BUILD)/$(TRANSLATION).mo: $(TOP)/locale/$(TRANSLATION).po | $(HEADER_BUILD)
246-
$(Q)msgfmt -o $@ $^
246+
$(Q)$(PYTHON) $(TOP)/tools/msgfmt.py -o $@ $^
247247

248248
$(HEADER_BUILD)/qstrdefs.preprocessed.h: $(PY_QSTR_DEFS) $(QSTR_DEFS) $(QSTR_DEFS_COLLECTED) mpconfigport.h $(MPCONFIGPORT_MK) $(PY_SRC)/mpconfig.h | $(HEADER_BUILD)
249249
$(STEPECHO) "GEN $@"

tools/msgfmt.py

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
#! /usr/bin/env python3
2+
# Written by Martin v. Löwis <[email protected]>
3+
4+
"""Generate binary message catalog from textual translation description.
5+
This program converts a textual Uniforum-style message catalog (.po file) into
6+
a binary GNU catalog (.mo file). This is essentially the same function as the
7+
GNU msgfmt program, however, it is a simpler implementation. Currently it
8+
does not handle plural forms but it does handle message contexts.
9+
Usage: msgfmt.py [OPTIONS] filename.po
10+
Options:
11+
-o file
12+
--output-file=file
13+
Specify the output file to write to. If omitted, output will go to a
14+
file named filename.mo (based off the input file name).
15+
-h
16+
--help
17+
Print this message and exit.
18+
-V
19+
--version
20+
Display version information and exit.
21+
"""
22+
23+
import os
24+
import sys
25+
import ast
26+
import getopt
27+
import struct
28+
import array
29+
from email.parser import HeaderParser
30+
31+
__version__ = "1.2"
32+
33+
MESSAGES = {}
34+
35+
36+
def usage(code, msg=""):
37+
print(__doc__, file=sys.stderr)
38+
if msg:
39+
print(msg, file=sys.stderr)
40+
sys.exit(code)
41+
42+
43+
def add(ctxt, id, str, fuzzy):
44+
"Add a non-fuzzy translation to the dictionary."
45+
global MESSAGES
46+
if not fuzzy and str:
47+
if ctxt is None:
48+
MESSAGES[id] = str
49+
else:
50+
MESSAGES[b"%b\x04%b" % (ctxt, id)] = str
51+
52+
53+
def generate():
54+
"Return the generated output."
55+
global MESSAGES
56+
# the keys are sorted in the .mo file
57+
keys = sorted(MESSAGES.keys())
58+
offsets = []
59+
ids = strs = b""
60+
for id in keys:
61+
# For each string, we need size and file offset. Each string is NUL
62+
# terminated; the NUL does not count into the size.
63+
offsets.append((len(ids), len(id), len(strs), len(MESSAGES[id])))
64+
ids += id + b"\0"
65+
strs += MESSAGES[id] + b"\0"
66+
output = ""
67+
# The header is 7 32-bit unsigned integers. We don't use hash tables, so
68+
# the keys start right after the index tables.
69+
# translated string.
70+
keystart = 7 * 4 + 16 * len(keys)
71+
# and the values start after the keys
72+
valuestart = keystart + len(ids)
73+
koffsets = []
74+
voffsets = []
75+
# The string table first has the list of keys, then the list of values.
76+
# Each entry has first the size of the string, then the file offset.
77+
for o1, l1, o2, l2 in offsets:
78+
koffsets += [l1, o1 + keystart]
79+
voffsets += [l2, o2 + valuestart]
80+
offsets = koffsets + voffsets
81+
output = struct.pack(
82+
"Iiiiiii",
83+
0x950412DE, # Magic
84+
0, # Version
85+
len(keys), # # of entries
86+
7 * 4, # start of key index
87+
7 * 4 + len(keys) * 8, # start of value index
88+
0,
89+
0,
90+
) # size and offset of hash table
91+
output += array.array("i", offsets).tobytes()
92+
output += ids
93+
output += strs
94+
return output
95+
96+
97+
def make(filename, outfile):
98+
ID = 1
99+
STR = 2
100+
CTXT = 3
101+
102+
# Compute .mo name from .po name and arguments
103+
if filename.endswith(".po"):
104+
infile = filename
105+
else:
106+
infile = filename + ".po"
107+
if outfile is None:
108+
outfile = os.path.splitext(infile)[0] + ".mo"
109+
110+
try:
111+
with open(infile, "rb") as f:
112+
lines = f.readlines()
113+
except IOError as msg:
114+
print(msg, file=sys.stderr)
115+
sys.exit(1)
116+
117+
section = msgctxt = None
118+
fuzzy = 0
119+
120+
# Start off assuming Latin-1, so everything decodes without failure,
121+
# until we know the exact encoding
122+
encoding = "latin-1"
123+
124+
# Parse the catalog
125+
lno = 0
126+
for l in lines:
127+
l = l.decode(encoding)
128+
lno += 1
129+
# If we get a comment line after a msgstr, this is a new entry
130+
if l[0] == "#" and section == STR:
131+
add(msgctxt, msgid, msgstr, fuzzy)
132+
section = msgctxt = None
133+
fuzzy = 0
134+
# Record a fuzzy mark
135+
if l[:2] == "#," and "fuzzy" in l:
136+
fuzzy = 1
137+
# Skip comments
138+
if l[0] == "#":
139+
continue
140+
# Now we are in a msgid or msgctxt section, output previous section
141+
if l.startswith("msgctxt"):
142+
if section == STR:
143+
add(msgctxt, msgid, msgstr, fuzzy)
144+
section = CTXT
145+
l = l[7:]
146+
msgctxt = b""
147+
elif l.startswith("msgid") and not l.startswith("msgid_plural"):
148+
if section == STR:
149+
add(msgctxt, msgid, msgstr, fuzzy)
150+
if not msgid:
151+
# See whether there is an encoding declaration
152+
p = HeaderParser()
153+
charset = p.parsestr(msgstr.decode(encoding)).get_content_charset()
154+
if charset:
155+
encoding = charset
156+
section = ID
157+
l = l[5:]
158+
msgid = msgstr = b""
159+
is_plural = False
160+
# This is a message with plural forms
161+
elif l.startswith("msgid_plural"):
162+
if section != ID:
163+
print(
164+
"msgid_plural not preceded by msgid on %s:%d" % (infile, lno), file=sys.stderr
165+
)
166+
sys.exit(1)
167+
l = l[12:]
168+
msgid += b"\0" # separator of singular and plural
169+
is_plural = True
170+
# Now we are in a msgstr section
171+
elif l.startswith("msgstr"):
172+
section = STR
173+
if l.startswith("msgstr["):
174+
if not is_plural:
175+
print("plural without msgid_plural on %s:%d" % (infile, lno), file=sys.stderr)
176+
sys.exit(1)
177+
l = l.split("]", 1)[1]
178+
if msgstr:
179+
msgstr += b"\0" # Separator of the various plural forms
180+
else:
181+
if is_plural:
182+
print(
183+
"indexed msgstr required for plural on %s:%d" % (infile, lno),
184+
file=sys.stderr,
185+
)
186+
sys.exit(1)
187+
l = l[6:]
188+
# Skip empty lines
189+
l = l.strip()
190+
if not l:
191+
continue
192+
l = ast.literal_eval(l)
193+
if section == CTXT:
194+
msgctxt += l.encode(encoding)
195+
elif section == ID:
196+
msgid += l.encode(encoding)
197+
elif section == STR:
198+
msgstr += l.encode(encoding)
199+
else:
200+
print("Syntax error on %s:%d" % (infile, lno), "before:", file=sys.stderr)
201+
print(l, file=sys.stderr)
202+
sys.exit(1)
203+
# Add last entry
204+
if section == STR:
205+
add(msgctxt, msgid, msgstr, fuzzy)
206+
207+
# Compute output
208+
output = generate()
209+
210+
try:
211+
with open(outfile, "wb") as f:
212+
f.write(output)
213+
except IOError as msg:
214+
print(msg, file=sys.stderr)
215+
216+
217+
def main():
218+
try:
219+
opts, args = getopt.getopt(sys.argv[1:], "hVo:", ["help", "version", "output-file="])
220+
except getopt.error as msg:
221+
usage(1, msg)
222+
223+
outfile = None
224+
# parse options
225+
for opt, arg in opts:
226+
if opt in ("-h", "--help"):
227+
usage(0)
228+
elif opt in ("-V", "--version"):
229+
print("msgfmt.py", __version__)
230+
sys.exit(0)
231+
elif opt in ("-o", "--output-file"):
232+
outfile = arg
233+
# do it
234+
if not args:
235+
print("No input file given", file=sys.stderr)
236+
print("Try `msgfmt --help' for more information.", file=sys.stderr)
237+
return
238+
239+
for filename in args:
240+
make(filename, outfile)
241+
242+
243+
if __name__ == "__main__":
244+
main()

0 commit comments

Comments
 (0)