Documentation

Five minutes from an empty project to translated files.

Lingvo is driven from the command line and reviewed in the browser. The CLI reads the localization files already in your repository, so nothing here asks you to restructure a project first.

Install

The CLI is a single executable JAR that needs a Java 21 runtime. Build it from the repository, then put the wrapper somewhere on your PATH.

./gradlew :cli:installDist
export PATH="$PATH:$PWD/cli/build/install/lingvo/bin"

lingvo --version

Authenticate

Create a token in the web app under Settings → API tokens. It is shown exactly once. Credentials resolve in a fixed order and the first hit wins.

Credential sources, in resolution order
Source How to set it Use when
LINGVO_TOKEN export LINGVO_TOKEN=lgo_… CI, containers, agents. Nothing is written to disk.
~/.lingvo/credentials lingvo login --token-stdin A developer's own machine. The file is created mode 600.

Logging in without leaking the secret

Piping the token in means it never reaches argv, shell history or the process list. Passing it as a flag works and warns, because those three places all keep a copy.

# Best: never touches argv, history, or the process list
printf '%s' "$LINGVO_API_TOKEN" | lingvo login --token-stdin

# Also fine: read from a named environment variable
lingvo login --token-env LINGVO_API_TOKEN

# Check what would be used — no network call, never prints the token
lingvo auth status

Configure — lingvo.yaml

The config lives at the repository root. Running lingvo init writes one for you by detecting the localization files already present and inferring the project kind.

project: my-project-slug        # required — the project slug in Lingvo
source_locale: en               # required — the locale your source files are written in

files:                          # required — at least one entry
  - source: composeResources/values/strings.xml
    translation: composeResources/values-%locale%/strings.xml
    format: android_xml

  - source: iosApp/Localizable.strings
    translation: iosApp/%locale%.lproj/Localizable.strings
    format: ios_strings

generate:                       # optional — typed key accessors
  target: kotlin-object         # kotlin-object | swift-enum
  output: shared/src/commonMain/kotlin/app/Strings.kt
  package: app                  # kotlin-object only

%locale% is replaced with each target locale. Android uses its own qualifier form (values-pt-rBR), which lingvo init writes correctly for android_xml paths.

Commit .lingvo-lock.json
It is written next to lingvo.yaml and records the content hash of every synced file. Without it, sync cannot tell "unchanged" from "edited locally", and a local edit gets silently overwritten.

The sync loop

Three commands cover day-to-day use. None of them is metered on any plan — Lingvo does not charge for keys, languages or words.

Everyday commands
Command Key options What it does
lingvo status Per-locale progress for the project.
lingvo push --dry-run --file --keep-obsolete --translations --locale Uploads source files as keys. Keys absent from the payload are marked obsolete unless --keep-obsolete.
lingvo pull --locale --fallback --format Writes translated files to the configured paths. Untranslated keys are omitted, never written empty.
lingvo sync --dry-run --adopt-local --locale --fallback push then pull in one pass. A translation file edited locally is reported as a conflict; --adopt-local uploads it instead of overwriting.
lingvo watch Re-pushes source files when they change. Interactive dev loop only — not for CI.
Always --dry-run the first push in an unfamiliar repo
Without --keep-obsolete, keys that exist in Lingvo but not in the pushed file are marked obsolete. The dry run prints the exact counts, including obsoletions, and changes nothing.

Reading the counts

Every push and sync reports five counts. The one to watch is skipped: it means the file held keys this project does not define, so their translations went nowhere. A non-zero skipped on a translation push almost always means the wrong destination file, or a source push that never happened — not "already up to date".

Inspect and export

Inspection commands
Command Key options What it does
lingvo keys list --file --search --state --limit Lists and filters keys.
lingvo qa --locale --fail-on-error Lists QA issues. With --fail-on-error, exits 1 when any ERROR-severity issue exists — the CI gate.
lingvo export --locale --file --format --bundle --out Writes one exported file, or a zip of every locale.
lingvo usage Plan, billable seats, and machine-translation credits left this month.

Machine translation

Six providers are supported. mymemory and libretranslate need no key, so machine translation works before any setup at all.

Machine-translation commands
Command Key options What it does
lingvo providers list Shows configured providers. Never prints keys.
lingvo providers set <id> --key-env --base-url --model --disable Configures a provider. The secret is read from stdin or --key-env, never argv.
lingvo providers test <id> One-string round trip against the provider.
lingvo translate --locale --provider --only-empty --tone --dry-run Machine-translates and streams progress. Results failing placeholder or length validation are reported, never saved.

Provider ids: claude, openai, deepl, google, libretranslate, mymemory.

What credits are

One credit is one string machine-translated into one language; Claude and OpenAI cost three. A project using its own provider key spends none, because Lingvo is not paying for those calls. Running out never stops you translating by hand, importing, exporting or running QA.

Typed keys

lingvo generate reads the generate: block and emits typed accessors, so a renamed key becomes a compile error instead of a blank label in production. Name collisions are a hard error naming both keys — never a silent rename.

generate:
  - target: kotlin-object
    output: shared/src/commonMain/kotlin/app/Strings.kt
    package: app
  - target: swift-enum
    output: iosApp/Generated/L10n.swift

Keys are fetched once and written to every target. Two targets pointing at the same file is a configuration error, since whichever ran last would silently win.

In CI

Set LINGVO_TOKEN as a secret and nothing is written to disk. Branch on exit codes rather than message text — they are a stable contract.

- name: Pull translations and gate on QA
  env:
    LINGVO_TOKEN: ${{ secrets.LINGVO_TOKEN }}
  run: |
    lingvo pull
    lingvo qa --fail-on-error
Exit codes
Code Meaning Retry?
0 Succeeded.
1 Ran, and reported an expected failure: QA errors with --fail-on-error, sync conflicts, rejected machine translations. No — fix the content.
2 Bad flag, bad argument, or invalid lingvo.yaml. No — fix the command.
3 No credential, or the server rejected it (HTTP 401/403). No — re-authenticate.
4 Server unreachable, timed out, rate-limited, or 5xx. Yes, with backoff.

Every command also takes --json, which prints a single stable envelope on stdout while human-readable progress stays on stderr.

Formats

Every format works in both directions, so you keep the native file type for each platform instead of reshaping your localization stack around the tool.

  • android_xml — Android string resources, including plurals and locale qualifiers
  • ios_strings, ios_stringsdict, xcstrings — Apple .strings, .stringsdict and String Catalogs
  • json_nested, json_flat — the two shapes web projects actually use
  • properties — Java and Spring resource bundles
  • yaml — Rails and generic YAML catalogues
  • csv — spreadsheets, for the people who insist
  • xliff, po — the interchange formats every other tool speaks