From c04130a53b3f64eee96595ec992ea4470edb6b3f Mon Sep 17 00:00:00 2001 From: Chris Phillipson Date: Thu, 2 Jul 2026 16:02:19 -0700 Subject: [PATCH] fix(ci): make i18n translation job manual + provider-selectable The 'Test Translation API Integration' job invoked 'ampel-i18n translate' without the required --lang argument, so it exited with code 2 on every push to main. The job was also structurally dead: the translate step had no id, so the create-pull-request gate on steps.translate.outputs.changed never fired, and --dry-run never wrote files to PR. Rework it into a manually-triggered, provider-selectable translation run: - switch the trigger to workflow_dispatch with a 'provider' choice input (deepl | google | openai | systran); the job no longer runs on push - inject ONLY the selected provider's secret into its matching *_API_KEY env var, so the fallback router pins translation to that provider - fail fast with a clear message if the chosen provider's secret is unset - build the binary once, then translate every locale directory (English source excluded -> 26 targets) via 'translate --lang ' - record changed locales from git porcelain output as step outputs (changed, languages) and gate the consolidated PR on them - make the PR body report which provider produced the updates All run-step inputs are trusted (locale names from the filesystem, secrets via env), so there is no workflow-injection surface. --- .github/workflows/i18n-validation.yml | 93 +++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 5 deletions(-) diff --git a/.github/workflows/i18n-validation.yml b/.github/workflows/i18n-validation.yml index f1a8b39..af55287 100644 --- a/.github/workflows/i18n-validation.yml +++ b/.github/workflows/i18n-validation.yml @@ -15,6 +15,18 @@ on: paths: - 'crates/ampel-api/locales/**' - 'frontend/public/locales/**' + workflow_dispatch: + inputs: + provider: + description: 'Translation provider to use (requires its matching *_API_KEY repository secret)' + type: choice + required: true + default: deepl + options: + - deepl + - google + - openai + - systran env: COVERAGE_THRESHOLD: 95 @@ -452,7 +464,7 @@ jobs: translation-api: name: Test Translation API Integration runs-on: ubuntu-latest - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + if: github.event_name == 'workflow_dispatch' permissions: contents: write pull-requests: write @@ -462,20 +474,91 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable - - name: Run translation update (dry-run) + - name: Build ampel-i18n-builder + run: cargo build --package ampel-i18n-builder --release + + - name: Run translation update (all languages) + id: translate env: - DEEPL_API_KEY: ${{ secrets.DEEPL_API_KEY }} + PROVIDER: ${{ inputs.provider }} + # Inject only the selected provider's secret. The fallback router + # initializes whichever *_API_KEY is present in the environment, so + # exposing exactly one key pins translation to the chosen provider. + PROVIDER_API_KEY: >- + ${{ inputs.provider == 'deepl' && secrets.DEEPL_API_KEY + || inputs.provider == 'google' && secrets.GOOGLE_API_KEY + || inputs.provider == 'openai' && secrets.OPENAI_API_KEY + || inputs.provider == 'systran' && secrets.SYSTRAN_API_KEY }} run: | - cargo run --package ampel-i18n-builder --release -- translate --dry-run + set -euo pipefail + + if [ -z "${PROVIDER_API_KEY:-}" ]; then + echo "::error::No API key configured for provider '$PROVIDER'. Add the matching repository secret (DEEPL_API_KEY / GOOGLE_API_KEY / OPENAI_API_KEY / SYSTRAN_API_KEY)." + exit 1 + fi + + # Expose the key under the env var the selected provider reads. + case "$PROVIDER" in + deepl) export DEEPL_API_KEY="$PROVIDER_API_KEY" ;; + google) export GOOGLE_API_KEY="$PROVIDER_API_KEY" ;; + openai) export OPENAI_API_KEY="$PROVIDER_API_KEY" ;; + systran) export SYSTRAN_API_KEY="$PROVIDER_API_KEY" ;; + *) echo "::error::Unknown provider '$PROVIDER'"; exit 1 ;; + esac + echo "Using translation provider: $PROVIDER" + + BIN=./target/release/ampel-i18n + LOCALES_DIR=frontend/public/locales + failed=() + + # Translate missing keys for every locale, deriving the target list from + # the locale directories (English is the source and is skipped). + for dir in "$LOCALES_DIR"/*/; do + lang="$(basename "$dir")" + [ "$lang" = "en" ] && continue + echo "::group::Translating $lang" + if "$BIN" translate --lang "$lang"; then + echo "✓ $lang complete" + else + echo "::warning::Translation failed for $lang" + failed+=("$lang") + fi + echo "::endgroup::" + done + + # Determine which locales actually changed (strip the porcelain status + # prefix, then reduce each path to its top-level locale directory). + changed_langs="$(git status --porcelain -- "$LOCALES_DIR" \ + | sed -E 's/^...//' \ + | sed -E "s#^${LOCALES_DIR}/([^/]+)/.*#\1#" \ + | sort -u \ + | paste -sd, -)" + + if [ -n "$changed_langs" ]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "languages=$changed_langs" >> "$GITHUB_OUTPUT" + echo "Changed languages: $changed_langs" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "No translation changes detected." + fi + + # Fail the job if any language errored, but only after recording the + # outputs so partial translations can still be opened as a PR. + if [ "${#failed[@]}" -gt 0 ]; then + echo "::error::Translation failed for: ${failed[*]}" + exit 1 + fi - name: Create translation PR if needed if: steps.translate.outputs.changed == 'true' uses: peter-evans/create-pull-request@v8 with: + token: ${{ secrets.GITHUB_TOKEN }} commit-message: 'chore(i18n): automated translation updates' title: '[i18n] Automated Translation Updates' body: | - Automated translation updates from DeepL API. + Automated translation updates via the `${{ inputs.provider }}` provider. **Changes:** - Updated translations for missing keys