Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -100,13 +117,15 @@ 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:
- name: Check the other jobs passed
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
50 changes: 15 additions & 35 deletions .github/workflows/translations.yml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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."
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,9 @@ DerivedData/
# What fastlane writes when it runs
/fastlane/README.md
/fastlane/report.xml

# Python: uv run scripts/<script>.py
/.venv/
__pycache__/
/.ruff_cache/
/.pytest_cache/
15 changes: 0 additions & 15 deletions .tx/config

This file was deleted.

11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,13 @@ the release workflow, which also takes the App Store's screenshots. See
## Scripts

`scripts/generate-proto.sh` regenerates the protocol code from
`Packages/ClementineKit/Proto/remotecontrolmessages.proto`, and
`scripts/merge-transifex-translations.py` merges translations pulled from Transifex (see
[RELEASING.md](RELEASING.md#translations)). `scripts/import-android-translations.py` imported the
Android remote's translations, before the app was on Transifex.
`Packages/ClementineKit/Proto/remotecontrolmessages.proto`, and `scripts/transifex.py` sends the
String Catalog to Transifex and merges its translations back (see
[RELEASING.md](RELEASING.md#translations)).

The Python scripts run with [uv](https://docs.astral.sh/uv/): `uv run scripts/transifex.py pull`.
CI checks them with `uv run ruff check`, `uv run ruff format --check`, `uv run ty check` and
`uv run pytest`.

## Licence

Expand Down
10 changes: 6 additions & 4 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,12 @@ Android remote's does:
pushed to Transifex for the translators. Transifex takes the translations in it too, replacing
its own, so the translations made on Transifex since the last pull are pulled into it first.
- Every night, the translations are pulled back and merged into the catalog
(`scripts/merge-transifex-translations.py`), and committed to `main` when they changed, as
"Automatic merge of translations from Transifex". Only translated strings come back, reviewed and
not reviewed yet alike (each pulled on its own, as one Transifex mode can leave the other
out), and a translation whose placeholders don't match the English is left out, with a warning. Xcode
(`scripts/transifex.py pull`), and committed to `main` when they changed, as
"Automatic merge of translations from Transifex". They're read from Transifex's API as data,
not as files, because Transifex fails to make a String Catalog file while a string with
substitutions (a plural of one of its numbers) isn't translated. Only translated strings come
back, reviewed and not reviewed yet alike, and a translation whose placeholders don't match the
English is left out, with a warning. Xcode
still decides which strings the app has. The commit has a release note, so new translations
make the next release; however many nights they changed, the release notes say so once.
- Transifex fills in strings whose English it has translated already, for this app or the
Expand Down
4 changes: 2 additions & 2 deletions design/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,8 +460,8 @@ Android remote does.

Plain, short, sentence case, as the design system says: "Connect", "Add to playlist", "No song
playing", "Clementine on studio-pc". Mode feedback names the new mode. Where a string says what an
Android string says, `scripts/import-android-translations.py` copies that string's translations into
the String Catalog; the rest are English until translated.
Android string says, it has the same English, so that Transifex's translation memory offers the
Android string's translations for it.

## Behaviour

Expand Down
61 changes: 61 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# The Python scripts in scripts/, which the workflows run: uv run scripts/<script>.py
# CI checks them with: uv run ruff check, uv run ruff format --check, uv run ty check,
# uv run pytest
[project]
name = "clementine-remote-ios-scripts"
version = "0.1.0"
description = "Scripts for translating and releasing Clementine Remote for iOS"
requires-python = ">=3.11"
license = "GPL-3.0-or-later"
dependencies = [
# transifex.py: Transifex's API.
"requests>=2.32",
]

[tool.uv]
# Scripts, not a package to build.
package = false

[dependency-groups]
dev = [
"pytest>=8",
# Pinned, so CI and local runs agree on what counts as an error.
"ruff==0.16.9",
"ty==0.0.84",
"types-requests>=2.32",
]

[tool.ruff]
target-version = "py311"
line-length = 88
# The scripts are first-party modules to their tests.
src = ["scripts"]

[tool.ruff.lint]
select = [
"E", "W", "F", # pycodestyle, pyflakes
"I", # import order
"UP", # modern syntax
"B", # likely bugs
"SIM", # simplifications
"ANN", # every function fully annotated
"DTZ", # naive datetimes
"PYI", # typing conventions
"PLC0415", # imports at the top of the file, not inside functions
"RUF",
]

[tool.ruff.lint.flake8-annotations]
# Not even *args: Any and **kwargs: Any without saying so.
allow-star-arg-any = false

[tool.pytest.ini_options]
testpaths = ["scripts"]
# The tests import the scripts they test as modules.
pythonpath = ["scripts"]

[tool.ty.environment]
extra-paths = ["scripts"]

[tool.ty.terminal]
error-on-warning = true
Binary file not shown.
Loading
Loading