translato-cli
v2.0.1
Published
Translato CLI — push & pull translation keys from CI/CD pipelines
Maintainers
Readme
translato-cli
CLI for Translato — push and pull translation keys between your local i18n files and the Translato TMS.
Install
npm install -g translato-cliQuick Start
# 1. Log in via your browser
translato login
# 2. Create config files interactively
translato init
# 3. Push source keys AND values to the TMS
translato push
# 4. Pull all translations (including source language) to local files
translato pull
# 5. Or do both in one step
translato syncCommands
| Command | Description |
|---------|-------------|
| translato init | Setup — creates .translato.yml and .env; interactive, or from flags with --yes |
| translato login | Authenticate with Translato via browser (device authorization flow) |
| translato logout | Remove stored credentials from .env |
| translato push | Upload source language keys and values to the TMS |
| translato pull | Download translations for all languages (including source) to local files |
| translato sync | Two-way sync: push keys then pull translations |
| translato status | Show project info and per-language translation progress bars |
| translato validate | Check files for missing keys, extra keys, empty values, and placeholder mismatches |
| translato diff | Compare local translation files with remote TMS translations |
| translato watch | Watch source files for changes and auto-push on save |
| translato langs add <codes> | Add languages to the project (de,fr or de fr) |
| translato langs list | The project's languages and how far each one has got |
| translato terms rm <keys> | Delete keys and their translations in every language |
| translato translations set <lang> <key> <value> | Set one translation without a file |
| translato migrate namespace-keys | One-off: move translations off the doubled keys an older CLI stored |
| translato config | Print the settings this run would use, and where each came from |
| translato whoami | Which account and project the current credentials belong to |
| translato keys | The keys the project holds on the server |
| translato doctor | Check config, credentials, files, permissions, lock and connectivity |
| translato completion <shell> | Print a completion script: bash, zsh, fish or powershell |
| translato update | Update the CLI to the latest published version |
Login
translato loginAuthenticates using a device authorization flow. The CLI displays a one-time code and opens your browser. Log in to Translato in the browser, enter the code, and the CLI receives an API token automatically. The token is saved to .env for future use.
🔑 Logging in to Translato...
Your authorization code: ABCD-EFGH
A browser window will open. Log in to Translato and enter the code above.
If the browser doesn't open, visit:
http://translato.app/auth/cli?code=ABCD-EFGH
Waiting for authorization... (expires in 10 minutes)
✅ Login successful!
Your API key has been saved to .env.
Run translato status to verify your connection.Logout
translato logoutRemoves TRANSLATO_API_KEY, TRANSLATO_PROJECT, and TRANSLATO_HOST from your .env file. If .env becomes empty after removal, the file is deleted. Other environment variables in .env are preserved.
Init
translato initInteractive setup wizard that creates a .translato.yml config file and optionally saves credentials to .env. The command:
- Auto-detects your locales directory — scans for
src/locales,src/i18n,src/lang,locales,i18n,lang,translations,public/locales,public/i18n,assets/locales,assets/i18n, andresources/lang. - Auto-detects languages — reads filenames (
en.json,de.yml) and subdirectories (en/,fr/) from the locales directory. - Auto-detects namespaces — finds namespace files like
common.json,auth.jsonin the source language directory. - Prompts for configuration — project name, locales directory, source language, target languages (if not detected), API key, and project ID.
- Verifies credentials — if both API key and project ID are provided, calls the API to confirm they work before writing config.
- Writes
.translato.yml— safe to commit (no secrets). - Writes credentials to
.env— with0600permissions on Unix (owner-only). - Updates
.gitignore— adds.env,.env.lock,.translato.state, and.translato.lockif missing.
🚀 Initializing Translato project...
📁 Detected locales directory: ./src/locales
🌐 Detected languages: en, de, fr, es
Project name (my-app):
Locales directory (src/locales):
Source language (en):
✓ Languages detected from files: en, de, fr, es
API key (or press Enter to set later in .env):
Project ID (or press Enter to set later in .env):
✅ Created .translato.yml (safe to commit)
✅ Added .env, .env.lock, .translato.state, .translato.lock to .gitignore
Then run: translato pushIn non-interactive environments (CI, piped input), the command skips prompts and uses auto-detected values or defaults.
Tip: If you already ran
translato login, your API key is already in.env—initwill pick it up automatically.
Push
translato push reads your source language files, extracts all keys, and uploads them to the Translato API:
- Terms — every key in the source files is registered as a term in the project.
- Source translations — the actual values (e.g.
"Hello","Active API Access Tokens") are stored as translations for the source language. This ensures the TMS shows the real text instead of "click to translate".
The CLI tracks file hashes locally (.translato.state) and skips the push when nothing has changed since the last push. Use --force to push anyway.
Pushing keys to Translato...
📄 ./src/i18n/en.json — 5 keys
Total unique keys: 5
Terms — parsed: 5, added: 2
Translations (en) — parsed: 5, added: 2, updated: 3
✅ Push complete.Before pushing, the CLI performs two safety checks:
- Pull-before-push guard — if the remote project has translations you haven't pulled yet (or you've never pulled), the push is blocked. Run
translato pullfirst, or use--forceto skip. - Orphan key detection — remote keys that no longer exist in your local source files are flagged, and the push is aborted. Use
--pruneto delete them, or--forceto push and leave them in place. Leaving them is not free: project progress is computed as translated ÷ total terms, so every dead key permanently understates how complete each language is.
Pull
translato pull downloads translations for every language in the project (including the source language) and writes them to local files. On success, it records a pull timestamp in .translato.state so the push guard knows your workspace is current. If any language fails to pull, the timestamp is not updated.
Pulling translations from Translato...
Languages: en, fr, es, de
✅ en 5 keys no changes → src/i18n/en.json
✅ fr 5 keys +2 added ~1 changed → src/i18n/fr.json
✅ es 5 keys new file → src/i18n/es.json
⏭ de no translations yet
2 of 3 file(s) changed, 1 skipped: +7 added ~1 changed
✅ Pull complete.Languages are downloaded in parallel (up to --concurrency at a time, default: 5). Per-language errors are handled gracefully — one failed language won't stop the rest from being pulled.
Sync
translato syncTwo-way sync: push, then pull, in one command. Useful as a one-liner for CI
or when you want to upload new keys and download the latest translations in one
step.
On a workspace that has never pulled — a fresh clone, a new CI runner — a
bare push refuses, because the project already holds translations you have not
seen. sync is asked to reconcile both directions, so it pulls first and then
pushes. That pull merges: remote values win on a key both sides have, and a key
that exists only locally survives to be pushed. (It did not always: until the
fifth pass that pull deleted such a key and the push then sent what was left.)
Status
translato statusShows project information and per-language translation progress:
📊 Translato Status
Credentials: API key found
Project ID: cmnee05bm0001fw1uv7jo5f6k
API Host: https://translato.app
Authentication: ✅ Key accepted
Project: My App
Terms: 248
Created: 2026-03-31
Last push: 2026-08-27T09:12:04.001Z
Last pull: 2026-08-27T09:10:55.320Z
Languages:
en ████████████████████ 100%
de ██████████████░░░░░░ 71%
fr ████████████░░░░░░░░ 60% 12 need review
es ████░░░░░░░░░░░░░░░░ 20%Validate
translato validate
translato validate --lang es,frChecks all translation files against the source language for:
- Missing keys — keys present in source but absent in a target language
- Extra keys — keys in a target file that don't exist in source
- Empty values — keys that exist but have no translation text
- Placeholder mismatches —
{name},{{count}},%{user},%s,%dpatterns that differ between source and target
Exit code is non-zero when issues are found — useful as a CI gate.
Diff
translato diff
translato diff --lang de,frCompares your local translation files against what is stored in the remote TMS. Shows keys that exist only locally, keys only in remote, and keys with different values. Languages are compared in parallel (up to --concurrency).
🔄 Comparing local files with remote translations...
Languages: en, fr, de
✅ en — in sync
⚠️ fr — 3 difference(s):
+ 1 key(s) only in local files
+ nav.settings: Settings
~ 2 key(s) with different values
~ greeting:
local: Bonjour le monde
remote: Bonjour
✅ de — in sync
──────────────────────────────────────────────────
⚠️ 3 total difference(s) found.Watch
translato watchWatches source language files for changes and auto-pushes when a file is saved.
Changes to target language files are ignored — and a file whose name looks like a
locale but is a namespace (app.json, nav.json) counts as a source file, which
it did not until the fifth pass. The CLI debounces rapid saves (500 ms) and never
runs two pushes at once; a save that arrives mid-push is pushed straight after.
The workspace lock is held around each push, not for the session, so other
commands still work while the watcher runs. Each push is a whole-project push, so
the guards a bare push applies apply here too, and the watcher stays up when
one of them stops a push. Ctrl+C stops it and exits 130.
Inspecting the setup
translato whoami # which project the current credentials belong to
translato keys # the keys the project holds on the server (--json for a list)
translato config # the settings this run would use, and where each came from
translato langs list # the project's languages and how far each one has got
translato doctor # config, credentials, files, .gitignore, lock, connectivity — exit 2 if anything is off
translato completion zsh >> ~/.zshrc # or bash
translato completion fish > ~/.config/fish/completions/translato.fish
translato completion powershell | Out-String | Invoke-Expressiontranslato config is the fastest answer to "why is it pulling the wrong
files?": it prints the effective project, host, glob, format, source language
and language filter, names the source of each — flag or environment,
.translato.yml, or the built-in default — and lists the files the glob
matches.
Configuration
.translato.yml
Running translato init creates this file in your project root. It is safe to commit — no secrets are stored here.
# Translato CLI Configuration
project: my-app
paths:
locales: ./src/locales
source: en
format: json # json | yaml | po | properties
indent: 2 # spaces of indentation in written files (0-8)
languages: # default for --lang; `init` fills it from the project
- en
- de
- fr
- es
namespaces:
- name: common
file: common.json
hooks:
pre-push: npm run lint:translations
post-pull: npm run format:translations
timeout: 30 # seconds (default: 60, minimum: 5).env (secrets)
Credentials are stored in .env — never commit this file.
TRANSLATO_API_KEY=your-api-key
TRANSLATO_PROJECT=your-project-idThe CLI loads .env automatically on startup. You can also set these as real environment variables (e.g. in CI).
Environment Variable References
Use ${VAR_NAME} syntax inside .translato.yml to reference environment variables. They are resolved at load time. The CLI warns when a referenced variable is not set.
paths:
locales: ${MY_LOCALES_DIR}Config Priority
CLI flags are saved to .env and take effect immediately. Priority:
CLI flags > environment variables > .env file > .translato.yml valuesYou can set credentials with any command — they are saved for future use:
translato status --api-key sk-my-key --project proj-123Global Options
These can be passed to any command:
| Option | Env Variable | Description |
|--------|-------------|-------------|
| --api-key <key> | TRANSLATO_API_KEY | API key — saved to .env for future use |
| --project <id> | TRANSLATO_PROJECT | Project ID — saved to .env for future use |
| --host <url> | TRANSLATO_HOST | API server URL (optional — only for self-hosted instances) |
| --files <glob> | — | File glob pattern (default: from config or src/locales/*.json) |
| --format <fmt> | — | File format: json / yaml / po / properties |
| --out-dir <path> | — | Where to write pulled translations. A language sub-directory is added only when the filename would otherwise collide across languages — not when the filename or the out-dir already names the language (--out-dir res/values-de writes strings.xml, not de/strings.de.xml). A relative path outside the project is allowed |
| --concurrency <n> | — | Max parallel language operations (default: 5, range 1–20; a value outside it is clamped with a warning) |
| --force | — | Skip orphan key warnings and change detection on push |
| --prune | — | Delete remote keys that no longer exist locally (push only). Asks first at a terminal; assumes yes under --json, with --yes, or with no TTY |
| --lang <codes> | — | Only process these languages (comma-separated, e.g. de,fr; case-insensitive, and an unknown code is named) |
| --exclude-lang <codes> | — | Skip these languages (comma-separated, e.g. en,ja) |
| --skip-existing | — | Skip files that already exist locally (pull only) |
| --json | — | Output results as JSON (for CI pipelines) — implies --exit-code |
| --exit-code | — | Exit 1 from diff when local and remote differ |
| --local-only | — | Forget credentials locally without revoking the server token (logout) |
| --config <path> | — | Use a .translato.yml somewhere else. Its directory becomes the project: the .env beside it is loaded, its globs, lock, state file and written locale files all resolve there |
| --quiet | — | Warnings and errors only |
| --no-color | — | No colour (also honours NO_COLOR) |
| --timeout <ms> | — | Request timeout, minimum 1000 (default: 30000) |
| --retries <n> | — | Retries for a failed request, 0–10 (default: 3) |
| --no-delete | — | pull keeps local keys the server no longer has (in every layout, namespaced included) |
| --min-completion <pct> | — | status exits 1 when a language is below this percentage |
| --token <key> | — | login saves this key without opening a browser (verified before it is saved) |
| --no-save | — | Do not write --api-key / --project / --host to .env |
| --yes | — | Do not ask: init takes the detected defaults, push --prune deletes without confirming |
| --no-hooks | — | Run without the pre-push / post-pull hooks in .translato.yml |
| --no-source | — | Leave the source language out of a pull or diff (also paths.pullSource: false) |
| --check | — | pull writes nothing and exits 1 when a pull would change a file |
| --dry-run | — | Preview changes without writing anything. pull and push --prune still read from the API to report what would change; nothing is written and no hook runs |
| --verbose | — | Show detailed output including HTTP requests |
| -v, --version | — | Print CLI version |
Exit Codes
Every command uses the same codes, so a CI script can tell "the secret is missing" from "the check failed".
| Code | Meaning | Examples |
|------|---------|----------|
| 0 | The command did what it was asked | a push with nothing to send, a pull where every file already matched, diff finding drift without --exit-code |
| 1 | The command ran and the answer is no | orphaned keys would be left behind, validate found issues, diff --exit-code (or --json) found drift, the API rejected the key, the host could not be reached |
| 2 | Nothing ran, because the setup is wrong | no API key or project ID, a language filter matching none of the project's languages, an unsupported --format or file extension, validate with no target-language files, a glob whose every file is a target language, a configured hook that is not enabled (see Hooks) |
| 3 | Some languages failed; the rest were done | a language that could not be downloaded, written, or compared — pull and diff both use it |
| 4 | Another translato is writing in this workspace | a concurrent push, pull or sync holds .translato.lock — safe to retry |
| 130 | Interrupted — Ctrl+C or SIGTERM, after a graceful stop | watch stopped by hand |
In --json mode every failure also prints one object on stdout — {"success":false,"error":"…","exitCode":…}, or a richer object where the command has one (a blocked push lists orphanedKeys).
File Formats
What the formats can and cannot carry
The server stores flat key/value pairs, so a few things are converted on the way up. The CLI says so when it happens rather than changing your file quietly:
| In your file | What is pushed | Note |
|---|---|---|
| YAML scalars | the exact text | version: 1.10 stays 1.10; nothing is re-typed |
| JSON number / boolean | the text of it (5, true) | the type is read back from the file being replaced, so {"count": 5} stays a number on disk; a new file gets strings |
| An object keyed "0", "1", … | one key per index | stays an object if the local file had one, and an array if it had an array |
| YAML merge keys (<<) | not applied | warned; write the keys out |
| JSON array | one key per index (plurals.0, …) | round-trips: a node keyed exactly 0..n-1 comes back as an array |
| JSON null | empty string | warned |
| .properties key:value | supported, like key=value | =, : and spaces in keys are escaped on write |
| .properties value with a newline | \n, decoded again on read | round-trips |
| .properties value with a backslash | \\, decoded again on read | round-trips |
| .properties key starting # or ! | escaped, so it is not read as a comment | round-trips |
| PO msgid_plural | one key per form | warned: writing the file back does not restore msgid_plural |
| PO #: references and #. notes | kept | read from the file being replaced and re-emitted |
| PO #, flags (fuzzy) | dropped | the flag describes a translation the pull may have just replaced |
| PO header (Language:, Plural-Forms:) | not pushed | kept exactly as the file had it, and no generated timestamp is written |
| .properties value ending in \ | \\, decoded again on read | round-trips; a line is continued only on an odd trailing backslash, as in Java |
| .properties value padded with spaces | \ at either end | round-trips; whitespace after the separator is separator |
| .properties key value (space separator) | supported, like key=value | Java's third separator |
| ARB @@locale / @key metadata | not pushed | kept for the keys that still exist |
| Android <plurals>, <string-array>, comments, translatable="false" | not pushed | the file is edited in place, so all of it stays; a <string> inside a comment is not a key |
| Android and XLIFF inline markup (<xliff:g>, <g>) | part of the string | kept as markup rather than escaped, while a value that is not markup still is |
| A CSV column for a language the sheet does not have | added on write | reading it gives nothing rather than another language's text |
| A CSV row this language no longer has | kept, with this language's cell cleared | the row belongs to every language in the sheet |
| XLIFF notes, states, unit attributes | not pushed | kept; a unit with no <target> reads as its <source> |
| .xcstrings | not read | one file holds every language, which "one file per language" cannot express — the run stops and says so |
| CRLF line endings | read in every format | a file that used CRLF keeps it when written |
A pull writes keys in the order the local file already uses, with new keys appended — so the diff shows what changed, not a reordering of everything.
| Format | Extensions | Notes |
|--------|-----------|-------|
| JSON | .json | Nested objects flattened to dot notation on push, unflattened on pull |
| YAML | .yml, .yaml | Full YAML support via js-yaml |
| PO/Gettext | .po, .pot | msgid/msgstr, multiline, msgctxt, plural forms; header kept |
| Properties | .properties | key=value, key:value, key value; \uXXXX escapes; line continuation |
| ARB (Flutter) | .arb | JSON plus metadata; @@locale and @key blocks kept |
| Apple strings | .strings | "key" = "value";; the comment above a key is kept |
| CSV | .csv | key,value, or a sheet with one column per language — de.csv reads and writes the de column and leaves the others alone |
| Android | .xml | <string name="…">; edited in place, so plurals, arrays and comments stay |
| XLIFF | .xlf, .xliff | 1.2 <trans-unit> and 2.0 <unit>; edited in place, so notes and states stay, and a new key is added in the shape that file's version uses |
Every format is checked the same way: parse → write → parse is identity for everything it carries, in LF and CRLF, and writing twice produces the same bytes — so a pull that changes nothing leaves no diff.
JSON nesting
Nested JSON objects are flattened to dot-separated keys for the TMS:
{ "common": { "hello": "Hello" } }Becomes the term key common.hello with value Hello. On pull, it is unflattened back to the original nested structure.
Namespaces
When namespaces are configured in .translato.yml, the CLI groups keys by namespace:
- Push: Keys from files matching a namespace (e.g.
auth.json) are prefixed asauth:login.titlewhen uploaded. - Pull: Keys with a namespace prefix (e.g.
auth:) are split back into the correct namespace file. - The CLI auto-detects directory-based (
en/common.json) vs flat (en.json) locale structures. - A namespace file whose name looks like a locale —
app.json,nav.json,api.json— is treated as a namespace, because the config says so.
Files that don't match any namespace entry are handled as ungrouped keys.
Directory-per-language layouts without namespaces
translato init writes this pattern for you: when a source-language directory
exists it sets paths.locales: ./src/locales/en/*.json and prints how many files
match, so a config no command can use never reaches your repository.
A layout whose glob names the source language directory —
src/locales/en/*.json, the i18next and next-intl default — needs no
namespaces: block. Every source file is mirrored under the target language,
keeping its own name (src/locales/de/common.json, not de/de.json), and each
key goes to the file whose source counterpart defines it. A key the source
layout does not know — added remotely since the last pull — goes to the largest
file, and --verbose says which.
pull and diff decide this the same way, so a project on either layout can
gate CI on diff --exit-code.
Hooks
Hooks run shell commands from
.translato.yml, so they are opt-in. With noTRANSLATO_ALLOW_HOOKS=1in the environment, a command that has a hook configured stops (exit 2) and prints it — read it first, then export the variable (a cloned repository cannot set it for you). It stops rather than continues because apre-pushhook usually regenerates the locale files, and carrying on without it pushed whatever was last committed and exited 0. Pass--no-hooksto run without the hook deliberately.
Hooks run in the project's directory — the one --config names, when it names
one — and inherit the environment, TRANSLATO_API_KEY included.
| Hook | Runs | Triggered By |
|------|------|-------------|
| pre-push | Before uploading keys | push, sync, watch |
| post-pull | After downloading translations | pull, sync |
Hooks run in a shell and abort the command if they exit with a non-zero code. The timeout is configurable (default: 60 seconds, minimum: 5 seconds):
hooks:
pre-push: npm run lint:translations
post-pull: npm run format:translations
timeout: 30 # secondsSecurity: Dangerous patterns (eval, backtick subshells, curl | sh, source, xargs, find -exec, etc.) are blocked. Commands starting with known-safe prefixes (npm, npx, yarn, pnpm, bun, node, prettier, eslint, biome, echo, true) bypass the blocklist check. Commands longer than 500 characters are rejected.
Note: Hooks execute shell commands. Always audit hook commands in
.translato.ymlbefore running the CLI in a new project, especially from untrusted sources.
Language Filtering
Use --lang and --exclude-lang to limit which languages are processed. These flags work with pull, validate, and diff.
# Only pull German and French
translato pull --lang de,fr
# Pull everything except English and Japanese
translato pull --exclude-lang en,ja
# Validate only Spanish translations
translato validate --lang es
# Compare only specific languages with remote
translato diff --lang de,frWhen both flags are used together, --lang is applied first (allowlist), then --exclude-lang removes from the result.
Dry Run
Preview what would happen without changing anything:
translato push --dry-run # the keys it would send, and what --prune would delete
translato pull --dry-run # per-file counts: added, changed, removed
translato logout --dry-run # what it would revoke and remove
translato langs add de --dry-run
translato terms rm app.old --dry-run
translato translations set de app.title Titel --dry-runA dry run writes no files, records no sync point and runs no hook. pull and
push --prune still read from the API — that is where the numbers come from —
and say so. push --dry-run also runs the pull-before-push check and reports
what a real push would do, instead of reporting success in a state where the
real push refuses.
pull --check
translato pull --checkAnswers one question, for CI: would a pull change anything? Nothing is written;
exit 1 and a per-file list when something would change, exit 0 when every file
already matches. diff --exit-code compares against the export rather than
against what a pull would write, so it cannot answer this on a layout where the
two differ.
JSON Output
Use --json to get structured JSON output on stdout (human-readable output goes to stderr). Useful for CI pipelines and scripting:
translato status --json
translato validate --json
translato diff --jsonOne object per run, on stdout — except watch, which is a stream and emits one
object per event (watch-started, then change-detected, push-complete or
push-failed as they happen). A failure prints an object too, and it agrees with
the exit code: pull --json whose post-pull hook fails reports
"success": false, because the hook now runs before the payload is written.
Environment variables
| Variable | Effect |
|----------|--------|
| TRANSLATO_API_KEY | The API key. Written by login and init |
| TRANSLATO_PROJECT | The project ID |
| TRANSLATO_HOST | The API host, for a self-hosted instance |
| TRANSLATO_ALLOW_HOOKS=1 | Run the pre-push / post-pull hooks from .translato.yml. They are shell commands from a committed file, so they are opt-in |
| TRANSLATO_ALLOW_CONFIG_HOST=1 | Trust an api.endpoint in .translato.yml that is not the default host. Without it the run stops rather than sending credentials to a host a committed file chose |
| TRANSLATO_NO_BROWSER=1 | login prints the URL instead of opening a browser (also skipped with no TTY, or under CI) |
| TRANSLATO_NO_UPDATE_CHECK=1 | No update check. NO_UPDATE_NOTIFIER, CI, --json and --quiet also skip it |
| NO_COLOR | No coloured output, same as --no-color |
| HTTP_PROXY / HTTPS_PROXY / NO_PROXY | Proxy for every request, the update check included |
| XDG_CACHE_HOME | Where the update check keeps its cache, instead of a dotfile in $HOME |
| npm_config_registry | The registry translato update asks for the latest version (npm sets it for you) |
Update Notifications
The CLI checks npm for newer versions once every 24 hours and displays a warning if an update is available. To disable (e.g. in CI), set TRANSLATO_NO_UPDATE_CHECK=1 or the conventional NO_UPDATE_NOTIFIER. The check is also skipped when CI is set, under --json or --quiet, and when the CLI cannot read its own version.
Upgrading a namespaced project
Versions before this one sent a namespace twice — once in the term and once as a
context the API folds into the key — so a project with namespaces: has its keys
stored as namespace:app.app:title rather than app:title. Those keys hold
every translation done so far.
A push from the new CLI writes the correct names and then reports the old ones
as orphaned. Do not prune them: that deletes the translations. Run this once
per project instead:
translato migrate namespace-keys --dry-run # what would move
translato migrate namespace-keys # move it
translato pull # bring the files up to dateIt creates the new keys, copies every language's translations onto them, and only
then removes the old keys — so a run that stops part-way leaves the originals
untouched. translato doctor reports the same thing if you are not sure whether
a project is affected.
Rate limits
The API limits by API token, tiered by how expensive an endpoint is. The CLI
paces itself against the headers every response carries
(X-RateLimit-Remaining, X-RateLimit-Reset) and honours Retry-After on a
429, so a large push waits for the window instead of failing in the middle of
it.
| Endpoints | Limit |
|---|---|
| Reads — projects/*, languages/list, terms/list, exports | 120 / minute |
| Writes — terms/add, terms/delete | 30 / minute |
| Bulk — translations/add, projects/upload, projects/sync | 60 / minute |
| Any one token, across everything | 300 / minute |
A push is a handful of requests whatever the project size: batches are sized by
body bytes, not by a fixed count, so 20 000 keys is two bulk requests rather
than thirty. A pull is one request per language.
If you run several CI jobs against the same token at once, they share these buckets — the limits are per token, not per machine, so a shared runner IP does not throttle anyone else.
Proxy Support
The CLI respects HTTP_PROXY, HTTPS_PROXY, and NO_PROXY.
HTTPS_PROXY=http://proxy.corp:8080 translato pullThe proxy agent is undici, an optional dependency, because it requires
Node 18.17 or newer while the CLI itself runs on Node 18.0. It installs by
default; installs on Node 18.0–18.16, and installs run with --ignore-optional,
skip it. Nothing else in the CLI uses it, so the only thing missing is the
proxy — and if a proxy variable is set without it, the CLI says so and tells you
to npm install undici instead of quietly sending the request direct.
What the CLI will not do quietly
- A download URL from the server is only followed on the API host. The export endpoint may hand back a URL instead of the strings; one pointing anywhere else — a CDN, a bucket, a cloud metadata address — is refused rather than fetched and written to disk.
- Hooks inherit the whole environment,
TRANSLATO_API_KEYincluded, and run only withTRANSLATO_ALLOW_HOOKS=1. Under--jsontheir output is relayed to stderr so the payload on stdout stays parseable. - A credential passed as a flag is saved.
--api-key,--projectand--hostare written to.envonce the command succeeds, normalized; pass--no-savefor a one-off run. - Large pushes are split by size, not by count. Each request stays under 2 MB (the API allows 8), and the number of requests stays small — the bulk endpoints allow ten a minute, so a fixed count of 1 000 translations per request could not finish a 20 000-key project inside the window. A push that still fails part-way says which batch stopped it, and both uploads replace what they send, so running it again is safe.
- A
--pruneasks first. Deleting remote keys also deletes their translations in every language, so at a terminal it is confirmed. Under--json, with--yes, or with no TTY it proceeds. - A configured hook that cannot run stops the command rather than letting it
do something other than what
.translato.ymldescribes. - A pull keeps the types and shapes the file already had. A number stays a
number, a boolean stays a boolean, and an object keyed
"0","1"stays an object — read from the file being replaced, because guessing from the string would turn a genuine"5"into a number. - Every file skipped is a refusal, not a success. A glob whose every match is a target-language file exits 2 and names them, instead of reporting "No new keys to push".
- A write is not retried on a server error. A 5xx from
terms/add,terms/deleteortranslations/addmay have been applied, so only transport failures and an explicit 429 are retried.
Files to .gitignore
Add these to your .gitignore (automatically added by translato init):
.env
.env.lock
.translato.state
.translato.lockCI/CD
GitHub Actions
name: Translation Sync
on:
push:
paths: ['src/locales/en.json']
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npx translato-cli push
env:
TRANSLATO_API_KEY: ${{ secrets.TRANSLATO_API_KEY }}
TRANSLATO_PROJECT: ${{ secrets.TRANSLATO_PROJECT }}GitLab CI
translate:
script:
- npx translato-cli push
variables:
TRANSLATO_API_KEY: $TRANSLATO_API_KEY
TRANSLATO_PROJECT: $TRANSLATO_PROJECTPR Validation
Catch translation problems before merge. All three exit non-zero on a finding, so each one gates on its own question:
- run: npx translato-cli validate --json # missing keys, empty values, placeholders
- run: npx translato-cli diff --exit-code # local files vs the TMS
- run: npx translato-cli pull --check # would a pull change a file?A hook configured in .translato.yml stops a CI run unless the job sets
TRANSLATO_ALLOW_HOOKS=1 (to run it) or passes --no-hooks (to run without it).
Troubleshooting
Output path fallback
When pulling translations, if the CLI cannot determine an output path from a {lang} placeholder, wildcard, or source language directory in your file glob, it falls back to inserting the language code before the file extension. For example, messages.json becomes messages.de.json. To avoid this fallback, use a {lang} placeholder in your glob (e.g. locales/{lang}.json) or structure files with a source language directory (e.g. locales/en/messages.json). Run with --verbose to see when the fallback is used.
"click to translate" after push
If the TMS web UI shows "click to translate" instead of your source text, run translato push again. The CLI pushes both terms and source language translations. Check the output for the Translations (en) line — added or updated should be non-zero.
Pull skips a language
If pull shows ⏭ xx — no translations, skipping, it means no translation records exist on the server for that language. Push first (translato push), then translate in the TMS, then pull.
Missing API key or project ID
❌ Missing API key. Use --api-key or set TRANSLATO_API_KEY.Set credentials with translato login (recommended), or pass --api-key and --project flags (saved to .env automatically), or add them manually to .env in your project root.
"No changes since last push"
The CLI tracks a SHA-256 hash of your source translations. If nothing changed, push is skipped to avoid unnecessary API calls. Use --force to push anyway.
"Cannot write .translato.lock"
The project directory is read-only, or owned by another user. The lock and the
state file live there, so a writable project directory is required even when
--out-dir points somewhere else. This used to be reported as "another translato
process is running — delete .translato.lock", which named neither the cause nor a
file that existed.
"None of the files matching … holds the source language"
Every file the glob matched reads as a target language, so there was nothing to
push. Usually the glob points one level too high or too low
(src/locales/*.json against a directory-per-language layout), paths.source
does not match the files, or the files are namespaces whose names look like
locales (app.json) and are not listed in namespaces:.
Another process is running
If you see a warning about another translato process, it means a .translato.lock file exists from a previous run. The CLI checks if the PID is still alive — stale locks from crashed processes are cleaned up automatically.
Development
# Run in dev mode (without building)
npm run dev -- push
# Run tests
npm test
# Build for distribution
npm run buildLicense
MIT
