Getting Started

From an Unconfigured Repository to a Gated CI Job

The outputs below come from running the sequence against a Vue project with four locales in one layer. Your numbers differ; the shapes do not.

Install the CLI

npm install -g @the-i18n-kit/cli

Or skip the install and prefix every command with npx -y @the-i18n-kit/cli@latest — the binary is named the-i18n-cli either way, and that is the name used throughout this page.

the-i18n-cli is also published unscoped, under the name the binary has. Install the scoped @the-i18n-kit/cli; the unscoped package publishes from the same source at the same versions but stops receiving updates.

1. Generate a Config From Detection

Run init with --dryRun first. It reports the config it would write and touches no disk:

the-i18n-cli init --dryRun
{
  "config": {
    "$schema": "https://raw.githubusercontent.com/fabkho/the-i18n-kit/main/packages/mcp/schema.json",
    "context": "",
    "glossary": {},
    "translationPrompt": "",
    "localeNotes": {}
  },
  "detected": {
    "adapter": "vue",
    "label": "Vue",
    "confidence": 5,
    "derivesLocaleConfig": true
  },
  "configPath": ".i18n-mcp.json",
  "written": false
}

Read detected before you write anything. If adapter is not the framework you expected, detection explains how to pin one rather than working around a bad guess for the rest of this page.

derivesLocaleConfig: true is why the generated config names no locales and no directories: this adapter reads them out of the project's own files — a Vite plugin, a next-intl routing file, a Nuxt config — so restating them by hand would create two answers that can disagree. When no framework adapter matches, init probes for locale directories itself and writes localeDirs and defaultLocale into the file instead — that is the generic path, and derivesLocaleConfig is false on it.

Then write it:

the-i18n-cli init
ℹ Wrote .i18n-mcp.json (Vue, confidence 5)

init is non-interactive and refuses to overwrite an existing .i18n-mcp.json without --force, so running it in a configured repository cannot cost you a glossary. Full flags: init.

Verify: .i18n-mcp.json exists at your project root and contains a $schema pointer.

2. Take a Health Check

status reports coverage per locale and per layer — a layer being a scoped locale directory: a Nuxt layer, a workspace package, a shared UI library, or one app in a monorepo. A single-app project has one layer, named root.

the-i18n-cli status
{
  "locales": [
    { "code": "en-US", "total": 12, "translated": 12, "missing": 0, "empty": 0, "completion": 100 },
    { "code": "es-ES", "total": 12, "translated": 9, "missing": 3, "empty": 0, "completion": 75 },
    { "code": "fr-FR", "total": 12, "translated": 11, "missing": 1, "empty": 0, "completion": 91.7 }
  ],
  "layers": [
    { "layer": "root", "total": 36, "translated": 32, "missing": 4, "empty": 0, "completion": 88.9 }
  ],
  "summary": {
    "referenceLocale": { "code": "en-US", "language": "en-US", "file": "en-US.json" },
    "layersScanned": ["root"],
    "protectedLocales": [],
    "totalKeys": 36,
    "translatedKeys": 32,
    "missingKeys": 4,
    "emptyKeys": 0,
    "completionPercent": 88.9
  }
}

Output is JSON whenever stdout is not a terminal, so the-i18n-cli status | jq .summary works with no flag. Logs go to stderr, which is what keeps the pipe parseable. Pass --json to force it in a terminal too.

Check two fields:

  • referenceLocale is the locale everything is compared against. Some adapters derive it and some cannot: the Vue and React adapters take the alphabetically first locale they find, which is how a project with English source strings ends up measured against de-DE.
  • protectedLocales is empty until you say otherwise, so every locale is a candidate for machine translation.

Fix both in the file init wrote in step 1:

.i18n-mcp.json
{
  "$schema": "https://raw.githubusercontent.com/fabkho/the-i18n-kit/main/packages/mcp/schema.json",
  "defaultLocale": "en-US",
  "protectedLocales": ["de-DE"]
}

Run status again and both fields follow. A protected locale drops out of the counted set, so the completion percentage moves — that is the confirmation the file was read.

Verify: summary.referenceLocale.code is the locale your developers write strings in, and summary.protectedLocales names every locale a human maintains.

The Other Half of the Health Check

status measures locale files against each other. It cannot see a key your code calls that no locale file defines — that key renders as a raw string in production, and it is the failure your users notice first. check finds those:

the-i18n-cli check
{
  "undefinedKeys": [
    {
      "key": "booking.cancelled",
      "app": "default",
      "searchedLayers": ["root"],
      "usages": [{ "file": "src/App.vue", "line": 17 }]
    }
  ],
  "uncertainKeys": [],
  "summary": {
    "usedKeysChecked": 5,
    "undefinedCount": 1,
    "uncertainCount": 0,
    "filesScanned": 2,
    "locale": "en-US"
  },
  "gatesTripped": [
    {
      "name": "undefined-keys",
      "counter": "undefinedCount",
      "direction": "above",
      "threshold": 0,
      "observed": 1
    }
  ]
}

That run exits 2, not 1. Exit 2 means a gate tripped: the command worked and found what you asked it to fail on. Exit 1 would mean the run itself fell over and its counters say nothing about your project. check is the one command whose gate is always on and needs no flag, because a key that renders raw is a defect rather than a threshold you opt into caring about. The full table is on the CLI reference.

uncertainKeys holds references the static scanner could not resolve — template literals and other dynamically built keys. They are reported and never counted as findings, so a dynamic key cannot fail your build on a guess.

Verify: check exits 0 on a clean project, and exits 2 listing file and line when you delete a key that is still called.

3. Translate the Missing Keys

Preview before spending anything. --dryRun reports what would be translated and calls no provider at all:

the-i18n-cli translate --dryRun
{
  "summary": {
    "mode": "dry-run",
    "totalTranslated": 0,
    "totalFailed": 0,
    "totalSkipped": 0,
    "totalWouldTranslate": 4,
    "layers": ["root"],
    "referenceLocale": { "code": "en-US", "language": "en-US", "file": "en-US.json" },
    "targetLocales": [
      { "code": "es-ES", "language": "es-ES", "file": "es-ES.json" },
      { "code": "fr-FR", "language": "fr-FR", "file": "fr-FR.json" }
    ]
  }
}

targetLocales is the check that step 2 landed: the locale you protected is not in it. Without --layer, every locale-backed layer is translated in one run and the totals are aggregated across them.

Now the real run. Name a provider, a model and a key:

the-i18n-cli translate \
  --provider google \
  --model gemini-2.5-flash \
  --apiKey <your-api-key>

Substitute your own key for <your-api-key>, or leave --apiKey off and export OPENAI_API_KEY, ANTHROPIC_API_KEY or GEMINI_API_KEY to match your provider. --provider accepts openai, anthropic and google; --model is required whenever --provider is set. For a gateway, a proxy or a self-hosted model speaking one of those protocols, add --baseUrl — it falls back to I18N_BASE_URL and then to providerBaseUrl in your config file.

Leave --provider off entirely and nothing is translated: the run reports the keys as skipped with the reason no-provider. That is the mode an agent drives through the MCP server, where the agent's own model does the translating.

Every key is accounted for in the result — translated, failed with a reason from a fixed set, skipped with a reason — and provider output is validated before it is written: placeholder parity per plural variant, and plural variant-count parity with the source. A value that fails validation lands in failed rather than in your locale file. Flags: translate.

A partly failed run still writes what succeeded and exits 0. That is deliberate — the keys that failed stay missing and the next run retries them — but it means a green run is not proof of a complete run. Add --failOnFailed to exit 2 when any key failed.

Verify: status reports a higher completionPercent than it did in step 2, and your protected locale is untouched.

4. Gate a Pipeline on It

The commands above carry their own pass/fail decision in their exit code, so a CI job runs one command and needs no JSON parsing to decide an outcome.

GitHub Actions

.github/workflows/i18n.yml
name: i18n

on: pull_request

jobs:
  gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22

      - name: Fail on keys that would render raw
        run: npx -y @the-i18n-kit/cli@latest check

      - name: Fail when coverage drops below 90%
        run: npx -y @the-i18n-kit/cli@latest status --failUnder 90

Both steps fail the job with exit 2 on findings. Drop the second step if you do not want a coverage floor yet — check alone is the one gate worth having on day one, and it costs no API key.

To have the pipeline fix what it finds, add the action that ships with the kit. It installs the CLI and the SDK for your provider, runs translate, and opens a pull request with what it wrote:

.github/workflows/i18n-translate.yml
name: i18n translate

on: pull_request

jobs:
  translate:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v4

      - uses: fabkho/the-i18n-kit@main
        with:
          provider: google
          model: gemini-2.5-flash
          api_key: ${{ secrets.GEMINI_API_KEY }}
          layer: root

layer is required — pass the layer name status reported under layersScanned. Every input, with its default and whether it is required, is on the action reference.

GitLab CI

The kit ships a template with three reusable jobs. Include it and extend the one you want:

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

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

.i18n-check allows exit code 2 by default, which keeps findings informational — yellow, not red — while a scan that genuinely fell over still fails the job. Setting allow_failure: false, as above, turns findings into a hard gate.

The job writes a GitLab Code Quality report, which puts each finding on the merge request diff. The widget shows the difference between the merge request's report and the newest one from the default branch, so the second rule above is required: with no default-branch baseline to compare against, the widget stays blank.

Verify: open a merge request that calls a key you have not defined. The job fails, and the finding appears on the diff at the line that calls it.

Where to Go Next

  • Configuration — the two files kit policy can be declared in, and how each is found. A glossary and tone notes are what make provider translations sound like your product.
  • Monorepos — what changes when locales live in several layers and several apps consume them, and why usage is counted per consuming app.
  • Set up the MCP server — the same operations, driven by your agent instead of your terminal.
  • CLI reference — every command, generated from its own definition.

Limits

  • check and remove-orphans read your source statically and line by line. Keys built at runtime cannot be verified, so they are reported as uncertain and never treated as findings. That is a floor on what these commands can prove, not a bug to be configured away.
  • Coverage is measured against a reference locale. If that locale is itself missing a key, nothing here reports the key as missing anywhere — check is what catches it, from the calling side.
  • The translate gate is opt-in for a reason worth knowing before you turn it on: --failOnFailed fails a job that already committed the keys that succeeded.
Copyright © 2026