Most of the time you want every string in a file translated. For the exceptions — brand names, feature flags, legal copy, internal junk — the CLI gives you three per-file controls, set inside a files[] entry in .lingo/config.json.
| Control | Config field | What the engine does |
|---|---|---|
| Lock | lockedKeys | Copies the source value into every target, untranslated. |
| Preserve | preservedKeys | Keeps whatever is already in the target, never overwrites it. |
| Ignore | ignoredKeys | Omits the key from the target file entirely. |
All three take key paths in dot/bracket notation that mirrors the structure of the file:
{
"files": [
{
"pattern": "content/en/app.json",
"lockedKeys": ["meta.version"],
"preservedKeys": ["legal.terms"],
"ignoredKeys": ["internal.debug"]
}
]
}Lock — keep the value identical everywhere#
lockedKeys copies the source value into every target file without translating it. Use it for values that must stay byte-for-byte identical across locales:
{
"pattern": "content/en/app.json",
"lockedKeys": ["meta.version", "config.apiUrl"]
}de.json and fr.json get meta.version with the exact source string. Change the source and the next lingo push propagates the new value to every locale, still untranslated.
Preserve — protect a hand-written target#
preservedKeys tells the engine never to overwrite a target value that already exists. Use it when a key needs human translation — legal text, compliance copy, anything you've reviewed and don't want the model touching:
{
"pattern": "content/en/settings.jsonc",
"preservedKeys": ["featureFlags"]
}The engine seeds the key from the source on first translation, then leaves your edits alone on every run after that. Compare with overrides below.
Ignore — drop the key from the output#
ignoredKeys removes the key from target files altogether — it isn't translated, copied, or written. Use it for debug strings, internal flags, and test data that should never reach a translated build:
{
"pattern": "content/en/app.json",
"ignoredKeys": ["internal.debug", "dev.testData"]
}JSON and JSONC
Key controls operate on structured key/value formats — json and jsonc. For markdown-family formats, scope translation with translateFrontmatterFields and translateComponentProps instead (see Formats).
Overrides vs. preserve#
There are two ways an existing target value survives a run:
- Preserve (
preservedKeys) — declarative. The key is protected by config, on every locale, forever. - Local edits —
lingo pushcompares each target's hash against the lockfile. If you hand-edited a target file, the push reports it asskipped (local edits)and leaves it untouched. Pass--force(with a scope) to overwrite. See lingo push.
Reach for preservedKeys when the protection is permanent and locale-wide; rely on local-edit detection for one-off manual tweaks.
Migrating from the legacy CLI#
The legacy CLI also had key renaming (carry a translation forward when a key id changed). That isn't part of the current CLI — translation state is tracked per file hash, so renaming a key re-translates it on the next push. Lock, preserve, and ignore all carry over, with one change: paths use dot/bracket notation (meta.version) instead of the old slash notation (meta/version).
