# Lingo.dev: an open source CLI and compiler for repo-based localization

> The lingodotdev/lingo.dev repository ships a CLI, a React compiler and a Docker entrypoint that connect a codebase to hosted localization engines. It is for teams that want translations to live in the repository rather than in a spreadsheet.

**lingodotdev/lingo.dev** — Open-source localization engineering tools. Connects to Lingo.dev localization engineering platform for consistent, quality translations.

- Repository: https://github.com/lingodotdev/lingo.dev
- Website: https://lingo.dev
- Stars: 5,408 · Forks: 813
- Language: TypeScript
- License: Apache-2.0
- Published: 2026-09-22 · Updated: 2026-09-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/lingodotdev-lingo-dev

## The problem Lingo.dev addresses in a JavaScript repository

Most localization workflows break at the handoff. Strings are extracted into a file, that file is uploaded to a translation service, translations come back in a different shape, and someone reconciles the two by hand. Lingo.dev treats the repository as the source of truth and the platform as a service the repository calls. The README describes it as a localization engineering platform that measures translation quality, translates with LLMs and proofreads with native speakers. The target reader is a developer or a localization engineer working in TypeScript or JavaScript, on React, React Native or a plain Node.js service. The repository topics confirm that focus: i18n, javascript, react, react-native, typescript. If your stack is a Java backend with .properties bundles and no Node.js runtime anywhere, this is not the tool for you.

## Engines, precedence layers and how a request is resolved

The core abstraction is the localization engine: a stateful translation API the team configures and Lingo.dev runs. A request through an engine applies every configured layer in a fixed order. The README lists those layers as LLM models (which model handles each language pair, with ranked fallbacks, drawn from 400+ models, and the response names the model that ran), brand voice (how the product speaks per locale), rules (linguistic conventions such as adjective position in Spanish or a space before percentage signs), glossary (exact term mappings per locale, matched by meaning, so "911" becomes "112" for European markets and product names pass through), and AI reviewers (scoring that runs after every translation, including GEMBA scores, BERTScore and glossary compliance). Glossaries, rulesets and brand voices belong to the organization and an engine applies them by attachment, so one glossary can govern five engines and one edit reaches all five. That attachment model is the interesting design choice: it separates reusable linguistic assets from the per-product pipeline that consumes them.

## Installing the CLI and pushing your first translation

The README gives a three-step path for repository content. Install the CLI globally, initialize and link the project, then push. The push sends the files to the engine named in .lingo/config.json, and a later pull writes translations back from any machine. The README does not document what lingo init writes into that config file, so treat the generated file as the thing to inspect before your first push reaches a real engine.

```bash
npm install -g @lingo.dev/cli
lingo init && lingo link
lingo push
```

If you would rather call an engine directly, the README shows a synchronous HTTP call that names the engine by ID and passes a source locale, a target locale and a data object. The response carries the translated data, the model that ran and token usage with a cost figure.

```javascript
const res = await fetch("https://api.lingo.dev/process/localize", {
  method: "POST",
  headers: { "X-API-Key": process.env.LINGO_API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({
    engineId: "eng_abc123",
    sourceLocale: "en",
    targetLocale: "de",
    data: { greeting: "Hello, world!", cta: "Get started" },
  }),
});

const { data, model, usage } = await res.json();
```

The README does not state a default rate limit or a timeout for this endpoint. The CLI side claims eighteen formats, including JSON, YAML, Markdown, MDX, PO, XLIFF, Flutter ARB, Android and Xcode strings, SubRip and PHP. That format list is the strongest practical argument for the CLI over a hand-rolled script.

## Running lingo.dev ci in Docker and in CI runners

The repository ships a Dockerfile that wraps the CI command. The base image is node:20.12.2-alpine, and the file adds git with a comment explaining that git is required by lingo.dev ci and is not included in the node:alpine base image. The entrypoint runs npx lingo.dev@latest ci and passes the API key, pull request, commit message, pull request title, working directory and process-own-commits flags through environment variables prefixed with LINGODOTDEV_.

```dockerfile
FROM node:20.12.2-alpine

RUN apk add --no-cache git

ENTRYPOINT ["sh", "-c", "npx lingo.dev@latest ci \
  --api-key \"$LINGODOTDEV_API_KEY\" \
  --pull-request \"$LINGODOTDEV_PULL_REQUEST\" \
  --commit-message \"$LINGODOTDEV_COMMIT_MESSAGE\" \
  --pull-request-title \"$LINGODOTDEV_PULL_REQUEST_TITLE\" \
  --working-directory \"$LINGODOTDEV_WORKING_DIRECTORY\" \
  --process-own-commits \"$LINGODOTDEV_PROCESS_OWN_COMMITS\""]
```

Note the version pinning problem. The image fixes Node.js at 20.12.2 but the entrypoint resolves lingo.dev@latest at container start, so two builds of the same image a week apart can run different CLI versions. The README's CI/CD page says any runner with Node.js 22+ works, which does not match the Node 20 base image in the Dockerfile. Pick one and pin it deliberately. For teams that would rather not manage a runner at all, the README describes a GitHub App that installs once and opens or updates a translation pull request on every push to the default branch, with no runner, no API key secret and no lockfile to manage.

## Where Lingo.dev is the wrong choice

The CLI is a client. There is no documented self-hosted translation path in the README; the engine configuration, the playground and the reviewer scoring all live on the platform, and the API call needs an X-API-Key header. If your organization cannot send source strings to a third-party endpoint, the repository does not describe an alternative. A second constraint is version drift. The monorepo publishes many packages under the @lingo.dev scope, and the recent releases show three of them moving on the same day at different versions (@lingo.dev/compiler@0.4.14, @lingo.dev/_react@0.7.12 and @lingo.dev/_compiler@0.12.15 on 2026-09-14). The leading underscore on two of those package names is a fair signal that they are internal build artifacts rather than stable public APIs. If you depend on them directly, expect churn. A third case: if your translators already work inside a translation management system with its own review workflow, adding a second review layer through AI reviewers duplicates the process rather than replacing it.

## How this differs from gettext and from a TMS-first workflow

The closest conventional alternative is a gettext-style pipeline built on PO files, where msgmerge reconciles catalogs locally and a human or a service fills in the fuzzy entries. Lingo.dev's CLI does read and write PO among its eighteen formats, so it can sit in that pipeline, but the reconciliation step is not local: lingo push sends files to a remote engine and lingo pull brings results back, with the engine deciding how the glossary, rules and brand voice apply. The other alternative is a translation management system used as the system of record, where developers export strings and the TMS owns the file. That inverts the ownership. Lingo.dev keeps the repository as the place where translations land, in the same format they were read from, which is why the eighteen-format list matters more than the model list for day-to-day work. The trade is that the linguistic configuration lives on the platform, not in the repo, so a reviewer reading a pull request sees translated strings without seeing the rules that produced them.

## Conclusion

Adopt Lingo.dev if your translations already live in the repository and you want one command to move them through a configured engine, or if you are building a React or React Native app and want the compiler to extract strings. Do not adopt it if you need a fully offline pipeline: the CLI is a client for a hosted platform and needs an API key. Before committing, verify which engine the .lingo/config.json points at, confirm the CLI version you installed, and check the licence file at LICENSE.md for the terms that apply to your use.

## FAQ

### What is Lingo.dev?

It is an open source localization toolkit in TypeScript that connects a repository to the Lingo.dev localization engineering platform. The repository ships a CLI, a React compiler and a Docker entrypoint for CI, and the README describes the platform as the place where engines, glossaries, rules and brand voices are configured.

### What is the Lingo.dev alternative if I cannot use a hosted platform?

The README does not document a self-hosted translation path. The CLI pushes files to an engine named in .lingo/config.json and the direct API call requires an X-API-Key header, so a local gettext or PO-based pipeline is the alternative the repository itself does not provide.

### How do I install the Lingo.dev CLI?

The README gives the command npm install -g @lingo.dev/cli, followed by lingo init && lingo link and then lingo push. The push sends repository files to the engine named in .lingo/config.json.

### Which LLM does Lingo.dev use for translation?

The engine configuration decides, per language pair, with ranked fallbacks drawn from a set the README describes as 400+ models. The API response names the model that actually ran, so you can log it rather than assume it.

### What file formats can the Lingo.dev CLI handle?

The README lists eighteen formats, including JSON, YAML, Markdown, MDX, PO, XLIFF, Flutter ARB, Android and Xcode strings, SubRip and PHP. Files are pushed in one of those formats and translations are pulled back in the same one.

### Does the Lingo.dev Docker image pin the CLI version?

No. The Dockerfile fixes the base image at node:20.12.2-alpine but the entrypoint runs npx lingo.dev@latest ci, so the CLI version is resolved when the container starts rather than when the image is built.

## Sources

- [License: Apache-2.0](https://github.com/lingodotdev/lingo.dev/blob/main/LICENSE)
- [lingodotdev/lingo.dev on GitHub](https://github.com/lingodotdev/lingo.dev)
- [Project website](https://lingo.dev)
- [README](https://github.com/lingodotdev/lingo.dev/blob/main/README.md)
- [Releases](https://github.com/lingodotdev/lingo.dev/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/lingodotdev-lingo-dev
