diff --git a/.github/workflows/translation-check.yaml b/.github/workflows/translation-check.yaml index f99cc330..15be16cf 100644 --- a/.github/workflows/translation-check.yaml +++ b/.github/workflows/translation-check.yaml @@ -11,128 +11,12 @@ on: - 'archinstall/locales/**' - '.github/workflows/translation-check.yaml' jobs: - validate-po: - name: Validate .po files and translation patterns + translations: + name: Validate translations runs-on: ubuntu-latest - container: - image: archlinux/archlinux:latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 0 - - name: Find changed files - id: changed - run: | - if [ "${{ github.event_name }}" = "pull_request" ]; then - base="${{ github.event.pull_request.base.sha }}" - head="${{ github.event.pull_request.head.sha }}" - else - base="${{ github.event.before }}" - head="${{ github.sha }}" - fi - changed_po=$(git diff --name-only "$base" "$head" 2>/dev/null | grep '\.po$' || true) - changed_py=$(git diff --name-only "$base" "$head" 2>/dev/null | grep '\.py$' || true) - if [ -n "$changed_po" ]; then - echo "has_po=true" >> "$GITHUB_OUTPUT" - echo "$changed_po" > /tmp/changed_po.txt - else - echo "has_po=false" >> "$GITHUB_OUTPUT" - fi - if [ -n "$changed_py" ]; then - echo "has_py=true" >> "$GITHUB_OUTPUT" - else - echo "has_py=false" >> "$GITHUB_OUTPUT" - fi - name: Install gettext - if: steps.changed.outputs.has_po == 'true' - run: | - pacman-key --init - pacman --noconfirm -Sy archlinux-keyring - pacman --noconfirm -Syyu - pacman --noconfirm -S gettext - - name: Check changed .po syntax with msgfmt - if: steps.changed.outputs.has_po == 'true' - run: | - failed=0 - while IFS= read -r po; do - if [ -f "$po" ] && ! msgfmt --check --output-file=/dev/null "$po" 2>&1; then - echo "FAIL: $po" - failed=1 - fi - done < /tmp/changed_po.txt - if [ "$failed" -eq 1 ]; then - echo "::error::Some .po files have syntax errors" - exit 1 - fi - echo "All changed .po files passed syntax check" - - name: Warn if changed .mo files are out of sync - if: steps.changed.outputs.has_po == 'true' - run: | - out_of_sync=0 - while IFS= read -r po; do - if [ ! -f "$po" ]; then continue; fi - mo="${po%.po}.mo" - if [ ! -f "$mo" ]; then - echo "::warning::Missing .mo file for $po" - out_of_sync=1 - continue - fi - tmp_mo=$(mktemp) - msgfmt --output-file="$tmp_mo" "$po" - if ! cmp -s "$mo" "$tmp_mo"; then - echo "::warning::$mo is out of sync with $po - run locales_generator.sh" - out_of_sync=1 - fi - rm -f "$tmp_mo" - done < /tmp/changed_po.txt - if [ "$out_of_sync" -eq 1 ]; then - echo "Some .mo files are out of sync (warning only)" - fi - - name: Check for tr(f-string) anti-pattern - if: steps.changed.outputs.has_py == 'true' - run: | - if grep -rn --include='*.py' "tr(f['\"]" archinstall/; then - echo "::error::Found tr(f'...') pattern. Use tr('...{}').format(...) instead" - exit 1 - fi - echo "No tr(f-string) anti-pattern found" - - validate-pot: - name: Validate base.pot is up to date - runs-on: ubuntu-latest - container: - image: archlinux/archlinux:latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 0 - - name: Check for changed Python files - id: check_py - run: | - if [ "${{ github.event_name }}" = "pull_request" ]; then - base="${{ github.event.pull_request.base.sha }}" - head="${{ github.event.pull_request.head.sha }}" - else - base="${{ github.event.before }}" - head="${{ github.sha }}" - fi - if git diff --name-only "$base" "$head" 2>/dev/null | grep -q '\.py$'; then - echo "has_py_changes=true" >> "$GITHUB_OUTPUT" - else - echo "has_py_changes=false" >> "$GITHUB_OUTPUT" - fi - - name: Install gettext - if: steps.check_py.outputs.has_py_changes == 'true' - run: | - pacman-key --init - pacman --noconfirm -Sy archlinux-keyring - pacman --noconfirm -Syyu - pacman --noconfirm -S gettext - - name: Check for missing translatable strings - if: steps.check_py.outputs.has_py_changes == 'true' - run: | - cd archinstall - find . -type f -name '*.py' | sort | xargs xgettext \ - --no-location --omit-header --keyword='tr' \ - -d base -o /tmp/generated.pot - msgcmp --use-untranslated locales/base.pot /tmp/generated.pot + 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/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