From dccd0c8ccf8d3212dd398a015c0210a5c9cff77e Mon Sep 17 00:00:00 2001 From: Softer Date: Mon, 18 May 2026 13:10:56 +0300 Subject: [PATCH] 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 --- .github/workflows/translation-check.yaml | 22 +++++ archinstall/lib/global_menu.py | 2 +- archinstall/locales/base.pot | 2 +- archinstall/locales/fi/LC_MESSAGES/base.po | 4 +- archinstall/locales/hi/LC_MESSAGES/base.po | 3 - archinstall/locales/locales_generator.sh | 99 +++++++++++++++++----- 6 files changed, 106 insertions(+), 26 deletions(-) create mode 100644 .github/workflows/translation-check.yaml diff --git a/.github/workflows/translation-check.yaml b/.github/workflows/translation-check.yaml new file mode 100644 index 00000000..15be16cf --- /dev/null +++ b/.github/workflows/translation-check.yaml @@ -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 diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index eac936bd..1e18890f 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -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() diff --git a/archinstall/locales/base.pot b/archinstall/locales/base.pot index e1681526..4db4554c 100644 --- a/archinstall/locales/base.pot +++ b/archinstall/locales/base.pot @@ -1245,7 +1245,7 @@ msgid "Product" msgstr "" #, python-brace-format -msgid "Invalid configuration: {error}" +msgid "Invalid configuration: {}" msgstr "" msgid "Ready to install" diff --git a/archinstall/locales/fi/LC_MESSAGES/base.po b/archinstall/locales/fi/LC_MESSAGES/base.po index 31cf6af2..9534151e 100644 --- a/archinstall/locales/fi/LC_MESSAGES/base.po +++ b/archinstall/locales/fi/LC_MESSAGES/base.po @@ -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" diff --git a/archinstall/locales/hi/LC_MESSAGES/base.po b/archinstall/locales/hi/LC_MESSAGES/base.po index 9d64d477..3cfe309f 100644 --- a/archinstall/locales/hi/LC_MESSAGES/base.po +++ b/archinstall/locales/hi/LC_MESSAGES/base.po @@ -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 "मिरर का नाम" diff --git a/archinstall/locales/locales_generator.sh b/archinstall/locales/locales_generator.sh index 7ce2ea19..18977dfc 100755 --- a/archinstall/locales/locales_generator.sh +++ b/archinstall/locales/locales_generator.sh @@ -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} " + echo "" + echo "Commands:" + echo " all Regenerate base.pot and update all languages" + echo " 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} " - echo "Special case 'all' for 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