From 0e71c14e3d1ac508061f00107cccba405770808b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Nov 2026 17:50:10 +0000 Subject: [PATCH] Talk to Transifex's API instead of using tx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Transifex stopped making the String Catalog's translation files once the catalog had a string with substitutions (a plural of one of its numbers, here "Downloading %lld of %lld songs…") that wasn't translated yet: every download mode but onlytranslated fails with a parse_error, "'NoneType' object is not iterable", which tx doesn't show. So every pull failed, and with it every push, which pulls first. scripts/transifex.py replaces tx and the shell and merge scripts. Its pull reads each language's translations from the API as data, with their strings, and puts each catalog entry together itself, substitutions included: Transifex keeps a substitution as a string of its own, ".substitutions.", and writes a "." in a key as "\.". One pass takes reviewed and unreviewed translations alike, which needed two downloads before. The placeholder check and the catalog's formatting are as before, and a pull lists the catalog's strings Transifex doesn't have yet. push-source and push-translations upload the catalog as tx did, and print Transifex's errors when it refuses one. Run against Transifex, the pull changes only the 7 translations made since the last one. The scripts run with uv, and a Python job in CI checks them with ruff, ty and pytest, as in the Android remote. Also removes import-android-translations.py, the one-off import from before the app was on Transifex. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01R2bAq7fKGbJyhCgNr9zbNH --- .github/workflows/ci.yml | 21 +- .github/workflows/translations.yml | 50 +-- .gitignore | 6 + .tx/config | 15 - README.md | 11 +- RELEASING.md | 10 +- design/DESIGN.md | 4 +- pyproject.toml | 61 +++ ...rge-transifex-translations.cpython-311.pyc | Bin 8599 -> 0 bytes scripts/import-android-translations.py | 288 -------------- scripts/merge-transifex-translations.py | 104 ----- scripts/pull-transifex-translations.sh | 19 - scripts/test_transifex.py | 253 ++++++++++++ scripts/transifex.py | 368 ++++++++++++++++++ uv.lock | 342 ++++++++++++++++ 15 files changed, 1080 insertions(+), 472 deletions(-) delete mode 100644 .tx/config create mode 100644 pyproject.toml delete mode 100644 scripts/__pycache__/merge-transifex-translations.cpython-311.pyc delete mode 100755 scripts/import-android-translations.py delete mode 100755 scripts/merge-transifex-translations.py delete mode 100755 scripts/pull-transifex-translations.sh create mode 100644 scripts/test_transifex.py create mode 100644 scripts/transifex.py create mode 100644 uv.lock diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83d5dee..e5690f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,23 @@ jobs: working-directory: Packages/ClementineKit run: swift test + # The Python scripts in scripts/. ruff and ty are pinned in pyproject.toml's dev + # group, so uv runs the versions uv.lock has. + python: + name: Python + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: astral-sh/setup-uv@v10.2.0 + - name: ruff check + run: uv run ruff check --output-format github + - name: ruff format + run: uv run ruff format --check + - name: ty + run: uv run ty check --output-format github + - name: Unit tests + run: uv run pytest -v + # The app built for the simulator, with ClementineKit's and the UI tests. app: runs-on: xcode-27 @@ -100,7 +117,7 @@ jobs: # it needs fails, because GitHub treats a skipped required check as passing. ci: name: CI - needs: [package, app] + needs: [package, app, python] if: always() runs-on: ubuntu-latest steps: @@ -108,5 +125,7 @@ jobs: run: | echo "package: ${{ needs.package.result }}" echo "app: ${{ needs.app.result }}" + echo "python: ${{ needs.python.result }}" test "${{ needs.package.result }}" = success test "${{ needs.app.result }}" = success + test "${{ needs.python.result }}" = success diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml index 613fb8a..b6528df 100644 --- a/.github/workflows/translations.yml +++ b/.github/workflows/translations.yml @@ -1,15 +1,16 @@ -# Keeps the app's translations in step with Transifex, as Clementine and the Android remote do: +# Keeps the app's translations in step with Transifex, as Clementine and the Android remote do, +# with scripts/transifex.py, which talks to Transifex's API: # - push: when the String Catalog changes on main, sends it to Transifex for the translators. # Transifex takes a catalog's translations along with its strings, replacing its own (the # catalog has every language), so the push first pulls the translations made on Transifex # since the last pull into the catalog: without that, it would erase them. -# - pull: every night, brings the translations back into the catalog -# (scripts/merge-transifex-translations.py), and commits them to main when they changed. -# Each such commit has a release note, so new translations make the next release. +# - pull: every night, brings the translations back into the catalog, and commits them to main +# when they changed. Each such commit has a release note, so new translations make the next +# release. # -# The Transifex resource is in .tx/config. Needs two secrets (see RELEASING.md): TX_TOKEN, a -# Transifex API token, and TX_KEY, the private half of a deploy key with write access, which -# pushes the translations to main. Until they're set, each job is skipped with a notice. +# Needs two secrets (see RELEASING.md): TX_TOKEN, a Transifex API token, and TX_KEY, the private +# half of a deploy key with write access, which pushes the translations to main. Until they're +# set, each job is skipped with a notice. # # Run it by hand (Actions → translations → Run workflow) to pull now. With "Push translations" # ticked, it first sends the catalog's translations to Transifex: once, to fill a new Transifex @@ -19,9 +20,9 @@ on: push: branches: [main] paths: - - .tx/config - .github/workflows/translations.yml - App/Resources/Localizable.xcstrings + - scripts/transifex.py schedule: - cron: "37 2 * * *" # Every night, 02:37 UTC workflow_dispatch: @@ -38,12 +39,6 @@ concurrency: permissions: contents: read -env: - TX_VERSION: 1.6.17 - -# Every tx command is --silent: otherwise the client cuts each line, errors included, to the -# terminal's width, 80 columns in CI, which hides why Transifex refused a file. - jobs: push: name: Push strings to Transifex @@ -53,27 +48,17 @@ jobs: TX_TOKEN: ${{ secrets.TX_TOKEN }} steps: - uses: actions/checkout@v5 - - name: Install the Transifex client + - uses: astral-sh/setup-uv@v10.2.0 if: env.TX_TOKEN != '' - run: | - mkdir -p "$RUNNER_TEMP/tx" - curl -fsSL "https://github.com/transifex/cli/releases/download/v$TX_VERSION/tx-linux-amd64.tar.gz" \ - | tar -xz -C "$RUNNER_TEMP/tx" tx - echo "$RUNNER_TEMP/tx" >> "$GITHUB_PATH" - name: Pull the translations made since the last pull if: env.TX_TOKEN != '' - run: scripts/pull-transifex-translations.sh + run: uv run scripts/transifex.py pull - name: Push the English strings if: env.TX_TOKEN != '' - run: tx push --source --silent - # The catalog has every language, and Transifex takes them all from a file uploaded as - # any one of them. + run: uv run scripts/transifex.py push-source - name: Push the translations if: env.TX_TOKEN != '' && inputs.push_translations - run: | - mkdir -p build/transifex - cp App/Resources/Localizable.xcstrings build/transifex/de.xcstrings - tx push --translation --languages de --silent + run: uv run scripts/transifex.py push-translations - name: No Transifex token if: env.TX_TOKEN == '' run: echo "::notice::The TX_TOKEN secret isn't set, so nothing was sent to Transifex. See RELEASING.md." @@ -100,18 +85,13 @@ jobs: with: ref: main ssh-key: ${{ secrets.TX_KEY }} - - name: Install the Transifex client + - uses: astral-sh/setup-uv@v10.2.0 if: env.TX_TOKEN != '' && env.HAS_TX_KEY == 'true' - run: | - mkdir -p "$RUNNER_TEMP/tx" - curl -fsSL "https://github.com/transifex/cli/releases/download/v$TX_VERSION/tx-linux-amd64.tar.gz" \ - | tar -xz -C "$RUNNER_TEMP/tx" tx - echo "$RUNNER_TEMP/tx" >> "$GITHUB_PATH" - name: Pull the translations id: pull if: env.TX_TOKEN != '' && env.HAS_TX_KEY == 'true' run: | - scripts/pull-transifex-translations.sh + uv run scripts/transifex.py pull if git diff --quiet -- App/Resources/Localizable.xcstrings; then echo "Translations are up to date." else diff --git a/.gitignore b/.gitignore index a19fabb..5ddad8f 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,9 @@ DerivedData/ # What fastlane writes when it runs /fastlane/README.md /fastlane/report.xml + +# Python: uv run scripts/