Sync translations with TextSetu from GitHub Actions. A
thin wrapper around @textsetu/cli.
Where this lives. The action is developed here, inside the CLI package, so that a change to a CLI flag and the action input that passes it land in the same commit. It is published to the Marketplace from a mirror repo (
TextSetu/github-action), which is whatuses:refers to.
1. Push sources when they change. Your repo is the source of truth for English; TextSetu is where it gets translated.
name: Upload strings to TextSetu
on:
push:
branches: [main]
paths: ["locales/**"]
jobs:
upload:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: TextSetu/github-action@v1
with:
api_key: ${{ secrets.TEXTSETU_API_KEY }}
upload_sources: true2. Bring translations back as a pull request. On a schedule, so a reviewer
sees them rather than a bot committing to main.
name: Download translations from TextSetu
on:
schedule: [{ cron: "0 6 * * 1" }] # Mondays, 06:00 UTC
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
download:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: TextSetu/github-action@v1
with:
api_key: ${{ secrets.TEXTSETU_API_KEY }}
upload_sources: false
download_translations: true
create_pull_request: truepermissions is required, and so is allowing Actions to create pull requests
(Settings → Actions → General). Without both, the run fails at the push step.
- uses: TextSetu/github-action@v1
with:
api_key: ${{ secrets.TEXTSETU_API_KEY }}
upload_sources: false
check_threshold: 95 # fail the job if any locale is below 95%The gate runs last, so it reflects whatever the same run just synced. It fails
the job through the CLI's exit code 4, which is distinct from a generic
failure — so a pipeline can tell "translations aren't ready" from "the token
expired" (3).
Use a project token (tsu_proj_…) — it is bound to one project, which is
what you want for a repo secret. Create it in the web app under Project settings
→ API tokens, granting only translation_read (to pull) and translation_create
(to push).
Note a project token never counts as an approver: on a project that requires
approval, its pushes land as pending_review rather than live, and it cannot
complete a branch merge. Both are usually what you want from CI.
| Input | Default | Notes |
|---|---|---|
api_key |
— | Required. Put it in a secret, never in the workflow file. |
api_url |
production | Only for a self-hosted deployment. |
project |
from config | Overrides projectId in textsetu.json. |
config |
textsetu.json |
Path, if not at the repo root. |
cli_version |
latest |
Pin it for reproducible builds. |
upload_sources |
true |
|
upload_translations |
false |
Opt-in: pushing targets can overwrite reviewed work. |
download_translations |
false |
|
download_status |
approved |
all | approved | pending_review | draft |
locales |
all | Space-separated, e.g. fr de. |
branch |
main | A TextSetu translation branch, not a git branch. |
dry_run |
false |
Print the resolved file plan and change nothing. |
check_threshold |
— | Set to run the completeness gate. |
create_pull_request |
true |
Only applies when downloading. |
localization_branch_name |
l10n_textsetu |
|
pull_request_title |
New TextSetu translations |
|
pull_request_body |
… | |
pull_request_base_branch_name |
the current branch | |
pull_request_labels |
— | Comma-separated. |
commit_message |
chore(i18n): update translations from TextSetu |
|
github_token |
github.token |
Needs contents:write + pull-requests:write. |
command / command_args |
— | Escape hatch: run any CLI command instead. |
| Output | Notes |
|---|---|
changed |
true if downloading modified any tracked file. |
pull_request_url |
The PR that was created or updated, if any. |
- The PR branch is force-pushed to a single commit. It represents "the
translations as they are now"; a growing chain of
update translationscommits makes the PR unreviewable. An already-open PR is reused rather than replaced. pullruns with--force. A runner is a fresh clone, so the CLI's lockfile conflict guard has no local edit to protect — it would only ever be comparing against what is committed, and exiting5there would wedge the workflow permanently. On a developer's machine that guard is exactly what you want, which is why it is on by default there and off here.- Nothing is committed when nothing changed.
changedisfalseand the PR step is skipped.