name: i18n Auto-Translate

# Uses GitHub Models inference API (https://models.github.ai/inference) with the
# repository's GITHUB_TOKEN. Requires `models: read` permission. Default model is
# openai/gpt-5-mini (a fast/cheap GPT-5 in the catalog); override via repository variable
# TRANSLATE_MODEL (e.g. openai/gpt-5-nano for cheaper, openai/gpt-5 for flagship).
#
# Optional: to use Anthropic Claude directly, add a repo secret ANTHROPIC_API_KEY,
# then set repository variable TRANSLATE_BACKEND=anthropic and TRANSLATE_MODEL to
# a Claude model id (e.g. claude-sonnet-4-6). Claude is NOT in the GitHub Models
# catalog as of 2026-05.
#
# Scope: on pull_request events, only skills whose SKILL*.md files actually changed
# in the PR are checked/translated — keeps token usage proportional to the PR. If
# manifest.json or categories.json changed, falls back to full-repo scan (these
# affect the i18n fallback chain globally). workflow_dispatch always does a full
# scan unless `skill` input is supplied.

on:
  pull_request:
  workflow_dispatch:
    inputs:
      target_locale:
        description: "Target locale (default: all from manifest.supportedLocales)"
        required: false
      skill:
        description: "Specific skill path, e.g. skills/web-access (default: all)"
        required: false

permissions:
  contents: write
  pull-requests: write
  models: read

concurrency:
  group: i18n-translate-${{ github.ref }}
  cancel-in-progress: true

jobs:
  translate:
    # Don't run on bot's own commits to avoid loops
    if: github.actor != 'desirecore-bot[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Checkout PR branch
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.ref || github.ref }}
          token: ${{ secrets.DESIRECORE_BOT_TOKEN || secrets.GITHUB_TOKEN }}
          fetch-depth: 0

      - name: Detect relevant changes
        id: changes
        env:
          BASE_SHA: ${{ github.event.pull_request.base.sha }}
          HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
        run: |
          # GitHub Actions default shell already runs with -eo pipefail; we set
          # it explicitly so the `|| true` workaround on grep below is unambiguous.
          set -eo pipefail
          # workflow_dispatch: full scan (skill input handled later if provided)
          if [ "${{ github.event_name }}" != "pull_request" ]; then
            echo "relevant=true" >> "$GITHUB_OUTPUT"
            echo "skill_paths=" >> "$GITHUB_OUTPUT"
            exit 0
          fi

          # Ensure BASE_SHA is locally reachable; if we can't fetch it, fall back
          # to a full scan rather than silently producing an empty diff that would
          # mis-classify a real i18n PR as relevant=false.
          if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
            if ! git fetch --no-tags --depth=1 origin "$BASE_SHA" 2>/dev/null; then
              echo "::warning::failed to fetch base SHA ${BASE_SHA}; falling back to full scan"
              echo "relevant=true" >> "$GITHUB_OUTPUT"
              echo "skill_paths=" >> "$GITHUB_OUTPUT"
              exit 0
            fi
          fi

          # `A...B` (triple-dot) needs merge-base(A,B) in history. With
          # actions/checkout@v4 + fetch-depth: 0 it normally is, but if the base
          # branch advanced after the PR was opened and the merge-base isn't
          # locally reachable, fall back to a full scan instead of silently
          # producing a wrong diff.
          if ! merge_base=$(git merge-base "${BASE_SHA}" "${HEAD_SHA}" 2>/dev/null); then
            echo "::warning::could not compute merge-base for ${BASE_SHA}...${HEAD_SHA}; falling back to full scan"
            echo "relevant=true" >> "$GITHUB_OUTPUT"
            echo "skill_paths=" >> "$GITHUB_OUTPUT"
            exit 0
          fi
          changed=$(git diff --name-only "${merge_base}" "${HEAD_SHA}")

          # If manifest.json or categories.json changed, fall back to full scan
          # (these affect i18n fallback chain / supportedLocales globally).
          if printf '%s\n' "$changed" | grep -qE '^(manifest\.json|categories\.json)$'; then
            echo "relevant=true" >> "$GITHUB_OUTPUT"
            echo "skill_paths=" >> "$GITHUB_OUTPUT"
            exit 0
          fi

          # Otherwise: extract unique skill directories touched by SKILL*.md edits.
          # Grep enforces:
          #   - skill name: lowercase ASCII letters, digits, hyphens
          #     (no leading/trailing hyphen — anchored with [a-z0-9])
          #   - locale tag (BCP-47 subset): 2-3 lowercase letters + optional -RR
          # awk enforces the rest of the schema's name constraints (no consecutive
          # hyphens, no reserved names) as defense-in-depth — those would already
          # have been blocked upstream by validate-i18n.py / schema validation.
          # The brace group with `|| true` turns grep's "no match" (exit 1) into
          # success so pipefail doesn't kill the step when this PR is unrelated.
          # Strict filtering also sanitizes input for the $GITHUB_OUTPUT write
          # below (any path failing the shape is dropped).
          skill_paths=$(printf '%s\n' "$changed" \
            | { grep -E '^skills/[a-z0-9]([a-z0-9-]*[a-z0-9])?/SKILL(\.[a-z]{2,3}(-[A-Z]{2})?)?\.md$' || true; } \
            | awk -F/ '
                $2 !~ /--/ && $2 != "anthropic" && $2 != "claude" {
                  print $1 "/" $2
                }' \
            | sort -u \
            | tr '\n' ' ' \
            | sed 's/ $//')

          if [ -n "$skill_paths" ]; then
            echo "relevant=true" >> "$GITHUB_OUTPUT"
            echo "skill_paths=$skill_paths" >> "$GITHUB_OUTPUT"
          else
            echo "relevant=false" >> "$GITHUB_OUTPUT"
            echo "skill_paths=" >> "$GITHUB_OUTPUT"
          fi

      - name: Install uv
        if: steps.changes.outputs.relevant == 'true'
        uses: astral-sh/setup-uv@v3

      - name: Check for stale translations
        id: precheck
        if: steps.changes.outputs.relevant == 'true'
        env:
          SKILL_PATHS: ${{ steps.changes.outputs.skill_paths }}
          INPUT_SKILL: ${{ github.event.inputs.skill }}
        run: |
          set +e
          ARGS=(--check)
          if [ -n "$INPUT_SKILL" ]; then
            # workflow_dispatch with explicit skill input takes precedence
            ARGS+=("$INPUT_SKILL")
          elif [ -n "$SKILL_PATHS" ]; then
            # pull_request: read space-separated paths into an array to avoid
            # word-splitting / glob expansion surprises.
            read -r -a SKILL_ARR <<<"$SKILL_PATHS"
            ARGS+=("${SKILL_ARR[@]}")
          fi
          uv run --quiet scripts/i18n/translate.py "${ARGS[@]}"
          rc=$?
          if [ $rc -eq 0 ]; then
            echo "stale=false" >> "$GITHUB_OUTPUT"
          else
            echo "stale=true" >> "$GITHUB_OUTPUT"
          fi
          exit 0

      - name: Translate stale locales
        if: steps.changes.outputs.relevant == 'true' && steps.precheck.outputs.stale == 'true'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          TRANSLATE_BACKEND: ${{ vars.TRANSLATE_BACKEND || 'github' }}
          TRANSLATE_MODEL: ${{ vars.TRANSLATE_MODEL || 'openai/gpt-5-mini' }}
          SKILL_PATHS: ${{ steps.changes.outputs.skill_paths }}
          INPUT_SKILL: ${{ github.event.inputs.skill }}
          INPUT_TARGET: ${{ github.event.inputs.target_locale }}
        run: |
          set -e
          ARGS=()
          if [ -n "$INPUT_SKILL" ]; then
            # workflow_dispatch input takes precedence (single specific skill)
            ARGS+=("$INPUT_SKILL")
          elif [ -n "$SKILL_PATHS" ]; then
            # pull_request: only translate skills touched by this PR
            read -r -a SKILL_ARR <<<"$SKILL_PATHS"
            ARGS+=("${SKILL_ARR[@]}")
          fi
          if [ -n "$INPUT_TARGET" ]; then
            ARGS+=(--target "$INPUT_TARGET")
          fi
          uv run --quiet scripts/i18n/translate.py "${ARGS[@]}"

      - name: Validate after translation
        if: steps.changes.outputs.relevant == 'true' && steps.precheck.outputs.stale == 'true'
        run: uv run --quiet scripts/i18n/validate-i18n.py

      - name: Detect changes
        id: diff
        if: steps.changes.outputs.relevant == 'true' && steps.precheck.outputs.stale == 'true'
        run: |
          if git diff --quiet; then
            echo "changed=false" >> "$GITHUB_OUTPUT"
          else
            echo "changed=true" >> "$GITHUB_OUTPUT"
            git diff --stat
          fi

      - name: Commit & push translations
        if: steps.diff.outputs.changed == 'true'
        run: |
          git config user.name "desirecore-bot"
          git config user.email "bot@desirecore.net"
          git add -A
          git commit -m "chore(i18n): auto-translate skills [skip ci]" \
                     -m "Generated by scripts/i18n/translate.py via i18n-translate workflow." \
                     -m "Backend: ${TRANSLATE_BACKEND:-github}  Model: ${TRANSLATE_MODEL:-openai/gpt-5-mini}"
          git push
        env:
          TRANSLATE_BACKEND: ${{ vars.TRANSLATE_BACKEND || 'github' }}
          TRANSLATE_MODEL: ${{ vars.TRANSLATE_MODEL || 'openai/gpt-5-mini' }}

      - name: Comment on PR
        if: steps.diff.outputs.changed == 'true' && github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.DESIRECORE_BOT_TOKEN || secrets.GITHUB_TOKEN }}
          script: |
            const backend = process.env.TRANSLATE_BACKEND || 'github';
            const model = process.env.TRANSLATE_MODEL || 'openai/gpt-5-mini';
            const body = [
              '🌐 **i18n auto-translation pushed**',
              '',
              'New machine-translated content was added to this PR by `scripts/i18n/translate.py`.',
              '',
              '**Please review the translated strings** before merging:',
              '- Check terminology against `scripts/i18n/glossary.json`',
              '- Verify Markdown structure (headings/tables/code fences) is preserved',
              '- If you edit a translation, set `metadata.i18n.<locale>.translated_by: human` to lock it.',
              '',
              `Backend: \`${backend}\`  Model: \`${model}\``,
            ].join('\n');
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body,
            });
        env:
          TRANSLATE_BACKEND: ${{ vars.TRANSLATE_BACKEND || 'github' }}
          TRANSLATE_MODEL: ${{ vars.TRANSLATE_MODEL || 'openai/gpt-5-mini' }}

      - name: Label on translation failure
        if: failure() && github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.DESIRECORE_BOT_TOKEN || secrets.GITHUB_TOKEN }}
          script: |
            await github.rest.issues.addLabels({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              labels: ['i18n-translation-failed'],
            });

      - name: Skip notice
        if: steps.changes.outputs.relevant != 'true'
        run: echo "No i18n-relevant changes; translation skipped."
