Skip to content

Commit dccd0c8

Browse files
authored
Add translation CI validation (#4519)
* Fix broken localization: tr(f-string) never matches translation catalog tr(f'Invalid configuration: {error}') evaluates the f-string before tr() runs, so xgettext extracts the literal placeholder as the msgid while runtime passes the formatted string - the two never match. Switch to tr('...{}').format(...) and update msgid in base.pot. * Add CI validation for translations and pot_tools dev utility Add translation-check workflow with two jobs: - validate-po: msgfmt --check on changed .po files, .mo sync warning, tr(f-string) anti-pattern grep on changed .py files - validate-pot: verify all tr() strings exist in base.pot when .py files change Workflow only triggers on .py/.po/.pot file changes. Add scripts/pot_tools.py developer utility (stats, list, add_missing) for managing base.pot. * Fix code style: use tabs and reformat xgettext arguments Align check_pot_freshness.py and pot_tools.py with project indentation (tabs) and ruff format requirements. Sorry :-) * Replace custom PO parser with msgcmp, drop pot_tools.py Address review feedback: use standard gettext msgcmp instead of hand-rolled parser for base.pot freshness check. Remove pot_tools.py that duplicated locales_generator.sh functionality. * Move translation checks into locales_generator.sh, simplify CI workflow Use msgcmp instead of diff for base.pot validation to avoid failing on legacy stale entries - the same cascading breakage that killed the original workflow (disabled 2023, removed in #4483). * Fix broken .po files: duplicate msgid in Hindi, missing format args in Finnish
1 parent 22bf6e3 commit dccd0c8

6 files changed

Lines changed: 106 additions & 26 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Translation validation
2+
on:
3+
push:
4+
paths:
5+
- 'archinstall/**/*.py'
6+
- 'archinstall/locales/**'
7+
- '.github/workflows/translation-check.yaml'
8+
pull_request:
9+
paths:
10+
- 'archinstall/**/*.py'
11+
- 'archinstall/locales/**'
12+
- '.github/workflows/translation-check.yaml'
13+
jobs:
14+
translations:
15+
name: Validate translations
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
19+
- name: Install gettext
20+
run: sudo apt-get update && sudo apt-get install -y gettext
21+
- name: Run translation checks
22+
run: bash archinstall/locales/locales_generator.sh check

archinstall/lib/global_menu.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ def _prev_install_invalid_config(self, item: MenuItem) -> str | None:
502502
return text[:-1] # remove last new line
503503

504504
if error := self._validate_bootloader():
505-
return tr(f'Invalid configuration: {error}')
505+
return tr('Invalid configuration: {}').format(error)
506506

507507
self.sync_all_to_config()
508508
summary = ConfigurationOutput(self._arch_config).as_summary()

archinstall/locales/base.pot

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1245,7 +1245,7 @@ msgid "Product"
12451245
msgstr ""
12461246

12471247
#, python-brace-format
1248-
msgid "Invalid configuration: {error}"
1248+
msgid "Invalid configuration: {}"
12491249
msgstr ""
12501250

12511251
msgid "Ready to install"

archinstall/locales/fi/LC_MESSAGES/base.po

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2190,7 +2190,7 @@ msgstr "Valittu työpöydän profiili vaati tavallisen käyttäjän kirjautumise
21902190

21912191
#, python-brace-format
21922192
msgid "Environment type: {} {}"
2193-
msgstr "Ympäristötyyppi: {}"
2193+
msgstr "Ympäristötyyppi: {} {}"
21942194

21952195
msgid "Input cannot be empty"
21962196
msgstr "Syöttö ei voi olla tyhjä"
@@ -2245,4 +2245,4 @@ msgstr "Määritetään U2F laitetta käyttäjälle: {}"
22452245

22462246
#, python-brace-format
22472247
msgid "Default: {}ms, Recommended range: 1000-60000"
2248-
msgstr "Oletusarvo: 10000ms, suositeltu 1000-60000ms"
2248+
msgstr "Oletusarvo: {}ms, suositeltu 1000-60000ms"

archinstall/locales/hi/LC_MESSAGES/base.po

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1502,9 +1502,6 @@ msgstr "सक्षम"
15021502
msgid "Disabled"
15031503
msgstr "अक्षम"
15041504

1505-
msgid "Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues"
1506-
msgstr "कृपया इस समस्या (और फ़ाइल) को https://github.com/archlinux/archinstall/issues पर सबमिट करें।"
1507-
15081505
msgid "Mirror name"
15091506
msgstr "मिरर का नाम"
15101507

Lines changed: 80 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,109 @@
11
#!/usr/bin/env bash
22
set -euo pipefail
33

4-
cd $(dirname "$0")/..
4+
cd "$(dirname "$0")/.."
55

6-
function update_lang() {
7-
file=${1}
6+
usage() {
7+
echo "Usage: ${0} <command>"
8+
echo ""
9+
echo "Commands:"
10+
echo " all Regenerate base.pot and update all languages"
11+
echo " <lang> Regenerate base.pot and update a single language"
12+
echo " check Run translation validation checks"
13+
echo " -h, --help Show this help"
14+
}
15+
16+
generate_pot() {
17+
find . -type f -iname '*.py' | sort \
18+
| xargs xgettext --no-location --omit-header --keyword='tr' \
19+
-d base -o locales/base.pot
20+
}
821

22+
update_lang() {
23+
local file=${1}
924
echo "Updating: ${file}"
25+
local path
1026
path=$(dirname "${file}")
1127
msgmerge --quiet --no-location --width 512 --backup none --update "${file}" locales/base.pot
1228
msgfmt -o "${path}/base.mo" "${file}"
1329
}
1430

15-
16-
function generate_all() {
31+
cmd_generate_all() {
32+
generate_pot
1733
for file in $(find locales/ -name "base.po"); do
1834
update_lang "${file}"
1935
done
2036
}
2137

22-
function generate_single_lang() {
23-
lang_file="locales/${1}/LC_MESSAGES/base.po"
24-
38+
cmd_generate_single() {
39+
local lang_file="locales/${1}/LC_MESSAGES/base.po"
2540
if [ ! -f "${lang_file}" ]; then
2641
echo "Language files not found: ${lang_file}"
2742
exit 1
2843
fi
29-
44+
generate_pot
3045
update_lang "${lang_file}"
3146
}
3247

48+
cmd_check_po_syntax() {
49+
echo "Checking .po syntax..."
50+
local failed=0
51+
while IFS= read -r po; do
52+
if ! msgfmt --check --output-file=/dev/null "$po" 2>&1; then
53+
echo "FAIL: $po"
54+
failed=1
55+
fi
56+
done < <(find locales/ -name '*.po')
57+
if [ "$failed" -eq 1 ]; then
58+
echo "ERROR: some .po files have syntax errors" >&2
59+
return 1
60+
fi
61+
echo "All .po files passed syntax check."
62+
}
63+
64+
cmd_check_no_tr_fstring() {
65+
echo "Checking for tr(f-string) anti-pattern..."
66+
if grep -rnE "tr\(\s*f['\"]" . --include='*.py'; then
67+
echo "ERROR: use tr('...{}').format(...) instead of tr(f'...')" >&2
68+
return 1
69+
fi
70+
echo "No tr(f-string) anti-pattern found."
71+
}
72+
73+
cmd_check_pot_freshness() {
74+
# msgcmp (not diff) because base.pot carries legacy stale entries from
75+
# --join-existing; diff would always fail until a full cleanup is done.
76+
echo "Checking base.pot for missing strings..."
77+
find . -type f -iname '*.py' | sort \
78+
| xargs xgettext --no-location --omit-header --keyword='tr' \
79+
-d base -o /tmp/generated.pot
80+
if ! msgcmp --use-untranslated locales/base.pot /tmp/generated.pot; then
81+
echo "ERROR: base.pot is missing strings - run: locales_generator.sh all" >&2
82+
return 1
83+
fi
84+
echo "base.pot contains all translatable strings."
85+
}
86+
87+
cmd_check() {
88+
local failed=0
89+
cmd_check_po_syntax || failed=1
90+
cmd_check_no_tr_fstring || failed=1
91+
cmd_check_pot_freshness || failed=1
92+
if [ "$failed" -eq 1 ]; then
93+
echo "Some translation checks failed." >&2
94+
exit 1
95+
fi
96+
echo "All translation checks passed."
97+
}
3398

3499
if [ $# -eq 0 ]; then
35-
echo "Usage: ${0} <language_abbr>"
36-
echo "Special case 'all' for <language_abbr> builds all languages."
100+
usage
37101
exit 1
38102
fi
39103

40-
lang=${1}
41-
42-
# Update the base file containing all translatable strings
43-
find . -type f -iname "*.py" | xargs xgettext --join-existing --no-location --omit-header --keyword='tr' -d base -o locales/base.pot
44-
45-
case "${lang}" in
46-
"all") generate_all;;
47-
*) generate_single_lang "${lang}"
104+
case "${1}" in
105+
check) cmd_check ;;
106+
all) cmd_generate_all ;;
107+
-h|--help) usage ;;
108+
*) cmd_generate_single "${1}" ;;
48109
esac

0 commit comments

Comments
 (0)