mcpField
G
github-action

github-action

github.com/crowdinCloud & DevOps944 installsUpdated 3d ago
Repository

About github-action

Crowdin GitHub Action runs Crowdin CLI in a Docker-based GitHub Actions step to sync source files and translations. It can commit downloads to a localization branch and open a pull request, with inputs to control the pipeline stages.

Key features

  1. 01上传源文件和翻译
  2. 02下载源文件、翻译或 bundle
  3. 03提交到本地化分支并推送
  4. 04按需创建拉取请求
  5. 05通过 command 运行 CLI 命令

Use cases

  1. 01在代码推送时将源文件同步到 Crowdin
  2. 02定期下载翻译并提交拉取请求
  3. 03在工作流中检查翻译状态

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/crowdin/skills --skill github-action

After installing, type/github-actionin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

evals/
evals.json
references/
inputs.md
recipes.md

Details

From the project README

Crowdin GitHub Action

crowdin/github-action runs 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.

Because 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-*.

Reference: README · examples · inputs and outputs · workflow recipes

The step is a pipeline, not a command

One step performs up to five operations, always in this fixed order, and each one is gated by its own input:

OrderInputRuns
1upload_sources: true (default)crowdin upload sources
2upload_translations: truecrowdin upload translations
3download_sources: truecrowdin download sources, then the git half
4download_translations: truecrowdin download, then the git half
5download_bundle: <id>crowdin bundle download <id>, then the git half

The 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.

That order explains nearly every surprise the action produces:

  • 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.
  • The git half runs only after a download. create_pull_request: true on an upload-only step is inert — there is nothing to commit.
  • Two downloads in one step run the git half twice — two commits to the same localization branch, under one PR.
  • 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.
  • 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.

Setting it up

Two 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.

1. Configure crowdin.yml

The action reads the same crowdin.yml as the CLI — the crowdin-cli skill covers files, placeholders, and language mapping. Three parts matter specifically here:

"project_id_env": "CROWDIN_PROJECT_ID"
"api_token_env": "CROWDIN_PERSONAL_TOKEN"
"preserve_hierarchy": true

files:
  - source: "/src/locales/en/**/*.json"
    translation: "/src/locales/%two_letters_code%/**/%original_file_name%"
  • project_id_env / api_token_env point at the environment variables the workflow supplies, so no credential is ever committed.
  • 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.
  • Keep the file at the repository root, or pass its path via the config input (no leading /).

Repositories with a single source and translation pattern can skip the file entirely and pass source and translation as inputs instead — see no-crowdin.yml. Pick one or the other: a crowdin.yml in the repository conflicts with those inputs.

2. Create the repository secrets

In Settings → Secrets and variables → Actions, add:

  • CROWDIN_PROJECT_ID — the numeric project ID, from Tools → API in the Crowdin project.
  • CROWDIN_PERSONAL_TOKEN — a Crowdin personal access token (crowdin.com, or Account Settings → Access Tokens in Enterprise).

Scope 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.

3. Write the workflow

.github/workflows/crowdin.yml:

name: Crowdin

on:
  push:
    branches: [ main ]
  workflow_dispatch:

permissions:
  contents: write
  pull-requests: write

concurrency:
  group: crowdin
  cancel-in-progress: false

jobs:
  synchronize:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7

      - name: Synchronize with Crowdin
        uses: crowdin/github-action@v3
        with:
          upload_sources: true
          upload_translations: false
          download_translations: true

          localization_branch_name: l10n_crowdin
          create_pull_request: true
          pull_request_title: 'New Crowdin translations'
          pull_request_base_branch_name: main
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
          CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

Details worth keeping when you adapt it:

  • 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.)
  • workflow_dispatch is what lets you test the workflow without waiting for a push.
  • 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.
  • 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.
  • 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.
  • actions/checkout@v7 was current in August 2026; check its releases rather than copying a pin from an older example.
4. Grant the token write access

Pushing 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.

The 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 (scoped, auto-rotating, the better default) or a classic PAT with the repo scope, passed as GH_TOKEN or GITHUB_TOKEN.

5. Prove it runs

Valid 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:

  1. 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.
  2. 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.
  3. 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.

Dispatching needs the human who holds the secrets; ask for the run's log rather than stopping at the workflow file.

What breaks, and why

Read 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.

SymptomCauseFix
Green run, no PR, log says NOTHING TO COMMITDownloaded translations are byte-identical to what's committedWorking as intended — nothing to do
Green run, no PR, no download in the logdownload_translations left at its false defaultSet download_translations: true
ERROR: Either 'GITHUB_TOKEN' or 'GH_TOKEN' must be setToken missing from the step's env:Add GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FAILED TO AUTHENTICATE WITH GITHUB, or 403 on pushJob lacks write permissions, or the repo blocks Actions from creating PRsAdd the permissions: block and enable the repository setting (step 4)
PR opens but no CI checks run on itPRs from the default GITHUB_TOKEN don't trigger workflowsUse a GitHub App token or a PAT
Checks restart on every sync runThe localization branch is force-pushed each timePass a PAT to actions/checkout so the pushes come from a user
PR creation fails on a tag or release triggerBase branch defaults to the triggering ref, which is refs/tags/...Set pull_request_base_branch_name
Later steps hit permission errors on downloaded filesThe container runs as root, so the files it writes are root-ownedAdd user: auto to detect the workspace owner
409 / build already in progressParallel runs against one Crowdin projectSerialize with concurrency, or max-parallel: 1 in a matrix
Build artifacts in the translation PRgit add . commits the whole workspaceMove the Crowdin step before the build, or into its own job
Nothing downloads, project is string-baseddownload_translations doesn't apply to string-based projectsDownload a bundle: download_bundle: <id>
crowdin.yml not foundThe action looks in the repository rootconfig: path/to/crowdin.yml, no leading /
Enterprise: 404 or auth failure from the CLIEnterprise needs an organization-specific API hostbase_url: 'https://{organization}.api.crowdin.com'

command — running any CLI command

command (plus optional command_args) turns the step into an arbitrary CLI invocation: status, auto-translate, bundle, string, anything the CLI supports.

- name: Fail the build unless translations are complete
  uses: crowdin/github-action@v3
  with:
    command: 'status translation'
    command_args: '--fail-if-incomplete'
  env:
    CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
    CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

Two consequences of how it is implemented, neither of them documented upstream:

  • 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 is the canonical example).
  • 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).

The command's stdout is available to later steps as the command_output output.

Which version to pin

@v3 is the current stable line and runs Crowdin CLI 5.

@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 lists the rest. Stay on @v2 only while a command step still depends on v4-specific CLI syntax.

Related skills

  • crowdin-cli — crowdin.yml configuration and the commands and flags behind every input
  • crowdin-api-client — for automation the CLI doesn't cover
  • croql — CroQL filters for command_args on string commands

Comments

Markdown supported