|
Documentation
Book a DemoPlatform
PlatformMCPCLI
APIWorkflows
GuidesChangelog

Lingo.dev CLI

  • How it works
  • Setup
  • Quick Start
  • Monorepos

Configuration

  • Supported Formats
  • i18n.json
  • i18n.lock
  • Supported Locales

Features

  • Existing Translations
  • Adding Languages
  • Overrides
  • Translator Notes
  • Translation Keys
  • Key Renaming
  • Key Locking
  • Key Ignoring
  • Key Preserving
  • Extract Keys with AI

Performance

  • Large Projects
  • Parallel Processing

Retranslation

  • Automatic Retranslation
  • Retranslation
  • Remove Translations

Monorepos

Max PrilutskiyMax Prilutskiy·Updated about 12 hours ago·4 min read

The CLI supports monorepos natively. Use a single i18n.json at the root with a recursive ** glob to auto-discover packages, a single root config with explicit per-package paths, or separate i18n.json files in each package combined with a matrix-strategy GitHub Action.

When to use each pattern#

ScenarioPatternConfig filesGitHub Action
All packages share the same layout and locales, and you want new packages picked up automaticallyRecursive single configOne i18n.json at root with a ** globOne step, default working-directory
All packages share source and target localesSingle configOne i18n.json at rootOne step, default working-directory
Packages need different target localesPer-package configsOne i18n.json per packageMatrix strategy with working-directory
Packages need different engines (glossary, brand voice)Per-package configs + separate enginesOne i18n.json per package, each with its own engineIdMatrix strategy with working-directory

Pattern 1: Recursive single config#

When every package follows the same layout (e.g. apps/<name>/locales/[locale].json), use a recursive ** glob in a single root i18n.json. New packages are picked up automatically as long as they match the layout:

json
{
  "$schema": "https://lingo.dev/schema/i18n.json",
  "version": "1.15",
  "locale": {
    "source": "en",
    "targets": ["es", "fr", "de", "ja"]
  },
  "buckets": {
    "json": {
      "include": ["apps/**/locales/[locale].json"]
    }
  }
}

The CLI ignores node_modules, .git, dist, build, .next, and .turbo by default, so recursive patterns won't descend into vendored or build trees. Add your own exclude entries for anything else you want skipped.

The GitHub Action is identical to Pattern 2 below – one step, default working directory.

Use this when every package shares the same source locale, target locales, layout, and localization engine, and you don't want to edit i18n.json every time you add a package.

Pattern 2: Single config, shared locales#

One i18n.json at the repository root. Bucket paths reach into each package explicitly:

json
{
  "$schema": "https://lingo.dev/schema/i18n.json",
  "version": "1.15",
  "locale": {
    "source": "en",
    "targets": ["es", "fr", "de", "ja"]
  },
  "buckets": {
    "json": {
      "include": [
        "apps/web/locales/[locale].json",
        "apps/dashboard/locales/[locale].json"
      ]
    },
    "markdown": {
      "include": ["packages/docs/content/[locale]/*.md"]
    }
  }
}

GitHub Action – standard single step:

yaml
name: Translate
on:
  push:
    branches: [main]
permissions:
  contents: write
  pull-requests: write
jobs:
  translate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: lingodotdev/lingo.dev@main
        with:
          api-key: ${{ secrets.LINGODOTDEV_API_KEY }}
          pull-request: true
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Use this when every package shares the same source locale, target locales, and localization engine, but the layouts differ enough that a recursive glob would be too broad.

Pattern 3: Per-package configs, different locales#

Each package has its own i18n.json with independent locale targets:

apps/web/i18n.json:

json
{
  "$schema": "https://lingo.dev/schema/i18n.json",
  "version": "1.15",
  "locale": {
    "source": "en",
    "targets": ["es", "fr", "de", "ja", "ko", "zh-Hans"]
  },
  "buckets": {
    "json": {
      "include": ["locales/[locale].json"]
    }
  }
}

apps/marketing/i18n.json:

json
{
  "$schema": "https://lingo.dev/schema/i18n.json",
  "version": "1.15",
  "locale": {
    "source": "en",
    "targets": ["es", "fr", "de"]
  },
  "buckets": {
    "json": {
      "include": ["locales/[locale].json"]
    },
    "markdown": {
      "include": ["content/[locale]/*.md"]
    }
  }
}

GitHub Action – matrix strategy with working-directory:

yaml
name: Translate
on:
  push:
    branches: [main]
permissions:
  contents: write
  pull-requests: write
jobs:
  translate:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        package: [apps/web, apps/marketing]
    steps:
      - uses: actions/checkout@v4
      - uses: lingodotdev/lingo.dev@main
        with:
          api-key: ${{ secrets.LINGODOTDEV_API_KEY }}
          working-directory: ${{ matrix.package }}
          pull-request: true
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Each matrix entry runs the CLI scoped to that package's i18n.json. The action translates only the files that package declares.

Pattern 4: Per-package configs, separate engines#

Same structure as Pattern 3, but each package connects to a different localization engine. This gives each package its own glossary, brand voice, and model configuration.

apps/product/i18n.json:

json
{
  "$schema": "https://lingo.dev/schema/i18n.json",
  "version": "1.15",
  "locale": {
    "source": "en",
    "targets": ["es", "fr", "de", "ja"]
  },
  "buckets": {
    "json": {
      "include": ["locales/[locale].json"]
    }
  },
  "engineId": "eng_ProductApp123"
}

apps/docs/i18n.json:

json
{
  "$schema": "https://lingo.dev/schema/i18n.json",
  "version": "1.15",
  "locale": {
    "source": "en",
    "targets": ["es", "fr"]
  },
  "buckets": {
    "markdown": {
      "include": ["content/[locale]/*.md"]
    }
  },
  "engineId": "eng_DocsEngine456"
}

The GitHub Action YAML is identical to Pattern 3 – the engineId in each i18n.json routes translations to the correct engine automatically.

How the lockfile works#

Each i18n.json location produces its own i18n.lock file in the same directory. The lockfile tracks which source content has been translated, enabling incremental updates. In Patterns 1 and 2, there is one lockfile at the root. In Patterns 3 and 4, each package has its own lockfile.

FAQ#

Will new packages be picked up automatically?

Only with Pattern 1 (recursive **). With Patterns 3 and 4, you add a new entry to the workflow's matrix.package list when you add a package.

Can I have different locales per package?

Yes. Use Pattern 3 or 4. Each package's i18n.json declares its own locale.targets array independently.

Do I need one workflow file or multiple?

One workflow file with a matrix strategy handles all packages. Each matrix entry runs against a different working-directory, scoping the CLI to that package's config.

Can packages share a glossary but have different locales?

Yes. Point both packages to the same engineId but configure different locale.targets in each i18n.json. The engine's glossary applies to whichever locale pairs are requested.

Next Steps#

i18n.json
Full configuration reference
GitHub Actions
All workflow patterns and inputs
Supported Formats
25+ file formats with examples
Connect Your Engine
Route translations through your engine

Was this page helpful?