Repository navigation
Push Translations to Transifex #9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Push Translations to Transifex | |
| on: | |
| push: | |
| branches: | |
| - "3.15" | |
| paths: | |
| - "**/*.po" | |
| workflow_dispatch: | |
| inputs: | |
| push_translations: | |
| description: "Push Punjabi translations to Transifex" | |
| required: true | |
| type: boolean | |
| default: true | |
| push_sources: | |
| description: "Generate and push CPython source templates" | |
| required: true | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: transifex-push-${{ github.ref }} | |
| cancel-in-progress: false | |
| env: | |
| TX_CLI_VERSION: "1.6.16" | |
| TX_PROJECT: python-newest | |
| TX_LANGUAGE: pa | |
| CPYTHON_VERSION: "3.15" | |
| jobs: | |
| push-translations: | |
| name: Push Punjabi translations | |
| if: github.actor != 'github-actions[bot]' && (github.event_name == 'push' || inputs.push_translations) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout translation repository | |
| uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install configuration dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y gettext | |
| python -m pip install requests cogapp polib transifex-python sphinx-intl blurb six | |
| - name: Install Transifex CLI | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "$RUNNER_TEMP/txcli" | |
| curl -fsSL \ | |
| "https://github.com/transifex/cli/releases/download/v${TX_CLI_VERSION}/tx-linux-amd64.tar.gz" \ | |
| -o "$RUNNER_TEMP/txcli/tx.tar.gz" | |
| tar -xzf "$RUNNER_TEMP/txcli/tx.tar.gz" -C "$RUNNER_TEMP/txcli" | |
| TX_BINARY="$(find "$RUNNER_TEMP/txcli" -type f -name tx -print -quit)" | |
| if [ -z "$TX_BINARY" ]; then | |
| echo "ERROR: Transifex CLI binary was not found in the archive." | |
| find "$RUNNER_TEMP/txcli" -maxdepth 3 -type f -print | |
| exit 1 | |
| fi | |
| sudo install -m 0755 "$TX_BINARY" /usr/local/bin/tx | |
| echo "/usr/local/bin" >> "$GITHUB_PATH" | |
| /usr/local/bin/tx --version | |
| - name: Generate Transifex configuration | |
| run: | | |
| curl -fsSLO https://raw.githubusercontent.com/python-docs-translations/transifex-automations/main/sample-workflows/transifex-util.py | |
| chmod +x transifex-util.py | |
| ./transifex-util.py recreate_tx_config \ | |
| --language "$TX_LANGUAGE" \ | |
| --project-slug "$TX_PROJECT" \ | |
| --version "$CPYTHON_VERSION" | |
| - name: Validate Transifex configuration | |
| run: | | |
| python - <<'PY' | |
| import configparser | |
| from pathlib import Path | |
| path = Path(".tx/config") | |
| config = configparser.ConfigParser(interpolation=None) | |
| if not path.is_file() or not config.read(path): | |
| raise SystemExit("ERROR: .tx/config was not generated") | |
| expected = { | |
| "builtins--constants", | |
| "builtins--exceptions", | |
| "builtins--functions", | |
| "builtins--index", | |
| "builtins--stdtypes", | |
| "builtins--threadsafety", | |
| "builtins--time-complexity", | |
| } | |
| resources = { | |
| section.rsplit(":r:", 1)[-1]: section | |
| for section in config.sections() | |
| if ":r:" in section | |
| } | |
| missing = expected - resources.keys() | |
| if missing: | |
| raise SystemExit( | |
| "ERROR: missing builtins resources: " | |
| + ", ".join(sorted(missing)) | |
| ) | |
| for resource in sorted(expected): | |
| section = resources[resource] | |
| source = config.get( | |
| section, "source_file", fallback="" | |
| ).strip() | |
| expected_source = ( | |
| f"gettext/{resource.replace('--', '/')}.pot" | |
| ) | |
| if source != expected_source: | |
| raise SystemExit( | |
| f"ERROR: unexpected source_file for {resource}: " | |
| f"{source}" | |
| ) | |
| print( | |
| "Transifex configuration and all seven builtins mappings validated." | |
| ) | |
| PY | |
| - name: Diagnose builtins index resource | |
| run: | | |
| python - <<'PY' | |
| import ast | |
| import configparser | |
| from pathlib import Path | |
| config = configparser.ConfigParser(interpolation=None) | |
| if not config.read(".tx/config"): | |
| raise SystemExit("ERROR: Could not read .tx/config") | |
| section = "o:python-doc:p:python-newest:r:builtins--index" | |
| if not config.has_section(section): | |
| raise SystemExit( | |
| f"ERROR: missing Transifex resource: {section}" | |
| ) | |
| print("=== Generated Transifex mapping ===") | |
| for key in ( | |
| "file_filter", | |
| "source_file", | |
| "type", | |
| "resource_name", | |
| ): | |
| print( | |
| f"{key} = " | |
| f"{config.get(section, key, fallback='<missing>')}" | |
| ) | |
| po_path = Path("builtins/index.po") | |
| if not po_path.is_file(): | |
| raise SystemExit( | |
| f"ERROR: translation file not found: {po_path}" | |
| ) | |
| print(f"\nTranslation file exists: {po_path}") | |
| entries = [] | |
| msgid = None | |
| msgstr = "" | |
| field = None | |
| for line in ( | |
| po_path.read_text(encoding="utf-8").splitlines() + [""] | |
| ): | |
| if line.startswith("msgid "): | |
| if msgid is not None: | |
| entries.append((msgid, msgstr)) | |
| msgid = ast.literal_eval(line[6:]) | |
| msgstr = "" | |
| field = "msgid" | |
| elif line.startswith("msgstr "): | |
| msgstr = ast.literal_eval(line[7:]) | |
| field = "msgstr" | |
| elif line.startswith('"') and field: | |
| value = ast.literal_eval(line) | |
| if field == "msgid": | |
| msgid += value | |
| else: | |
| msgstr += value | |
| elif not line.strip(): | |
| if msgid is not None: | |
| entries.append((msgid, msgstr)) | |
| msgid = None | |
| msgstr = "" | |
| field = None | |
| translated = [ | |
| (source, target) | |
| for source, target in entries | |
| if source and target.strip() | |
| ] | |
| print(f"Non-empty translated entries: {len(translated)}") | |
| for source, target in translated: | |
| print( | |
| f" {source[:100]!r} -> {target[:100]!r}" | |
| ) | |
| if len(translated) < 5: | |
| raise SystemExit( | |
| "ERROR: expected at least five translated entries " | |
| "in builtins/index.po" | |
| ) | |
| print("Builtins index diagnostic passed.") | |
| PY | |
| - name: Stage builtins index translation for Transifex | |
| run: | | |
| python - <<'PY' | |
| import configparser | |
| import shutil | |
| from pathlib import Path | |
| config_path = Path(".tx/config") | |
| config = configparser.ConfigParser(interpolation=None) | |
| if not config.read(config_path): | |
| raise SystemExit("ERROR: Could not read .tx/config") | |
| section = "o:python-doc:p:python-newest:r:builtins--index" | |
| if not config.has_section(section): | |
| raise SystemExit( | |
| f"ERROR: Missing Transifex resource: {section}" | |
| ) | |
| source = Path("builtins/index.po") | |
| staged = Path("builtins/pa/index.po") | |
| if not source.is_file(): | |
| raise SystemExit( | |
| f"ERROR: Missing translation file: {source}" | |
| ) | |
| staged.parent.mkdir(parents=True, exist_ok=True) | |
| shutil.copy2(source, staged) | |
| config.set( | |
| section, | |
| "file_filter", | |
| "builtins/<lang>/index.po", | |
| ) | |
| with config_path.open("w", encoding="utf-8") as file: | |
| config.write(file) | |
| print(f"Staged Punjabi translation: {staged}") | |
| print( | |
| "Updated file_filter:", | |
| config.get(section, "file_filter"), | |
| ) | |
| if not staged.is_file(): | |
| raise SystemExit("ERROR: Staged translation was not created") | |
| if staged.read_bytes() != source.read_bytes(): | |
| raise SystemExit( | |
| "ERROR: Staged translation differs from the original" | |
| ) | |
| print("Staged translation validated successfully.") | |
| PY | |
| - name: Push Punjabi translations | |
| env: | |
| TX_TOKEN: ${{ secrets.TX_TOKEN }} | |
| run: | | |
| /usr/local/bin/tx push \ | |
| --translation \ | |
| --languages "$TX_LANGUAGE" \ | |
| --use-git-timestamps | |
| - name: Report Transifex push status | |
| if: always() | |
| run: | | |
| echo "Review the preceding Transifex CLI logs." | |
| echo "Confirm that builtins--index was processed successfully." | |
| echo "A successful command alone does not prove that remote translations changed." | |
| push-sources: | |
| name: Generate and push source templates | |
| needs: push-translations | |
| if: ${{ always() && github.event_name == 'workflow_dispatch' && inputs.push_sources && (needs.push-translations.result == 'success' || needs.push-translations.result == 'skipped') }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout translation repository | |
| uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false | |
| - name: Checkout CPython source | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: python/cpython | |
| ref: "3.15" | |
| path: cpython | |
| persist-credentials: false | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install documentation build dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y gettext | |
| python -m pip install -r cpython/Doc/requirements.txt | |
| python -m pip install requests cogapp polib transifex-python sphinx-intl blurb six | |
| - name: Install Transifex CLI | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "$RUNNER_TEMP/txcli" | |
| curl -fsSL \ | |
| "https://github.com/transifex/cli/releases/download/v${TX_CLI_VERSION}/tx-linux-amd64.tar.gz" \ | |
| -o "$RUNNER_TEMP/txcli/tx.tar.gz" | |
| tar -xzf "$RUNNER_TEMP/txcli/tx.tar.gz" -C "$RUNNER_TEMP/txcli" | |
| TX_BINARY="$(find "$RUNNER_TEMP/txcli" -type f -name tx -print -quit)" | |
| if [ -z "$TX_BINARY" ]; then | |
| echo "ERROR: Transifex CLI binary was not found in the archive." | |
| find "$RUNNER_TEMP/txcli" -maxdepth 3 -type f -print | |
| exit 1 | |
| fi | |
| sudo install -m 0755 "$TX_BINARY" /usr/local/bin/tx | |
| echo "/usr/local/bin" >> "$GITHUB_PATH" | |
| /usr/local/bin/tx --version | |
| - name: Generate gettext source templates | |
| run: | | |
| make -C cpython/Doc gettext | |
| test -d cpython/Doc/build/gettext | |
| mkdir -p gettext | |
| cp -a cpython/Doc/build/gettext/. gettext/ | |
| - name: Generate Transifex configuration | |
| run: | | |
| curl -fsSLO https://raw.githubusercontent.com/python-docs-translations/transifex-automations/main/sample-workflows/transifex-util.py | |
| chmod +x transifex-util.py | |
| ./transifex-util.py recreate_tx_config \ | |
| --language "$TX_LANGUAGE" \ | |
| --project-slug "$TX_PROJECT" \ | |
| --version "$CPYTHON_VERSION" | |
| - name: Validate configured source templates | |
| run: | | |
| python - <<'PY' | |
| import configparser | |
| from pathlib import Path | |
| path = Path(".tx/config") | |
| config = configparser.ConfigParser(interpolation=None) | |
| if not path.is_file() or not config.read(path): | |
| raise SystemExit("ERROR: .tx/config was not generated") | |
| missing = [] | |
| for section in config.sections(): | |
| source = config.get( | |
| section, "source_file", fallback="" | |
| ).strip() | |
| if source and not Path(source).is_file(): | |
| missing.append(f"{section}: {source}") | |
| if missing: | |
| print("Missing source templates:") | |
| print("\n".join(missing)) | |
| raise SystemExit(1) | |
| print("All configured source templates exist.") | |
| PY | |
| - name: Push source templates | |
| env: | |
| TX_TOKEN: ${{ secrets.TX_TOKEN }} | |
| run: | | |
| /usr/local/bin/tx push --source --skip |