The CLI's persistent state lives in three places: two inside the project (committed), one in your home directory (per-machine).
.lingo/config.json — committed#
Created by lingo init (localization section) and lingo link (org/engine binding). Commit it.
{
"orgId": "org_a8c...",
"engineId": "eng_b9d...",
"sourceLocale": "en",
"targetLocales": ["de", "fr", "es"],
"files": [
{ "pattern": "locales/en.json" },
{ "pattern": "docs/en/**/*.md" }
]
}Field reference#
| Field | Required | Description |
|---|---|---|
orgId | yes (after link) | Organization that owns the engine. |
engineId | yes (after link) | Engine that performs the translation. Holds model config, glossary, brand voice. |
sourceLocale | yes | Locale code of your source files (e.g. "en"). Source files are read; never written. |
targetLocales | yes | Locales to translate into. Outputs are written to the same directory as the source, with the locale code substituted into the pattern (locales/en.json → locales/de.json). |
files | yes | Array of source patterns. pattern is a glob (forward slashes, ** recursion, * wildcards). The CLI substitutes locale codes when resolving target paths. |
github | no | Settings for the GitHub App — ignored by the CLI itself. |
Pattern → target mapping#
The CLI replaces the source locale in the pattern with each target locale:
locales/en.json→locales/de.json,locales/fr.json, ...docs/en/**/*.md→docs/de/**/*.md(mirrored subtree)copy/en/marketing.md→copy/de/marketing.md
If your source layout doesn't include the locale code in the path, restructure it. The CLI can't infer where target files should go without it.
.lingo/lock.json — committed#
Tracks the last-known-server hash of every source and target file. Used for two things:
lingo pushconsults it to decide whether a source has changed since the last successful run. Unchanged files become a no-op without a server round-trip.lingo pullconsults it to detect local target edits — if a local target's hash differs from what the lockfile says, pulling would overwrite local work, sopullerrors unless you pass--force.
Source hashes are committed to the lockfile only after a fully successful push, so a half-completed run can be retried without manual cleanup.
Commit the lockfile alongside translated outputs. Treat conflicts the same way you'd treat package-lock.json conflicts: regenerate by running lingo push again.
~/.lingo/runs/<hash>.json — per-machine#
Records the most recently submitted push so that lingo pull knows which run's outputs to fetch — works across closed terminals and across machines that share the same checkout.
{
"runId": "run_a8c...",
"engineId": "eng_b9d...",
"organizationId": "org_a8c...",
"sourceLocale": "en",
"createdAt": "2026-05-22T14:32:01.000Z"
}The filename hash is derived from the absolute project root path, not from file contents, so:
- Editing
en.jsondoesn't invalidate the file — the samepullwill still find the run. - Two checkouts of the same repo on the same machine get distinct files (different absolute paths).
- Moving the project (
mv ~/Projects/foo ~/Projects/bar) between push and pull invalidates the lookup, since the hash changes. The JSON is still in~/.lingo/runs/if you need to recover the run ID manually.
This file is per-machine state, not project state — it isn't gitignored because it lives outside the repo entirely.
Authentication credentials — ~/.lingo/auth.json#
Stored by lingo login (OTP flow stores a Supabase session; --api-key flow stores the key). Not committed, never read by anything except the CLI itself.
lingo logout # clear credentials
lingo whoami # check what's stored and which org/engine the cwd resolves toResolution from subdirectories#
Every command walks up from cwd looking for the nearest .lingo/config.json. Running lingo push from src/components/ writes its lockfile back to the project root, not a fresh .lingo/ in components/. So you don't have to cd to root before each command.
