Skip to content

Guides

Work with translators

Your catalogs are plain flat 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 flat JSON 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, and your placeholders and tags travel in it as protected chips with meaningful names, so a translator can't break them by accident (plural and select words stay editable: those need translating). Gettext PO works with Poedit and any PO tool. 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

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.

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 stops the build when the translation cannot say the same thing. You do not have to remember these rules: if you break one, the error tells you which and where.

npx verbaly status is the quick look: how much is translated per language, how much is waiting for review 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 locales/.verbaly-drafts.json, committed with your catalogs and never edited by hand, so nobody confuses it with reviewed work.

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.

verbaly review lists what is waiting, verbaly review --approve accepts it after reading, 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.

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.

Copied to clipboard