Translate texts, guides, etc by @floriangeigl #38
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: Translate texts, guides, etc | |
| run-name: Translate texts, guides, etc by @${{ github.actor }} | |
| concurrency: | |
| group: translate-content | |
| cancel-in-progress: true | |
| on: | |
| push: | |
| branches: | |
| - "main" | |
| paths: | |
| - "UserGuide.md" | |
| - "Advertisement.md" | |
| - "ConnectIQStore/MeditateStoreDescription-en.txt" | |
| - "userGuideScreenshots/hero_meditate.png" | |
| - ".github/codex/prompts/translate-content.md" | |
| - ".github/workflows/translate-content.yml" | |
| jobs: | |
| detect-changes: | |
| name: Detect changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| text_changed: ${{ steps.filter.outputs.text }} | |
| image_changed: ${{ steps.filter.outputs.image }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dorny/paths-filter@v3 | |
| id: filter | |
| with: | |
| filters: | | |
| text: | |
| - "UserGuide.md" | |
| - "Advertisement.md" | |
| - "ConnectIQStore/MeditateStoreDescription-en.txt" | |
| - ".github/codex/prompts/translate-content.md" | |
| - ".github/workflows/translate-content.yml" | |
| image: | |
| - "userGuideScreenshots/hero_meditate.png" | |
| - ".github/workflows/translate-content.yml" | |
| prepare-branch: | |
| name: Prepare translation branch | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.text_changed == 'true' || needs.detect-changes.outputs.image_changed == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: cleanup and create release-translation branch | |
| run: | | |
| git push -d origin release-translation || echo "no remote branch to cleanup" | |
| git checkout -B release-translation | |
| git push -f origin release-translation | |
| translate-text: | |
| name: Translate text | |
| needs: [detect-changes, prepare-branch] | |
| if: needs.detect-changes.outputs.text_changed == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: release-translation | |
| - name: Translate via Codex | |
| id: codex_translate | |
| uses: openai/codex-action@v1 | |
| with: | |
| openai-api-key: ${{ secrets.CHATGPT_TOKEN }} | |
| prompt-file: .github/codex/prompts/translate-content.md | |
| codex-args: --full-auto | |
| safety-strategy: drop-sudo | |
| sandbox: workspace-write | |
| model: gpt-5.4 | |
| - name: Commit text translations | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add ./generated/ | |
| git commit -m "Update text translations" || echo "No text translation changes to commit" | |
| git pull --rebase origin release-translation || true | |
| git push origin release-translation | |
| translate-images: | |
| name: Translate hero image | |
| needs: [detect-changes, prepare-branch] | |
| if: needs.detect-changes.outputs.image_changed == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: release-translation | |
| - name: Translate hero image for each language | |
| shell: bash | |
| env: | |
| OPENAI_API_KEY: ${{ secrets.CHATGPT_TOKEN }} | |
| run: | | |
| pip install Pillow --quiet | |
| declare -A LANG_FULL=([de]="German" [pt]="Brazilian Portuguese" [ko]="Korean" [es]="Spanish" [zh]="Chinese Simplified" [uk]="Ukrainian" [ja]="Japanese" [fr]="French") | |
| SRC="userGuideScreenshots/hero_meditate.png" | |
| mkdir -p generated/HeroImages | |
| # Pad to 1536x1024 (nearest supported size) preserving center | |
| python3 -c " | |
| from PIL import Image | |
| img = Image.open('$SRC').convert('RGB') | |
| padded = Image.new('RGB', (1536, 1024), (0, 0, 0)) | |
| x = (1536 - img.width) // 2 | |
| y = (1024 - img.height) // 2 | |
| padded.paste(img, (x, y)) | |
| padded.save('/tmp/hero_padded.png') | |
| " | |
| for lang in "${!LANG_FULL[@]}"; do | |
| lang_name="${LANG_FULL[$lang]}" | |
| echo "Translating hero image to ${lang_name} (${lang})..." | |
| prompt="You are editing a hero banner image for a Garmin watch meditation and breathwork app. | |
| Translate ALL visible text in this image into ${lang_name}. | |
| Rules: | |
| - Translate the app name naturally (e.g. for German: \"Meditation & Atemübungen\"). Use the most idiomatic phrasing. Keep the & separator. | |
| - Translate all other visible text naturally and fluently, as a native ${lang_name} speaker would write it. The tone is warm, gentle, and encouraging. | |
| - Preserve the EXACT same visual design: background, colors, gradients, watch imagery, layout, typography style, text positioning, and text sizing. | |
| - Only change the language of the text. Everything else must remain pixel-identical. | |
| - The result must look professional and polished, as if it were the original design in ${lang_name}. | |
| - Do NOT add any new text, watermarks, logos, or visual elements." | |
| response=$(curl -s -X POST "https://api.openai.com/v1/images/edits" \ | |
| -H "Authorization: Bearer ${OPENAI_API_KEY}" \ | |
| -F "model=gpt-image-1" \ | |
| -F "image[]=@/tmp/hero_padded.png" \ | |
| -F "prompt=${prompt}" \ | |
| -F "size=1536x1024" \ | |
| -F "quality=high") | |
| # Check for errors | |
| error=$(echo "$response" | jq -r '.error.message // empty') | |
| if [ -n "$error" ]; then | |
| echo "::warning::Failed to translate hero image to ${lang_name}: ${error}" | |
| continue | |
| fi | |
| # Decode base64 image | |
| echo "$response" | jq -r '.data[0].b64_json' | base64 -d > "/tmp/hero_${lang}_raw.png" | |
| # Crop back to center 2:1 ratio then resize to exact 1440x720 | |
| python3 -c " | |
| from PIL import Image | |
| img = Image.open('/tmp/hero_${lang}_raw.png').convert('RGB') | |
| # Center crop to 1536x768 | |
| left = (img.width - 1536) // 2 | |
| top = (img.height - 768) // 2 | |
| img = img.crop((left, top, left + 1536, top + 768)) | |
| img = img.resize((1440, 720), Image.LANCZOS) | |
| img.save('generated/HeroImages/hero_meditate-${lang}.png') | |
| " | |
| rm -f "/tmp/hero_${lang}_raw.png" | |
| echo "Done: generated/HeroImages/hero_meditate-${lang}.png" | |
| done | |
| rm -f /tmp/hero_padded.png | |
| - name: Commit image translations | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add ./generated/HeroImages/ | |
| git commit -m "Update hero image translations" || echo "No image translation changes to commit" | |
| git pull --rebase origin release-translation || true | |
| git push origin release-translation | |
| create-pr: | |
| name: Create or update PR | |
| needs: [detect-changes, translate-text, translate-images] | |
| if: always() && (needs.translate-text.result == 'success' || needs.translate-images.result == 'success') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - name: Create or update PR to main | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const head = `${owner}:release-translation`; | |
| const base = 'main'; | |
| const { data: pulls } = await github.rest.pulls.list({ | |
| owner, | |
| repo, | |
| head, | |
| base, | |
| state: 'open', | |
| per_page: 10, | |
| }); | |
| if (pulls.length > 0) { | |
| core.info(`PR already exists: #${pulls[0].number}`); | |
| return; | |
| } | |
| const { data: pr } = await github.rest.pulls.create({ | |
| owner, | |
| repo, | |
| head: 'release-translation', | |
| base, | |
| title: 'Update translations', | |
| body: 'Automated translation updates generated by the translation workflow.', | |
| maintainer_can_modify: true, | |
| }); | |
| core.info(`Created PR: #${pr.number}`); |