|
Documentation
Book a DemoPlatform
PlatformMCPCLIAPIWorkflows
GuidesChangelog

Continuous Localization

  • How it works
  • Setup

Platforms

  • GitHub App
  • GitHub Actions
  • GitLab CI/CD
  • Bitbucket Pipelines
  • Advanced patterns

GitLab CI/CD

Run Lingo.dev inside GitLab CI so every merge request that adds or changes source strings comes back with translations already filled in. The pipeline runs lingo push, the engine translates only the new or changed keys, and the result is committed straight onto the MR branch — translations show up in the MR diff and get reviewed before a human merges. Nothing lands on your default branch unseen.

Working example

A complete, runnable setup lives at gitlab.com/lingo.dev/gitlab-cicd-example.

Prerequisites#

  • A Lingo.dev organization and engine, plus a service API key (Dashboard → API Keys → create, type service).

  • A project configured for Lingo.dev. Generate it once with:

    bash
    npx @lingo.dev/cli@latest init   # scaffolds .lingo/config.json
    npx @lingo.dev/cli@latest link   # connects the project to your org + engine

    .lingo/config.json declares the source/target locales and source globs:

    json
    {
      "sourceLocale": "en",
      "targetLocales": ["es", "fr", "de", "zh"],
      "files": [{ "pattern": "locales/en.json" }],
      "orgId": "org_...",
      "engineId": "eng_..."
    }
  • A committed baseline. The very first time, translate everything and commit it so CI has a lockfile to diff against:

    bash
    npx @lingo.dev/cli@latest push --backfill-missing --wait
    git add locales .lingo && git commit -m "chore(i18n): baseline translations"

Access tokens#

Add two masked CI/CD variables under Settings → CI/CD → Variables:

  • LINGO_API_KEY — your Lingo.dev service key (lingo_sk_...). The CLI reads it automatically to authenticate.
  • GITLAB_PUSH_TOKEN — a Project Access Token with the write_repository scope (role Developer). This lets CI commit the translations back onto the MR branch.

Create the Project Access Token under Settings → Access tokens. CI_JOB_TOKEN can't push commits, so a dedicated token is required for the commit-back step. Project access tokens require a paid GitLab plan.

Pipeline#

Commit this .gitlab-ci.yml. It runs on merge requests targeting the default branch and pushes the translations back onto the MR's source branch:

yaml
stages:
  - localize

localize:
  stage: localize
  image: node:22-alpine
  rules:
    # Only on merge requests that target the default branch.
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH'
  before_script:
    - apk add --no-cache git
  script:
    # Pin the CLI version — never @latest; bump deliberately after testing.
    # --wait blocks until the engine finishes and writes files: since 1.6.0
    # `push` is async by default (it submits the run and exits), so CI must
    # wait to have something to commit.
    - npx -y @lingo.dev/cli@1.6.0 push --wait
    - |
      if [ -z "$(git status --porcelain)" ]; then
        echo "Translations already up to date — nothing to commit."
        exit 0
      fi
      git config user.name "lingo-bot"
      git config user.email "bot@lingo.dev"
      git add locales .lingo/lock.json
      # [skip ci] keeps the bot's own commit from re-triggering this pipeline.
      git commit -m "chore(i18n): sync translations [skip ci]"
      git push "https://oauth2:${GITLAB_PUSH_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git" "HEAD:${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}"

Try it#

bash
git checkout -b feat/new-strings
# add or change a key in locales/en.json
git commit -am "feat: add strings" && git push -u origin feat/new-strings
# open an MR feat/new-strings -> main (UI, or: glab mr create --fill --target-branch main)

The MR pipeline runs lingo push --wait, commits locales/{...}.json plus the updated .lingo/lock.json to the MR branch, and the translations appear in the diff. A reviewer adjusts any value, then merges.

How human edits survive#

lingo push preserves manual edits per key:

  • Edit a target string (its English source unchanged) → that string is kept; every other key keeps getting translated.
  • The English source behind an edited key changes → a fresh translation is generated for that key (the meaning changed).
  • A new source key is added → translated and added, even into files that contain manual edits.

So a reviewer's fix in the MR survives every later pipeline run, while new and changed keys flow in automatically.

push modes#

  • lingo push — incremental; the CI default. Translates only new/changed keys, preserves everything else. Add --wait in CI so it blocks until outputs are written (1.6.0+ is async by default).
  • lingo push --backfill-missing — first-push / new-locale bootstrap; fills target files that don't exist yet. Not for ongoing changes.
  • lingo push --force --yes — re-translate everything from scratch (overwrites manual edits). Rare.

Customization#

  • Auto-commit to the default branch instead of MRs: trigger on $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH and push back to $CI_DEFAULT_BRANCH. Simpler, but AI output lands on the default branch without review.
  • Pin specific strings: use preservedKeys / lockedKeys in .lingo/config.json to keep chosen keys fixed even when their source changes.
  • Self-hosted GitLab: works unchanged. On gitlab.com, new accounts must pass identity verification before shared runners execute CI jobs.

Next Steps#

Full example repo
A complete, runnable GitLab CI setup
GitHub Actions
Set up GitHub Actions integration
Advanced Patterns
Translation checks, merge conflicts, workflow selection
Connect Your Engine
Route CI/CD translations through your engine

Was this page helpful?

Max PrilutskiyMax Prilutskiy·Updated 10 days ago·4 min read