CI/CD

GitLab CI

The template at the repository root defines three hidden jobs you extend, each installing @the-i18n-kit/cli and running one command. .i18n-translate commits and pushes translated locale files; .i18n-cleanup reports orphan keys and .i18n-check keys that render raw. Only the first writes to your repository.

Including the Template

Include the v8 ref: it follows the CLI's major, so template changes reach you with the release they were written for. main can run ahead of the CLI your job installs.

.gitlab-ci.yml
include:
  - remote: 'https://raw.githubusercontent.com/fabkho/the-i18n-kit/v8/gitlab-ci.yml'

stages:
  - lint

i18n-check:
  extends: .i18n-check
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Declare a lint stage: every job sets stage: lint, and GitLab rejects a pipeline naming a stage that does not exist. The URL tracks main, so pin it to a tag, and pin the CLI with I18N_CLI_VERSION.

Each job takes its outcome from the CLI's exit code. The scanning jobs carry allow_failure.exit_codes: [2]: findings warn yellow and still reach the merge request widget, a scan that fell over fails red. allow_failure: false on your job makes findings block the merge.

Translating

.gitlab-ci.yml
i18n-translate:
  extends: .i18n-translate
  variables:
    I18N_PROVIDER: google
    I18N_MODEL: gemini-2.5-flash
    I18N_API_KEY: $GEMINI_API_KEY
    I18N_LOCALE_PATHS: "i18n/locales/ app-*/i18n/locales/"
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      changes: ['i18n/locales/en.json']

Those three variables are required; without them the job exits 1 before calling anything. The key reaches the CLI as the environment variable of the provider you chose, never on the command line.

VariableDefaultDescription
I18N_LAYER(all)One layer, or every one in a single commit
I18N_LOCALES, I18N_SOURCE_LOCALE, I18N_KEYS(all)Targets, reference, key subset
I18N_BATCH_SIZE50Keys per LLM call
I18N_DRY_RUNfalsePreview, writing nothing
I18N_FAIL_ON_FAILED(off)Exit 2 when a key failed
I18N_LOCALE_PATHSi18n/locales/Globs staged and committed — must cover every layer
I18N_MEMORY_FILE.i18n-kit.lock.jsonThe translation memory, staged with the translations it describes
I18N_COMMIT_MESSAGE(generated)Replaces the message, [skip ci] included
I18N_PUSH_TOKEN(empty)Pushes with this instead of the job token
I18N_CLI_VERSION, I18N_INSTALL_PEER_DEPS8 (the template's major), (empty)CLI version, extra packages
Translations for a layer outside I18N_LOCALE_PATHS are written on the runner and never committed: a clean-looking run, and the keys still missing on the next one.

The job also stages the translation memory at I18N_MEMORY_FILE, which is what a later run reads to tell a translation still made from the current source from one whose source has changed since. It is skipped when the file does not exist — translationMemory: false — and when the project's .gitignore covers it, in which case nothing records what each translation was made from and stale stays empty.

The job allows failure, and its gate trips only after the commit and push, so a red job never costs you translations. The result stays as .i18n-reports/translate.json.

Letting it push. A CI token cannot write unless you allow it: Settings → CI/CD → Job token permissions → Allow Git push requests to the repository, or a token with write_repository in I18N_PUSH_TOKEN. Give that token the api scope too — a token push starts no pipeline, and the job uses api to start one at the new head, which keeps the merge request widgets alive after an auto-commit.

Scanning

.i18n-cleanup reports orphan keys and passes no flag that deletes any; I18N_FAIL_ON_ORPHANS: "true" makes them trip a gate. .i18n-check has nothing to opt into — a key that renders raw is a defect, so its gate always runs. Both take I18N_LAYER and keep their report and gl-codequality.json for 7 days. missing, status and find-duplicates write the same report format with --codequalityOutput <file> if you add a job for them.

gl-codequality.json renders as a diff against the newest default-branch report, so a job running only on merge requests has no baseline and the widget stays blank. Give every job a second rule for the default branch, without a changes: filter. Baselines expire with the artifacts; raise expire_in on a quiet branch.

Overriding

image, before_script, tags and cache are overridable on your job — the supported path for private registries; I18N_INSTALL_PEER_DEPS adds packages without replacing anything. A replacement before_script keeps the-i18n-cli on the PATH, keeps the || exit 1 that stops a failed install from reading as findings, and installs git and curl. Keep it POSIX sh.

Copyright © 2026