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 TextSetuon:
push:
branches: [main]paths: ["locales/**"]jobs:
upload:
runs-on: ubuntu-lateststeps:
- uses: actions/checkout@v4
- uses: TextSetu/github-action@v1with:
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 TextSetuon:
schedule: [{ cron: "0 6 * * 1" }] # Mondays, 06:00 UTCworkflow_dispatch:
permissions:
contents: writepull-requests: writejobs:
download:
runs-on: ubuntu-lateststeps:
- uses: actions/checkout@v4
- uses: TextSetu/github-action@v1with:
api_key: ${{ secrets.TEXTSETU_API_KEY }}upload_sources: falsedownload_translations: truecreate_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@v1with:
api_key: ${{ secrets.TEXTSETU_API_KEY }}upload_sources: falsecheck_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.