openlogi 0.6.26

OpenLogi command-line interface — a local-first companion for Logitech HID++ peripherals.
name: Crowdin

on:
  workflow_dispatch:
  schedule:
    - cron: "17 3 * * *"
  push:
    branches:
      - master
    paths:
      # English source is SoT; non-English catalogs come from Crowdin only.
      - crates/openlogi-gui/locales/en.yml
      - crowdin.yml
      - .github/workflows/crowdin.yml
      - scripts/i18n/**
      - .github/actions/github-app-token-from-1password/**

permissions:
  contents: read

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

jobs:
  synchronize:
    name: Synchronize translations
    runs-on: ubuntu-latest
    if: github.repository == 'AprilNEA/OpenLogi'

    steps:
      # persist-credentials must stay false: checkout's default GITHUB_TOKEN
      # http.extraheader would win over the app token when we push crowdin/i18n
      # (403 as github-actions[bot] despite env GITHUB_TOKEN).
      - name: Checkout
        uses: actions/checkout@v6
        with:
          persist-credentials: false

      - name: Mint GitHub App token
        id: github_app
        uses: ./.github/actions/github-app-token-from-1password
        with:
          op-service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
          op-github-app-item: ${{ secrets.OP_GITHUB_APP_ITEM }}

      - name: Configure git for App token
        env:
          GH_TOKEN: ${{ steps.github_app.outputs.token }}
        run: |
          set -euo pipefail
          git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"

      - name: Load Crowdin credentials from 1Password
        id: crowdin_config
        uses: 1password/load-secrets-action@eb2efd0703da22a93c467f2d1ffbb6826c11e19c # v4.1.1
        with:
          export-env: false
        env:
          OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
          CROWDIN_PROJECT_ID: ${{ secrets.OP_CROWDIN_SECRET_ITEM }}/CROWDIN_PROJECT_ID
          CROWDIN_PERSONAL_TOKEN: ${{ secrets.OP_CROWDIN_SECRET_ITEM }}/CROWDIN_PERSONAL_TOKEN

      - name: Self-test locale merge script
        run: python3 scripts/i18n/merge_crowdin_download.py --self-test

      # Complete catalogs in git are the merge base. Crowdin download overwrites
      # the working tree; without a snapshot we cannot restore keys the export
      # omitted (skip_untranslated) or un-clobber English fill-in.
      - name: Snapshot locale catalogs
        run: |
          set -euo pipefail
          mkdir -p /tmp/openlogi-locales-before
          cp crates/openlogi-gui/locales/*.yml /tmp/openlogi-locales-before/

      # Upload sources + seed per-language translations, then download only
      # (no push/PR — we merge first so sparse/English exports never land).
      - name: Synchronize with Crowdin
        uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2.17.0
        with:
          config: crowdin.yml
          upload_sources: true
          # Seed each language from git so Crowdin learns existing translations
          # (de/ja/…). import_eq_suggestions false: value==English is not a
          # translation and must not overwrite Crowdin work as "done".
          upload_translations: true
          import_eq_suggestions: false
          download_translations: true
          # Sparse export is fine: merge restores omitted keys from the snapshot.
          skip_untranslated_strings: true
          push_translations: false
          create_pull_request: false
        env:
          GITHUB_TOKEN: ${{ steps.github_app.outputs.token }}
          CROWDIN_PROJECT_ID: ${{ steps.crowdin_config.outputs.CROWDIN_PROJECT_ID }}
          CROWDIN_PERSONAL_TOKEN: ${{ steps.crowdin_config.outputs.CROWDIN_PERSONAL_TOKEN }}

      - name: Merge Crowdin export into complete catalogs
        run: |
          set -euo pipefail
          python3 scripts/i18n/merge_crowdin_download.py \
            --before /tmp/openlogi-locales-before \
            --locales crates/openlogi-gui/locales \
            --en crates/openlogi-gui/locales/en.yml

      - name: Push translation branch and open PR
        env:
          GH_TOKEN: ${{ steps.github_app.outputs.token }}
        run: |
          set -euo pipefail
          if git diff --quiet -- crates/openlogi-gui/locales/; then
            echo "No real Crowdin translation updates"
            exit 0
          fi

          git config user.name "Crowdin Bot"
          git config user.email "support+bot@crowdin.com"
          git checkout -B crowdin/i18n
          git add crates/openlogi-gui/locales/
          git commit -m "chore(i18n): sync Crowdin translations"
          git push -f origin crowdin/i18n

          if gh pr view crowdin/i18n --json number --jq .number >/dev/null 2>&1; then
            echo "Translation PR already open for crowdin/i18n"
          else
            body="$(cat <<'EOF'
          Automated per-language translation sync from Crowdin.

          - `en.yml` is the English source of truth; feature PRs keep key parity across all `locales/*.yml`.
          - Existing non-English catalogs are uploaded first so Crowdin is seeded with real work.
          - Download is merged into complete catalogs: only values that differ from English are applied.
          - Untranslated keys are never replaced with English fill-in and never deleted (#549 / #552).
          EOF
          )"
            # Strip the common indent from the workflow heredoc.
            body="$(printf '%s\n' "$body" | sed 's/^          //')"
            gh pr create \
              --base master \
              --head crowdin/i18n \
              --title "chore(i18n): sync Crowdin translations" \
              --body "$body"
          fi