Control UI Locale Refresh #35
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: Control UI Locale Refresh | |
| on: | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - ui/src/i18n/locales/en.ts | |
| - ui/src/i18n/locales/*.ts | |
| - ui/src/i18n/.i18n/* | |
| - ui/src/i18n/lib/types.ts | |
| - ui/src/i18n/lib/registry.ts | |
| - scripts/control-ui-i18n.ts | |
| - .github/workflows/control-ui-locale-refresh.yml | |
| release: | |
| types: | |
| - published | |
| schedule: | |
| - cron: "23 4 * * *" | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| concurrency: | |
| group: control-ui-locale-refresh-${{ github.event_name == 'push' && github.ref || github.event_name == 'workflow_dispatch' && format('manual-{0}', github.run_id) || github.event_name == 'release' && format('release-{0}', github.event.release.tag_name) || format('{0}-{1}', github.event_name, github.run_id) }} | |
| cancel-in-progress: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && github.actor != 'github-actions[bot]' }} | |
| jobs: | |
| plan: | |
| if: github.repository == 'openclaw/openclaw' && (github.event_name != 'push' || github.actor != 'github-actions[bot]') | |
| runs-on: ubuntu-latest | |
| outputs: | |
| has_locales: ${{ steps.plan.outputs.has_locales }} | |
| locales_json: ${{ steps.plan.outputs.locales_json }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| submodules: false | |
| - name: Plan locale matrix | |
| id: plan | |
| env: | |
| BEFORE_SHA: ${{ github.event.before }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| run: | | |
| set -euo pipefail | |
| all_locales_json='["zh-CN","zh-TW","pt-BR","de","es","ja-JP","ko","fr","ar","it","tr","uk","id","pl","th","vi","nl","fa"]' | |
| if [ "$EVENT_NAME" != "push" ]; then | |
| echo "has_locales=true" >> "$GITHUB_OUTPUT" | |
| echo "locales_json=$all_locales_json" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| before_ref="$BEFORE_SHA" | |
| if [ -z "$before_ref" ] || [ "$before_ref" = "0000000000000000000000000000000000000000" ]; then | |
| before_ref="$(git rev-parse HEAD^)" | |
| fi | |
| changed_files="$(git diff --name-only "$before_ref" HEAD)" | |
| echo "changed files:" | |
| printf '%s\n' "$changed_files" | |
| if printf '%s\n' "$changed_files" | grep -Eq '^(ui/src/i18n/locales/en\.ts|ui/src/i18n/lib/types\.ts|ui/src/i18n/lib/registry\.ts|scripts/control-ui-i18n\.ts|\.github/workflows/control-ui-locale-refresh\.yml)$'; then | |
| echo "has_locales=true" >> "$GITHUB_OUTPUT" | |
| echo "locales_json=$all_locales_json" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| locales_json="$(printf '%s\n' "$changed_files" | node <<'EOF' | |
| const fs = require("node:fs"); | |
| const changed = fs.readFileSync(0, "utf8").split(/\r?\n/).filter(Boolean); | |
| const locales = new Set(); | |
| for (const file of changed) { | |
| let match = file.match(/^ui\/src\/i18n\/locales\/(.+)\.ts$/); | |
| if (match && match[1] !== "en") { | |
| locales.add(match[1]); | |
| continue; | |
| } | |
| match = file.match(/^ui\/src\/i18n\/\.i18n\/(.+)\.(?:meta\.json|tm\.jsonl)$/); | |
| if (match) { | |
| locales.add(match[1]); | |
| } | |
| } | |
| process.stdout.write(JSON.stringify([...locales])); | |
| EOF | |
| )" | |
| if [ "$locales_json" = "[]" ]; then | |
| echo "has_locales=false" >> "$GITHUB_OUTPUT" | |
| echo "locales_json=[]" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "has_locales=true" >> "$GITHUB_OUTPUT" | |
| echo "locales_json=$locales_json" >> "$GITHUB_OUTPUT" | |
| refresh: | |
| needs: plan | |
| if: github.repository == 'openclaw/openclaw' && needs.plan.outputs.has_locales == 'true' | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 4 | |
| matrix: | |
| locale: ${{ fromJson(needs.plan.outputs.locales_json) }} | |
| runs-on: ubuntu-latest | |
| name: Refresh ${{ matrix.locale }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| persist-credentials: true | |
| submodules: false | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| - name: Ensure translation provider secrets exist | |
| env: | |
| OPENCLAW_DOCS_I18N_OPENAI_API_KEY: ${{ secrets.OPENCLAW_DOCS_I18N_OPENAI_API_KEY }} | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${OPENCLAW_DOCS_I18N_OPENAI_API_KEY:-}" ] && [ -z "${OPENAI_API_KEY:-}" ] && [ -z "${ANTHROPIC_API_KEY:-}" ]; then | |
| echo "Missing OPENCLAW_DOCS_I18N_OPENAI_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY secret." | |
| exit 1 | |
| fi | |
| - name: Refresh control UI locale files | |
| env: | |
| OPENCLAW_DOCS_I18N_OPENAI_API_KEY: ${{ secrets.OPENCLAW_DOCS_I18N_OPENAI_API_KEY }} | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| ANTHROPIC_MODEL: claude-opus-4-8 | |
| OPENAI_MODEL: ${{ vars.OPENCLAW_CI_OPENAI_MODEL_BARE || 'gpt-5.5' }} | |
| OPENCLAW_CONTROL_UI_I18N_THINKING: low | |
| OPENCLAW_CONTROL_UI_I18N_AUTH_OPTIONAL: "0" | |
| LOCALE: ${{ matrix.locale }} | |
| run: | | |
| set -euo pipefail | |
| run_refresh() { | |
| local provider="$1" | |
| local model="$2" | |
| local openai_api_key="${3-}" | |
| if [ "$provider" = "openai" ]; then | |
| OPENAI_API_KEY="$openai_api_key" \ | |
| OPENCLAW_CONTROL_UI_I18N_PROVIDER="$provider" \ | |
| OPENCLAW_CONTROL_UI_I18N_MODEL="$model" \ | |
| node --import tsx scripts/control-ui-i18n.ts sync --locale "${LOCALE}" --write | |
| return | |
| fi | |
| OPENCLAW_CONTROL_UI_I18N_PROVIDER="$provider" \ | |
| OPENCLAW_CONTROL_UI_I18N_MODEL="$model" \ | |
| node --import tsx scripts/control-ui-i18n.ts sync --locale "${LOCALE}" --write | |
| } | |
| run_openai_refresh() { | |
| local status=1 | |
| if [ -n "${OPENCLAW_DOCS_I18N_OPENAI_API_KEY:-}" ]; then | |
| set +e | |
| run_refresh openai "${OPENAI_MODEL}" "${OPENCLAW_DOCS_I18N_OPENAI_API_KEY}" | |
| status="$?" | |
| set -e | |
| if [ "$status" -eq 0 ]; then | |
| return 0 | |
| fi | |
| if [ -z "${OPENAI_API_KEY:-}" ] || [ "${OPENAI_API_KEY}" = "${OPENCLAW_DOCS_I18N_OPENAI_API_KEY}" ]; then | |
| return "$status" | |
| fi | |
| echo "::warning::Docs OpenAI control UI locale refresh key failed for ${LOCALE}; retrying with repository OpenAI key." | |
| fi | |
| if [ -z "${OPENAI_API_KEY:-}" ]; then | |
| return "$status" | |
| fi | |
| run_refresh openai "${OPENAI_MODEL}" "${OPENAI_API_KEY}" | |
| } | |
| if [ -n "${ANTHROPIC_API_KEY:-}" ]; then | |
| set +e | |
| run_refresh anthropic "${ANTHROPIC_MODEL}" | |
| status="$?" | |
| set -e | |
| if [ "$status" -eq 0 ]; then | |
| exit 0 | |
| fi | |
| if [ -z "${OPENCLAW_DOCS_I18N_OPENAI_API_KEY:-}" ] && [ -z "${OPENAI_API_KEY:-}" ]; then | |
| exit "$status" | |
| fi | |
| echo "::warning::Anthropic control UI locale refresh failed for ${LOCALE}; retrying with OpenAI." | |
| fi | |
| run_openai_refresh | |
| - name: Prepare locale artifact | |
| env: | |
| LOCALE: ${{ matrix.locale }} | |
| run: | | |
| set -euo pipefail | |
| artifact_dir="${RUNNER_TEMP}/control-ui-locale-${LOCALE}" | |
| mkdir -p "${artifact_dir}" | |
| git add -A ui/src/i18n | |
| git diff --cached --binary --full-index -- ui/src/i18n > "${artifact_dir}/${LOCALE}.patch" | |
| - name: Upload locale artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: control-ui-locale-${{ matrix.locale }} | |
| path: ${{ runner.temp }}/control-ui-locale-${{ matrix.locale }}/${{ matrix.locale }}.patch | |
| if-no-files-found: error | |
| retention-days: 1 | |
| finalize: | |
| name: Commit control UI locale refresh | |
| needs: refresh | |
| if: needs.refresh.result == 'success' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: true | |
| submodules: false | |
| - name: Download locale artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| pattern: control-ui-locale-* | |
| path: ${{ runner.temp }}/control-ui-locale-artifacts | |
| merge-multiple: true | |
| - name: Apply locale artifacts | |
| run: | | |
| set -euo pipefail | |
| while IFS= read -r patch; do | |
| if [ -s "${patch}" ]; then | |
| git apply "${patch}" | |
| fi | |
| done < <(find "${RUNNER_TEMP}/control-ui-locale-artifacts" -type f -name '*.patch' | sort) | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| - name: Validate control UI locale refresh | |
| run: node --import tsx scripts/control-ui-i18n.ts check | |
| - name: Commit and push aggregate locale refresh | |
| env: | |
| TARGET_BRANCH: ${{ github.event.repository.default_branch }} | |
| run: | | |
| set -euo pipefail | |
| if ! git status --porcelain -- ui/src/i18n | grep -q .; then | |
| echo "No control UI locale changes." | |
| exit 0 | |
| fi | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add -A ui/src/i18n | |
| git commit --no-verify -m "chore(ui): refresh control ui locales" | |
| for attempt in 1 2 3 4 5; do | |
| git fetch origin "${TARGET_BRANCH}" | |
| git rebase "origin/${TARGET_BRANCH}" | |
| if git push origin HEAD:"${TARGET_BRANCH}"; then | |
| exit 0 | |
| fi | |
| git rebase --abort >/dev/null 2>&1 || true | |
| echo "Aggregate push attempt ${attempt} failed; retrying." | |
| sleep $((attempt * 2)) | |
| done | |
| echo "Failed to push aggregate control UI locale update after retries." | |
| exit 1 |