lingo push

Max PrilutskiyCEO & Co-FounderUpdated: 27 days ago · 7 min read

Push source files to the engine, wait for the run, and write outputs to disk.

text
lingo push [patterns...] [--key <pattern>] [--force] [--backfill-missing] [--yes] [--wait] [--estimate]

Default behavior — delta push#

With no arguments, lingo push runs the delta-only mode:

  1. Hash every source file matched by the config's files patterns
  2. Diff each hash against the lockfile to find sources that changed
  3. Upload changed sources as a run on the engine
  4. Wait for the run to complete
  5. Write outputs to disk
  6. Commit the new source hashes to the lockfile

If no source has changed since the last successful push, the command short-circuits with ✓ Nothing to push. — no server round-trip, no token spend.

Arguments and flags#

Positional: patterns... — scoped push#

bash
lingo push docs/en/about.md
lingo push 'docs/en/**/*.md' 'locales/en.json'

Restricts the push to specific files (must match patterns already in .lingo/config.json). Switches the command into scoped mode:

  • No previous-source diff — every matching source is treated as in-scope, even if unchanged.
  • Server-side noop for targets that already exist with matching source hashes — the engine skips them and the CLI reports them as cached.

Use when you want to translate exactly one updated file without rehashing the entire project, or when you want to retranslate a single page with --force.

--key <pattern>#

bash
lingo push --key auth.login
lingo push --key auth.login --key billing.plan
lingo push --key "auth.*"

Re-translate only the keys a pattern claims, merge them into the existing translation, and leave every other key byte-identical. Repeatable — one --key per pattern.

A key scope ignores the source diff, so a key whose source text never changed is still re-translated. That is the point of the flag: it is the supported way to redo a handful of strings after a wording change, a model switch, or a glossary update, without paying for the rest of the file.

--force adds nothing beside it and suppresses the whole-file confirmation prompt.

What happens to each key#

Named in --keyPresent in the translationResult
yesyesre-translated
yesnotranslated and added
noyesexisting translation kept
nononot written at all

The last row is what separates a key scope from a normal push. A key added to the source since your last full push is not carried into the translation as source text — it is left out, and the next plain lingo push translates it.

How a pattern matches#

PatternClaims
auth.loginauth.login and auth.login.title — never auth.login_url
authauth and its whole subtree — never authority
"auth.*"everything under auth, including auth.login_url, but not auth
"auth*"the above plus authority — no boundary at all

A pattern matches a key exactly, as a prefix ending on a ., /, - or [ boundary, or as a glob. Array members are reachable through the bracket boundary, so nav.items claims nav.items[0].title.

Quote globs. Your shell expands them first: in zsh a bare --key auth.* either aborts with no matches found, or — if a file such as auth.json happens to sit in the directory — silently becomes that filename. A comma-separated value is not a list: --key "a,b" is one literal pattern that matches nothing. Repeat the flag instead.

What it refuses#

A key scope reports and skips rather than quietly doing more than you asked:

  • A locale with no translation yet. There is nothing to merge into, so the locale is named and skipped — translate it once with --backfill-missing, then use --key.
  • Formats that cannot leave a key out — the document formats, whose keys shift as soon as the document is edited, and xcode-stringsdict, whose plural categories the file needs in order to stay valid. Formats has the full list. These files are dropped with a warning, so a push may still mix them with key-value files; push them without --key.
  • A scope that matched nothing says so, instead of reporting the run as already up to date.

Positional members keep their source text even under a scope — array elements, Android <string-array> items, <plurals> quantities — because removing one would renumber the rest.

It does not advance the lockfile#

A key-scoped run translates part of a file, so it deliberately leaves the source hash in the lockfile alone. Whatever else changed in that file is still pending, and the next plain lingo push picks it up.

--force / -f#

bash
lingo push docs/en/about.md --force

Retranslate every matching target, ignoring any existing translations and bypassing server-side caching. Scope it — with positional patterns or --backfill-missing — unless you really do mean the whole project: bare lingo push --force retranslates every configured pattern, and the confirmation below is the only thing in the way.

On a project that has never been translated there is nothing to overwrite, so --force buys nothing there — reach for --backfill-missing instead. It is the safer habit in general: it only ever fills gaps, and it never prompts.

By default --force prompts before running:

text
! --force will retranslate every target for pattern(s): docs/en/about.md and
  overwrite existing translations. Continue? (Yes, retranslate / Cancel)

Pass --yes / -y to skip the prompt (CI-friendly).

To redo a few strings rather than whole files, reach for --key — it costs only the keys you name.

--backfill-missing#

bash
lingo push --backfill-missing

Translate every target that doesn't exist yet across every configured pattern. Equivalent to a scoped push over all config patterns, but only producing files where they're absent. Use after adding a new locale to targetLocales, or on the first push of a new project.

Combine with --force to retranslate everything from scratch:

bash
lingo push --backfill-missing --force --yes

--yes / -y#

Skips the --force confirmation prompt. No effect without --force, and none alongside --key — a key scope never prompts, because it only ever touches the keys you named.

--estimate#

bash
lingo push --estimate
lingo push 'docs/en/**/*.md' --estimate

Print the approximate cost of this push and exit without translating. The CLI runs the full push pipeline — hashing, delta, and uploading source bytes so the server can plan the exact delta — then asks the engine to price the run instead of starting it. Nothing is translated, written, or billed; the lockfile and your target files stay untouched.

Values are estimates, not quotes. --estimate composes with scope and with --key / --force / --backfill-missing, so you can price exactly the push you're about to run.

If no source has changed, --estimate short-circuits with ✓ Nothing to push., just like a regular push.

If a run for the same sources is already underway, --estimate fails rather than pricing a half-started run:

text
Error: Cannot estimate: existing group run_a8c... is already in 'running' state. Change a source file or wait for the run to finish.

Output#

On success:

text
Pushing source files to localization engine…
✓ Run run_a8c...: localized 12 target file(s), 4 already up-to-date, uploaded 1 new artifact(s).

The summary breaks down into:

  • Localized N target file(s) — the engine produced new translations and the CLI wrote them.
  • N already up-to-date — server-side cache hits (source matched, target reused).
  • Uploaded N new artifact(s) — sources that the engine hadn't seen before (binary/large content stored once, referenced thereafter).
  • N target(s) skipped (local edits) — local target hashes diverge from the lockfile. Rerun with --force to overwrite.

On per-target failure the CLI prints each failed target's error and exits non-zero — useful for CI:

text
✓ Run run_a8c...: localized 10 target file(s).
  2 target(s) failed:
    locales/de.json: rate limit on engine; retry later
    locales/fr.json: timeout

With --estimate:

text
Estimating push cost…
› Estimated cost: ~$1.87 (12 target(s), ~48,000 output tokens — estimate, not a quote)
  de: ~$0.9350 (6 target(s), ~24,000 tokens)
  fr: ~$0.9350 (6 target(s), ~24,000 tokens)
  4 target(s) already up-to-date — no cost.
✓ Estimate complete — nothing was translated. Run `lingo push` to start the translation.

Retry semantics#

The lockfile is updated only after a fully successful run. A partial failure (e.g. one locale times out) leaves source hashes unchanged in the lockfile, so the next lingo push retries the same diff — no manual reset.

If the engine errors out before any translation happens (auth, validation), nothing is written and the lockfile is unchanged.

Common patterns#

CI: translate-on-merge#

yaml
- run: lingo push --backfill-missing --yes
- run: git add . && git commit -m "chore: refresh translations" && git push

--backfill-missing is the safe default: doesn't overwrite anything, only fills gaps.

Redo a few strings#

bash
lingo push --key auth.login --key billing.plan --wait

Re-translate exactly those keys after a wording change, leaving every other key in the file untouched.

Single-file iteration#

bash
lingo push docs/en/onboarding.md -f -y

Retranslate just one source after a major copy change. Skip the prompt for fast iteration.

Adding a new locale#

After bumping targetLocales in .lingo/config.json:

bash
lingo push --backfill-missing

Translates the entire corpus into the new locale without retranslating existing ones.