Files
amethyst/.github/workflows/crowdin.yml
T
Claude b667fb0f4a ci(crowdin): convert Android escaping to Compose escaping during the sync
Both entries in crowdin.yml are declared `type: android`, so Crowdin's
Android serializer escapes apostrophes on the way down: `l'URL` comes
back as `l\'URL`. That is right for amethyst/src/main/res/, which aapt
un-escapes at build time, and wrong for commons/.../composeResources/,
where Compose resolves only \uXXXX, \n and \t and leaves \' \" \? \@
alone -- so the backslash reaches the screen.

Nothing prevented this, so every sync reopened the same regression and
CI's compose_escaping_check.py failed on the bot's own PR. It happened
three times (f9baab0e, 1685d7c0, e223d505), most recently 2,888
occurrences across 40 locale files, each time repaired by hand after the
fact. Convert on the way in instead, so the PR is born clean.

Two steps, placed after the ownership fix (the Crowdin container writes
as root, so the tree is not writable before it) and before the PR is
opened:

- Convert: runs the documented repair over the Compose catalog only, so
  the Android res tree keeps the escaping it needs. --no-unwrap-quotes
  is mandatory -- escape conversion is idempotent, quote-unwrapping is
  not, and a second unwrap would strip the real display quotes from
  values like import_follows_tips.
- Verify: re-runs the check that guards main, so a case the converter
  cannot repair fails the sync loudly here instead of opening a red PR.

Verified by replaying both steps against the real Crowdin output on
l10n_crowdin_translations (df930817): the check reproduces the failure
at 2,888 occurrences, the convert step fixes 1,996 entries across 40
files, the verify step then exits 0, amethyst/src/main/res/ is left
untouched, and the resulting catalog is byte-identical to main -- so
with this in place that sync would have carried no string changes at
all.

Known gap, documented inline on the verify step: fix_escapes.py only
rewrites text inside <string>/<item> elements while the check scans the
whole file, so an escape in an XML comment (comments do propagate into
the locale files) would fail the gate without the converter being able
to repair it. No such comment exists today; it has to be fixed at the
source string by hand if one ever appears.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVrW3p8NGWHgLbN673Fet2
2026-09-03 20:36:05 +00:00

113 lines
5.6 KiB
YAML

name: Crowdin Action
on:
push:
# paths: ["app/src/main/res/**/strings.xml"] // removes filter to allow downloads at any moment.
branches: [ main ]
permissions:
contents: write
pull-requests: write
jobs:
# Single job so the Crowdin translation sync and the translator-placeholder seed
# land in ONE pull request instead of two. The Crowdin action only downloads into
# the working tree (push_translations/create_pull_request disabled); the seed
# script then edits translators.json; finally one create-pull-request step opens a
# single PR with both sets of changes (and no-ops when there is no diff).
synchronize-with-crowdin:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
# Need tags so scripts/translators.sh can resolve the last v* release tag
# for the "since last tag" window.
fetch-depth: 0
- name: crowdin action
uses: crowdin/github-action@v2
with:
upload_sources: true
upload_translations: true
download_translations: true
# Let the downloaded translations stay in the working tree; the single
# create-pull-request step below opens the combined PR.
push_translations: false
create_pull_request: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
# crowdin/github-action runs in a Docker container as root, so any file or
# directory it downloads (especially a brand-new locale folder like
# values-en-rGB/) ends up owned by root. The unprivileged runner user in
# the create-pull-request step below then can't unlink those files, which
# aborts its branch checkout with "unable to unlink ... Permission denied".
# Reclaim ownership of the whole working tree before touching git.
- name: Fix ownership after Crowdin Docker action
run: sudo chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE"
# Both files in crowdin.yml are declared `type: android`, so Crowdin's Android
# serializer escapes apostrophes on the way down: `l'URL` comes back as `l\'URL`.
# That is correct for amethyst/src/main/res/, which aapt un-escapes at build time,
# and WRONG for commons/.../composeResources/, where Compose resolves only \uXXXX,
# \n and \t and leaves \' \" \? \@ alone -- so the backslash reaches the screen.
#
# Without this step every sync reopens the same regression and CI's
# compose_escaping_check.py fails on the bot's own PR. It happened three times
# (f9baab0e, 1685d7c0, e223d505 -- 2,888 occurrences across 40 locales the last
# time) before this step existed. Convert on the way in, so the PR is born clean.
#
# Only the Compose catalog is passed in; the Android res tree keeps its escaping.
# --no-unwrap-quotes is mandatory here: escape conversion is idempotent but
# quote-unwrapping is not, and a second unwrap would strip the real display quotes
# from values like import_follows_tips.
- name: Convert Android escaping to Compose escaping in the shared catalog
run: |
python3 tools/strings-migrate/fix_escapes.py --no-unwrap-quotes \
commons/src/commonMain/composeResources
# Assert the conversion actually satisfied the check that guards main, so a case
# the converter cannot repair fails the sync loudly here instead of opening a red
# PR. Known gap if this ever trips: fix_escapes.py only rewrites text inside
# <string>/<item> elements, while the check scans the whole file -- an escape in an
# XML comment (comments do propagate into the locale files) has to be fixed at the
# source string in commons/.../composeResources/values/strings.xml by hand.
- name: Verify the shared catalog is free of Android-only escaping
run: .claude/hooks/compose_escaping_check.py
# Keep docs/changelog/translators.json seeded with everyone who has translated
# recently, so the per-release `## Translations` credits (scripts/translators.sh)
# can resolve them to npubs. Only adds rows when a genuinely new contributor
# appears.
- name: Seed translator placeholders from Crowdin
run: bash scripts/translators.sh --seed
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Open or update the combined Crowdin PR
# peter-evans/create-pull-request is MIT-licensed CI-only tooling (not
# linked into any shipped artifact). It no-ops when there is no diff.
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.GITHUB_TOKEN }}
base: main
branch: l10n_crowdin_translations
add-paths: |
amethyst/src/main/res/**/strings.xml
commons/src/commonMain/composeResources/**/strings.xml
docs/changelog/translators.json
commit-message: 'chore: sync Crowdin translations and seed translator npub placeholders'
title: 'New Crowdin Translations'
body: |
New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action).
Any new Crowdin contributors were added to `docs/changelog/translators.json`
with blank npubs. Fill in the npubs you have so the next release's
`## Translations` credits generate automatically via
`scripts/translators.sh --from <prev-tag> --to <this-tag>`.