Skip to content

Guides

Work with translators

Your catalogs are plain JSON, with nothing proprietary to convert. That gives you three ways to get translations done: hand the files to a TMS, round-trip translator files with export/import, or machine-translate with translate.

Plain JSON, no lock-in

Most TMS platforms (Crowdin, Lokalise, Phrase and friends) ingest JSON catalogs natively. Point the platform at locales/ and you're done: no export step, no custom format. Translations come back as the same files, and every change reviews like a normal diff.

Export for humans

When there's no TMS (a freelance translator, an agency, a coworker with a spreadsheet), verbaly export writes one translator-ready file per locale, with the source text next to each translation. If you keep your catalog in groups, it exports one row per message, named by its full path, so a translator sees text and never a group.

npx verbaly export                   # verbaly-export/es.xlf, pt.xlf (XLIFF 2.0)
npx verbaly export --format csv      # key,source,target,location (any spreadsheet)
npx verbaly export --format po       # es.po, pt.po (gettext, any PO editor)
npx verbaly export --missing         # only what's untranslated

XLIFF 2.0 is the industry interchange format, so TMS and CAT tools open it directly. Your placeholders and tags travel in it as protected chips with meaningful names, so a translator cannot break them by accident; plural and select words stay editable, because those do need translating. Gettext PO works with Poedit and any PO tool, and CSV is for everyone else, editable anywhere. Every format says where each text lives in your source, so the translator sees the context instead of guessing it.

Before exporting, verbaly status shows how much is left per language: es: 45/48 translated (94%).

Import back, validated

verbaly import reads the translated files (XLIFF 2.0 or 1.2, CSV or gettext PO) and fills your catalogs. Every entry passes the same structural validation as machine translation: placeholders, plural variants and tags must survive verbatim. Anything else is rejected and reported, never written.

npx verbaly import verbaly-export/es.xlf
#   es: +42 imported
#   es: 1 rejected (params/tags not preserved): home.greeting
npx verbaly import es.csv --dry-run    # preview without writing
npx verbaly import es.xlf --overwrite  # replace existing translations
npx verbaly import es.xlf --draft      # keep it as drafts until someone reads it

Existing translations are kept unless you pass --overwrite; unknown keys are ignored and reported. Entries a PO tool marks as fuzzy count as untranslated. The target locale comes from the file itself (or the filename), and --locale overrides both. It is read as one of your languages, so gettext's pt_BR fills your pt-BR. A new language starts its catalog under its proper code, unless your config lists its locales: then a language outside that list is skipped with a warning and exit code 1, since your app would never load it. Pass --draft when nobody has read the file yet: what it brings stays marked as drafts until someone approves it.

The safety net, in plain words

Editing locales/es.json by hand is a perfectly good way to translate, and it gets the same protection as an imported file. verbaly check reads every translation next to its source and fails when the translation cannot say the same thing, and every build lists it. You do not have to remember these rules: if you break one, the error tells you which and where.

When someone changes a source text, its translations are not kept as if nothing had happened. Verbaly records which source text a translation was written for the moment it is written, by a provider, an agent, Studio or an imported file, and flags it as outdated once that text changes, until it is updated, or kept with verbaly review --approve when it still says the right thing. A translator's file brings the source text it was made from, so a file translated from an older version reads as outdated as soon as it lands. Editing a translation clears the flag on its own.

npx verbaly status is the quick look: how much is translated per language, how much is waiting for review, how much is outdated and how much is broken.

Export for mobile apps

The same catalogs can ship to a companion mobile app as native resources: android-xml writes strings.xml folders you can drop into res/, and ios-strings writes .lproj folders for Xcode.

npx verbaly export --format android-xml   # verbaly-export/values-es/strings.xml, values-pt-rBR/…
npx verbaly export --format ios-strings   # verbaly-export/es.lproj/Localizable.strings, …

Your source language becomes the platform default (values/strings.xml, en.lproj), and untranslated keys are left out so the app falls back to it instead of showing empty text. Keys are adapted to valid Android resource names, values keep their parameter syntax untouched, and the flow is one-way: translations go from your catalogs to the app.

No humans available yet?

Machine translation fills the gaps with the same structural guarantees, and everything a provider or a person gets wrong is caught by verbaly check in CI.

Machine output arrives marked as drafts: translate records what it wrote in verbaly.lock, next to your config. Like a package lockfile, Verbaly writes it and you commit it with your catalogs, and nobody edits it by hand, so nobody confuses machine output with reviewed work. If a merge leaves it with a conflict, Verbaly keeps the drafts of both sides and the next extract writes it whole: commit it with the merge. Deleting it would mark every draft as reviewed.

npx verbaly translate            # es: +12 translated (draft)
npx verbaly review               # list what's waiting for a human
npx verbaly review --approve     # accept after reading
npx verbaly check --drafts       # CI gate: fail while drafts remain

Before the first run, write down what must not be translated. Product names, feature names and any wording you have already settled on go in translate.glossary, and how you address the reader goes in translate.instructions. That is cheaper than fixing the same word by hand after every run.

A long run survives a bad connection. Requests that fail for a passing reason are tried again, and if one still does not come back, everything else is saved and the report names the messages it could not fill. Running translate again asks only for those, so an interrupted run never costs you the whole thing twice. It never costs a person their work either: a message someone saved while the run was out keeps its text, and the report says how many it left alone.

verbaly review lists what is waiting, machine drafts and translations written for an older source text, each with the text that ships next to its translation. verbaly review --approve accepts it after reading, followed by keys when you want to accept only those, and a translator file brought in with import counts as review on its own. In CI, verbaly check --drafts keeps failing while unreviewed drafts remain, and --outdated while an outdated translation does.

A coding agent can run this same loop through the MCP server: whatever it translates still arrives as a draft behind the same review gate, the translations it writes itself included.

↑↓to moveEnterto open
Copied to clipboard