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
This commit is contained in:
Softer 2026-05-18 13:10:56 +03:00 committed by GitHub
parent 22bf6e3c35
commit dccd0c8ccf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 106 additions and 26 deletions

View File

@ -0,0 +1,22 @@
name: Translation validation
on:
push:
paths:
- 'archinstall/**/*.py'
- 'archinstall/locales/**'
- '.github/workflows/translation-check.yaml'
pull_request:
paths:
- 'archinstall/**/*.py'
- 'archinstall/locales/**'
- '.github/workflows/translation-check.yaml'
jobs:
translations:
name: Validate translations
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install gettext
run: sudo apt-get update && sudo apt-get install -y gettext
- name: Run translation checks
run: bash archinstall/locales/locales_generator.sh check

View File

@ -502,7 +502,7 @@ class GlobalMenu(AbstractMenu[None]):
return text[:-1] # remove last new line
if error := self._validate_bootloader():
return tr(f'Invalid configuration: {error}')
return tr('Invalid configuration: {}').format(error)
self.sync_all_to_config()
summary = ConfigurationOutput(self._arch_config).as_summary()

View File

@ -1245,7 +1245,7 @@ msgid "Product"
msgstr ""
#, python-brace-format
msgid "Invalid configuration: {error}"
msgid "Invalid configuration: {}"
msgstr ""
msgid "Ready to install"

View File

@ -2190,7 +2190,7 @@ msgstr "Valittu työpöydän profiili vaati tavallisen käyttäjän kirjautumise
#, python-brace-format
msgid "Environment type: {} {}"
msgstr "Ympäristötyyppi: {}"
msgstr "Ympäristötyyppi: {} {}"
msgid "Input cannot be empty"
msgstr "Syöttö ei voi olla tyhjä"
@ -2245,4 +2245,4 @@ msgstr "Määritetään U2F laitetta käyttäjälle: {}"
#, python-brace-format
msgid "Default: {}ms, Recommended range: 1000-60000"
msgstr "Oletusarvo: 10000ms, suositeltu 1000-60000ms"
msgstr "Oletusarvo: {}ms, suositeltu 1000-60000ms"

View File

@ -1502,9 +1502,6 @@ msgstr "सक्षम"
msgid "Disabled"
msgstr "अक्षम"
msgid "Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues"
msgstr "कृपया इस समस्या (और फ़ाइल) को https://github.com/archlinux/archinstall/issues पर सबमिट करें।"
msgid "Mirror name"
msgstr "मिरर का नाम"

View File

@ -1,48 +1,109 @@
#!/usr/bin/env bash
set -euo pipefail
cd $(dirname "$0")/..
cd "$(dirname "$0")/.."
function update_lang() {
file=${1}
usage() {
echo "Usage: ${0} <command>"
echo ""
echo "Commands:"
echo " all Regenerate base.pot and update all languages"
echo " <lang> Regenerate base.pot and update a single language"
echo " check Run translation validation checks"
echo " -h, --help Show this help"
}
generate_pot() {
find . -type f -iname '*.py' | sort \
| xargs xgettext --no-location --omit-header --keyword='tr' \
-d base -o locales/base.pot
}
update_lang() {
local file=${1}
echo "Updating: ${file}"
local path
path=$(dirname "${file}")
msgmerge --quiet --no-location --width 512 --backup none --update "${file}" locales/base.pot
msgfmt -o "${path}/base.mo" "${file}"
}
function generate_all() {
cmd_generate_all() {
generate_pot
for file in $(find locales/ -name "base.po"); do
update_lang "${file}"
done
}
function generate_single_lang() {
lang_file="locales/${1}/LC_MESSAGES/base.po"
cmd_generate_single() {
local lang_file="locales/${1}/LC_MESSAGES/base.po"
if [ ! -f "${lang_file}" ]; then
echo "Language files not found: ${lang_file}"
exit 1
fi
generate_pot
update_lang "${lang_file}"
}
cmd_check_po_syntax() {
echo "Checking .po syntax..."
local failed=0
while IFS= read -r po; do
if ! msgfmt --check --output-file=/dev/null "$po" 2>&1; then
echo "FAIL: $po"
failed=1
fi
done < <(find locales/ -name '*.po')
if [ "$failed" -eq 1 ]; then
echo "ERROR: some .po files have syntax errors" >&2
return 1
fi
echo "All .po files passed syntax check."
}
cmd_check_no_tr_fstring() {
echo "Checking for tr(f-string) anti-pattern..."
if grep -rnE "tr\(\s*f['\"]" . --include='*.py'; then
echo "ERROR: use tr('...{}').format(...) instead of tr(f'...')" >&2
return 1
fi
echo "No tr(f-string) anti-pattern found."
}
cmd_check_pot_freshness() {
# msgcmp (not diff) because base.pot carries legacy stale entries from
# --join-existing; diff would always fail until a full cleanup is done.
echo "Checking base.pot for missing strings..."
find . -type f -iname '*.py' | sort \
| xargs xgettext --no-location --omit-header --keyword='tr' \
-d base -o /tmp/generated.pot
if ! msgcmp --use-untranslated locales/base.pot /tmp/generated.pot; then
echo "ERROR: base.pot is missing strings - run: locales_generator.sh all" >&2
return 1
fi
echo "base.pot contains all translatable strings."
}
cmd_check() {
local failed=0
cmd_check_po_syntax || failed=1
cmd_check_no_tr_fstring || failed=1
cmd_check_pot_freshness || failed=1
if [ "$failed" -eq 1 ]; then
echo "Some translation checks failed." >&2
exit 1
fi
echo "All translation checks passed."
}
if [ $# -eq 0 ]; then
echo "Usage: ${0} <language_abbr>"
echo "Special case 'all' for <language_abbr> builds all languages."
usage
exit 1
fi
lang=${1}
# Update the base file containing all translatable strings
find . -type f -iname "*.py" | xargs xgettext --join-existing --no-location --omit-header --keyword='tr' -d base -o locales/base.pot
case "${lang}" in
"all") generate_all;;
*) generate_single_lang "${lang}"
case "${1}" in
check) cmd_check ;;
all) cmd_generate_all ;;
-h|--help) usage ;;
*) cmd_generate_single "${1}" ;;
esac