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.
| 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.
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.
| 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. |
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
| 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.
| 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
| 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