← CrowdinCONTENT HISTORY

Update to Crowdin

Snapshot Oct 28, 2026 · 13:09 UTC · version 1.8.0

Collection source: downloaded plugin package.

WHAT CHANGED

First saved snapshot

No earlier snapshot is available to establish a change.

Save Crowdin and check future changes in Following.

Up to 10 plugins free · no card required.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "description": "Sets up and debugs the Crowdin GitHub Action (crowdin/github-action), which syncs a repository with a Crowdin project and opens the translation pull request. Use whenever the user wires Crowdin into GitHub Actions, writes or reviews a .github/workflows/crowdin.yml step, tunes its inputs (upload_sources, download_translations, localization_branch_name, create_pull_request, command), or debugs a run that opened no pull request, opened one that gets no CI checks, or hit a 409 while a build was in progress - including bare phrasings like \"automate translations in CI\" or \"my Crowdin workflow is broken\".",
  "included_files": [
    {
      "relative_path": "references/inputs.md",
      "size_in_bytes": 8840
    },
    {
      "relative_path": "references/recipes.md",
      "size_in_bytes": 10482
    }
  ],
  "name": "github-action",
  "skill_md_contents": "---\nname: github-action\ndescription: Sets up and debugs the Crowdin GitHub Action (crowdin/github-action), which syncs a repository with a Crowdin project and opens the translation pull request. Use whenever the user wires Crowdin into GitHub Actions, writes or reviews a .github/workflows/crowdin.yml step, tunes its inputs (upload_sources, download_translations, localization_branch_name, create_pull_request, command), or debugs a run that opened no pull request, opened one that gets no CI checks, or hit a 409 while a build was in progress - including bare phrasings like \"automate translations in CI\" or \"my Crowdin workflow is broken\".\n---\n\n# Crowdin GitHub Action\n\n`crowdin/github-action` runs [Crowdin CLI](https://crowdin.github.io/crowdin-cli) inside a Docker container as a single workflow step: it uploads source files to Crowdin, downloads finished translations, commits them to a localization branch, and opens a pull request.\n\nBecause it is a Docker action it only runs on **Linux** runners — `ubuntu-latest`, or a self-hosted Linux runner with Docker available. It will not run on `macos-*` or `windows-*`.\n\nReference: [README](https://raw.githubusercontent.com/crowdin/github-action/refs/heads/master/README.md) · [examples](https://raw.githubusercontent.com/crowdin/github-action/refs/heads/master/docs/EXAMPLES.md) · [inputs and outputs](references/inputs.md) · [workflow recipes](references/recipes.md)\n\n## The step is a pipeline, not a command\n\nOne step performs up to five operations, always in this fixed order, and each one is gated by its own input:\n\n| Order | Input | Runs |\n|---|---|---|\n| 1 | `upload_sources: true` *(default)* | `crowdin upload sources` |\n| 2 | `upload_translations: true` | `crowdin upload translations` |\n| 3 | `download_sources: true` | `crowdin download sources`, then the **git half** |\n| 4 | `download_translations: true` | `crowdin download`, then the **git half** |\n| 5 | `download_bundle: <id>` | `crowdin bundle download <id>`, then the **git half** |\n\nThe **git half** is a single unit of work: `git add .`, commit, force-push the localization branch, open the PR. Its parts are individually switchable (`push_translations`, `push_sources`, `create_pull_request`), and only rows 3–5 can reach it.\n\nThat order explains nearly every surprise the action produces:\n\n- **`upload_sources` defaults to `true`.** A step you intended as download-only also pushes your sources to Crowdin unless you write `upload_sources: false`. Set every flag you care about explicitly rather than relying on defaults.\n- **The git half runs only after a download.** `create_pull_request: true` on an upload-only step is inert — there is nothing to commit.\n- **Two downloads in one step run the git half twice** — two commits to the same localization branch, under one PR.\n- **The git half commits with `git add .`** — everything dirty in the workspace goes in, including files earlier steps generated. Keep the Crowdin step ahead of any build steps, or give it its own job.\n- **Every run with changes force-pushes**, so an open translation PR is rewritten rather than appended to — which is why its checks start over each time.\n\n## Setting it up\n\nTwo of the five steps belong to a human: the repository secrets in step 2 and the pull-request setting in step 4 need someone with repository admin access clicking through the GitHub UI. Hand that list over explicitly rather than assuming it is already in place — a workflow that is perfect on disk still fails its first run without them.\n\n### 1. Configure `crowdin.yml`\n\nThe action reads the same `crowdin.yml` as the CLI — the [crowdin-cli](../crowdin-cli/SKILL.md) skill covers `files`, placeholders, and language mapping. Three parts matter specifically here:\n\n```yaml\n\"project_id_env\": \"CROWDIN_PROJECT_ID\"\n\"api_token_env\": \"CROWDIN_PERSONAL_TOKEN\"\n\"preserve_hierarchy\": true\n\nfiles:\n  - source: \"/src/locales/en/**/*.json\"\n    translation: \"/src/locales/%two_letters_code%/**/%original_file_name%\"\n```\n\n- `project_id_env` / `api_token_env` point at the environment variables the workflow supplies, so no credential is ever committed.\n- `preserve_hierarchy: true` keeps Crowdin's file tree identical to the repository's. Without it Crowdin strips the common parent directory, and downloaded translations land in paths that don't match the repo — the usual cause of a PR full of files in the wrong place. It is also required when moving off the native GitHub (OAuth) integration.\n- Keep the file at the repository root, or pass its path via the `config` input (no leading `/`).\n\nRepositories with a single source and translation pattern can skip the file entirely and pass `source` and `translation` as inputs instead — see [no-`crowdin.yml`](references/recipes.md#no-crowdinyml-configuration). Pick one or the other: a `crowdin.yml` in the repository conflicts with those inputs.\n\n### 2. Create the repository secrets\n\nIn **Settings → Secrets and variables → Actions**, add:\n\n- `CROWDIN_PROJECT_ID` — the numeric project ID, from **Tools → API** in the Crowdin project.\n- `CROWDIN_PERSONAL_TOKEN` — a Crowdin personal access token ([crowdin.com](https://crowdin.com/settings#api-key), or **Account Settings → Access Tokens** in Enterprise).\n\nScope the token to what the workflow actually does — for a normal sync that is Projects → Read, Translation Status → Read, Source files & strings → Read and Write, Translations → Read and Write.\n\n### 3. Write the workflow\n\n`.github/workflows/crowdin.yml`:\n\n```yaml\nname: Crowdin\n\non:\n  push:\n    branches: [ main ]\n  workflow_dispatch:\n\npermissions:\n  contents: write\n  pull-requests: write\n\nconcurrency:\n  group: crowdin\n  cancel-in-progress: false\n\njobs:\n  synchronize:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v7\n\n      - name: Synchronize with Crowdin\n        uses: crowdin/github-action@v3\n        with:\n          upload_sources: true\n          upload_translations: false\n          download_translations: true\n\n          localization_branch_name: l10n_crowdin\n          create_pull_request: true\n          pull_request_title: 'New Crowdin translations'\n          pull_request_base_branch_name: main\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}\n          CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}\n```\n\nDetails worth keeping when you adapt it:\n\n- Credentials go in `env:`, not `with:` — the CLI and the action's git code read them from the environment. (`token` and `project_id` inputs exist as an alternative, but a token in `with:` shows up in the step's rendered inputs.)\n- `workflow_dispatch` is what lets you test the workflow without waiting for a push.\n- `concurrency` with `cancel-in-progress: false` queues runs instead of racing them. A second run that starts while Crowdin is still building the previous export fails with a 409.\n- `pull_request_base_branch_name` is worth setting explicitly: unset, the base branch is whatever ref triggered the run, which is correct for a push to `main` but broken for tag and release triggers.\n- A single sync step is the simplest shape, but the two directions have different natural rhythms — uploading on every push to `main`, downloading on a schedule. See [split workflows](references/recipes.md#split-upload-and-download).\n- `actions/checkout@v7` was current in August 2026; check its releases rather than copying a pin from an older example.\n\n### 4. Grant the token write access\n\nPushing a branch and opening a PR needs `contents: write` and `pull-requests: write`. With the default `GITHUB_TOKEN` that means both the job-level `permissions:` block above **and** the repository setting **Settings → Actions → General → Allow GitHub Actions to create and approve pull requests**. Without the setting, PR creation fails even though the permissions look right.\n\nThe default token has one limitation no permission fixes: **pull requests it opens do not trigger workflows**, so the translation PR arrives with no CI checks. If checks must run on it, authenticate as something other than the Actions bot — a [GitHub App](references/recipes.md#github-app-authentication) (scoped, auto-rotating, the better default) or a classic PAT with the `repo` scope, passed as `GH_TOKEN` or `GITHUB_TOKEN`.\n\n### 5. Prove it runs\n\nValid YAML is not the finish line. This action fails at runtime — on credentials, file patterns and permissions — so the work is done when a run has printed the stages you expected, not when the file is written:\n\n1. Set `dryrun_action: true` and dispatch the workflow. The CLI prints the files it would manage, so wrong `source` patterns surface here, before anything reaches Crowdin.\n2. Drop the flag, dispatch again, and read the log through to the end: `UPLOAD SOURCES`, then `DOWNLOAD TRANSLATIONS`, then either a PR URL or `NOTHING TO COMMIT`.\n3. Re-run a misbehaving run with **Enable debug logging** — the action detects `RUNNER_DEBUG`, dumps its environment, and adds `--verbose --debug` to every CLI call.\n\nDispatching needs the human who holds the secrets; ask for the run's log rather than stopping at the workflow file.\n\n## What breaks, and why\n\nRead the step's log first: it prints `UPLOAD SOURCES`, `DOWNLOAD TRANSLATIONS`, `PUSH TO BRANCH`, `NOTHING TO COMMIT`, and `CREATE PULL REQUEST` as it goes, so the pipeline stage that failed is visible before you start guessing.\n\n| Symptom | Cause | Fix |\n|---|---|---|\n| Green run, no PR, log says `NOTHING TO COMMIT` | Downloaded translations are byte-identical to what's committed | Working as intended — nothing to do |\n| Green run, no PR, no download in the log | `download_translations` left at its `false` default | Set `download_translations: true` |\n| `ERROR: Either 'GITHUB_TOKEN' or 'GH_TOKEN' must be set` | Token missing from the step's `env:` | Add `GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}` |\n| `FAILED TO AUTHENTICATE WITH GITHUB`, or 403 on push | Job lacks write permissions, or the repo blocks Actions from creating PRs | Add the `permissions:` block **and** enable the repository setting (step 4) |\n| PR opens but no CI checks run on it | PRs from the default `GITHUB_TOKEN` don't trigger workflows | Use a GitHub App token or a PAT |\n| Checks restart on every sync run | The localization branch is force-pushed each time | Pass a PAT to `actions/checkout` so the pushes come from a user |\n| PR creation fails on a tag or release trigger | Base branch defaults to the triggering ref, which is `refs/tags/...` | Set `pull_request_base_branch_name` |\n| Later steps hit permission errors on downloaded files | The container runs as root, so the files it writes are root-owned | Add `user: auto` to detect the workspace owner |\n| `409` / build already in progress | Parallel runs against one Crowdin project | Serialize with `concurrency`, or `max-parallel: 1` in a matrix |\n| Build artifacts in the translation PR | `git add .` commits the whole workspace | Move the Crowdin step before the build, or into its own job |\n| Nothing downloads, project is string-based | `download_translations` doesn't apply to string-based projects | Download a bundle: `download_bundle: <id>` |\n| `crowdin.yml` not found | The action looks in the repository root | `config: path/to/crowdin.yml`, no leading `/` |\n| Enterprise: 404 or auth failure from the CLI | Enterprise needs an organization-specific API host | `base_url: 'https://{organization}.api.crowdin.com'` |\n\n## `command` — running any CLI command\n\n`command` (plus optional `command_args`) turns the step into an arbitrary CLI invocation: `status`, `auto-translate`, `bundle`, `string`, anything the CLI supports.\n\n```yaml\n- name: Fail the build unless translations are complete\n  uses: crowdin/github-action@v3\n  with:\n    command: 'status translation'\n    command_args: '--fail-if-incomplete'\n  env:\n    CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}\n    CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}\n```\n\nTwo consequences of how it is implemented, neither of them documented upstream:\n\n- **`command` replaces the pipeline.** The step runs the command and exits — no upload, no download, no commit, no PR, whatever the other inputs say. Sequencing a command between an upload and a download takes three separate steps ([AI pre-translation](references/recipes.md#ai-pre-translation) is the canonical example).\n- **The action's config inputs don't reach it.** `config`, `token`, `project_id`, `base_url`, `base_path`, `crowdin_branch_name`, and `dryrun_action` are not forwarded to a `command` step — only `command_args` is. Supply credentials through `env:` and put everything else in `command_args` (`--config path/to/crowdin.yml`, `--branch main`, `--dryrun`).\n\nThe command's stdout is available to later steps as the `command_output` output.\n\n## Which version to pin\n\n`@v3` is the current stable line and runs Crowdin CLI 5.\n\n`@v2` is the legacy line, bundling the Java-based Crowdin CLI 4. Every input, output, and default is identical between the two, so upgrading a pipeline-style step is a one-line pin change; what changes is the CLI underneath, and therefore the strings you pass to `command` and `*_args`. Most notably v4's `pre-translate` became `auto-translate`, and `--plain` became `-o plain`; the [migration reference](../crowdin-cli/references/migrating-from-v4.md) lists the rest. Stay on `@v2` only while a `command` step still depends on v4-specific CLI syntax.\n\n## Related skills\n\n- [crowdin-cli](../crowdin-cli/SKILL.md) — `crowdin.yml` configuration and the commands and flags behind every input\n- [crowdin-api-client](../crowdin-api-client/SKILL.md) — for automation the CLI doesn't cover\n- [croql](../croql/SKILL.md) — CroQL filters for `command_args` on string commands\n"
}

SHA-256 of public snapshot: 644241491ec2c383b98357ad7415f24736e9da1a437c136adaf7d4f37ffacd49