Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/translation-check.yaml
Original file line number Diff line number Diff line change
@@ -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:
Copy link
Copy Markdown
Collaborator

@svartkanin svartkanin May 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can simplify this even more, for example

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: Validate .po syntax
              run: |
                  set -e
                  find archinstall/locales -name '*.po' -print0 \
                      | xargs -0 -n1 msgfmt --check --output-file=/dev/null

            - name: Reject tr(f-string) anti-pattern
              run: |
                  if grep -rnE "tr\(\s*f['\"]" archinstall --include='*.py'; then
                      echo "::error::Use tr('...{}').format(...) instead of tr(f'...')"
                      exit 1
                  fi

            - name: Verify base.pot is up to date
              run: |
                  cp archinstall/locales/base.pot /tmp/committed.pot
                  ( cd archinstall && find . -type f -iname '*.py' \
                      | xargs xgettext --no-location --omit-header --keyword=tr \
                            -d base -o locales/base.pot )
                  if ! diff -q /tmp/committed.pot archinstall/locales/base.pot; then
                      echo "::error::base.pot is out of date - run archinstall/locales/locales_generator.sh all"
                      diff -u /tmp/committed.pot archinstall/locales/base.pot || true
                      exit 1
                  fi

All these checks are only in the GH action which makes them difficult to test, so I suggest to put these into the already existing locale_generator.sh file and call them from here. That means during dev they can actually be called and tested and there is a single source of truth.
At that point this whole action can be reduced to

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

and the checks implemented in the bash file instead. As an example you can then have this in the script

...
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
}

cmd_check() {
	cmd_check_no_tr_fstring
     ...
	echo "All translation checks passed."
}

case "${1}" in
	check)    shift; cmd_check "$@" ;;
	generate) shift; cmd_generate "$@" ;;
	-h|--help) usage ;;
	*)        cmd_generate "$@" ;;  # back-compat: bare `<lang>` means generate
esac

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
2 changes: 1 addition & 1 deletion archinstall/lib/global_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ def _prev_install_invalid_config(self, item: MenuItem) -> str | 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()
Expand Down
2 changes: 1 addition & 1 deletion archinstall/locales/base.pot
Original file line number Diff line number Diff line change
Expand Up @@ -1245,7 +1245,7 @@ msgid "Product"
msgstr ""

#, python-brace-format
msgid "Invalid configuration: {error}"
msgid "Invalid configuration: {}"
msgstr ""

msgid "Ready to install"
Expand Down
4 changes: 2 additions & 2 deletions archinstall/locales/fi/LC_MESSAGES/base.po
Original file line number Diff line number Diff line change
Expand Up @@ -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ä"
Expand Down Expand Up @@ -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"
3 changes: 0 additions & 3 deletions archinstall/locales/hi/LC_MESSAGES/base.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 "मिरर का नाम"

Expand Down
99 changes: 80 additions & 19 deletions archinstall/locales/locales_generator.sh
Original file line number Diff line number Diff line change
@@ -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