Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: Configuration and time

Configuration and time

Exchanging translations with a service

On this page

Most teams send their wording to a translation service, such as Crowdin, Lokalise or any that reads XLIFF or JSON. jadpo translations export [<locale>] writes the files to upload, and jadpo translations import <file> reads the translations back, both with --json. They exchange every message, enum with wording, failure and email subject the default locale words, Jadpo's own messages (those that differ by language), the /// description of each, and each locale's wording.

Export

export writes one file for each translated locale (a supported locale other than the default and the pseudo-locale), or for the one named, to build/translations/<locale>.xlf. The file is the same each time, and the next export overwrites it. --format json writes <locale>.json instead, and --out <folder> writes to another folder, relative to the project and inside it. Naming the default locale writes the source file to upload: the same units with no translations.

A unit is one wording: a plain message, a value of the enum that picks a wording, or a plural form of the locale. Its id is <domain>.<message>, with .<value> or .<form> after it, which XLIFF splits into <file id> and <unit id>. In place of a domain, shared stands for shared/translations/, jadpo for Jadpo's own messages, and a library package's folder name for its text. A domain named jadpo would share ids with Jadpo's own messages, so export refuses it. Wording every locale shares (TextDirection's) isn't exchanged.

The wording is written as it is in the language file, {field} placeholders and doubled braces included, not as XLIFF inline <ph> codes. So the same text goes to both formats, comes back exactly, and a service sees ordinary text (both services recognise {x} as a placeholder). A plural form the default locale has no form of is translated from the default's other.

text
Wrote build/translations/de_de.xlf    de_de  5 units, 5 translatedWrote build/translations/pl_pl.xlf    pl_pl  7 units, 7 to translateExported 2 locales: 12 units, xliffnext: jadpo translations import build/translations/pl_pl.xlf  reads the translations back

The file formats

The XLIFF is version 2.0 (namespace urn:oasis:names:tc:xliff:document:2.0), which every tool that reads 2.x reads. It has a <file> for each domain and a <unit> for each wording. Each unit has description, context (the enum value or plural form it is for) and placeholders notes, and a stale note for wording translated from default wording that has since changed. Its <segment> has the state initial without a translation, translated with one, and reviewed when a person accepted it.

The JSON is { "<id>": { "message": ..., "description": ... } }, the shape Chrome extensions use for messages.json. It has a place for a description that Crowdin imports as it is ("Chrome JSON": message and description fields). message is empty until translated. A flat { id: text } object is the shape both services take, but it carries no description, which the exchange exists to send. Whether Lokalise reads XLIFF 2.0, and imports the Chrome shape, hasn't been checked; each format is the other's fallback.

text
<unit id="parcels.one">  <notes>    <note category="description">How many parcels an order has, on the tracking page.</note>    <note category="context">Used for the plural form `one` of `count`.</note>    <note category="placeholders">{count}</note>  </notes>  <segment state="translated">    <source xml:space="preserve">{count} parcel</source>    <target xml:space="preserve">{count} Paket</target>  </segment></unit>

Import

import tells a file's format by what it starts with: < for XLIFF 2.0 or 2.1, { for JSON. Plain strings are accepted for message, so a flat object from a service that can't keep descriptions reads too. The locale is the one the file names (XLIFF's trgLang), --locale, or a JSON file's name (pl_pl.json). XLIFF 1.2 is refused, with a message saying to ask the service for 2.x, since the two are built differently. Inline codes a service adds on the way back (<ph>, <pc>) are read as the text they stand for. A unit with no translation is left alone. A status in the file (reviewed) is never read back: only a person accepts a translation, with jadpo translations accept.

Import then does what a person editing the language files would have to:

  • each changed wording goes into its language file where its member is, keeping the file's other members, comments and order, or at the end;
  • a missing file is created;
  • the files are laid out by jadpo fmt;
  • the folder's sources.jadpo_record gets a line for each changed translation, made from today's default wording and not accepted. Only changed translations get one, so a round trip never creates or changes a line.

Before anything is written, the project is checked as jadpo check checks it, by the checker's own rules with nothing special for the exchange. Any new finding refuses the whole import, with that finding's own rule, pointing at the unit of the imported file that caused it: translation.placeholder_unknown, translation.wording_invalid, translation.variant_missing for a plural form or enum value left out, or the declinable translation.field_unused for a placeholder every wording dropped.

Two errors belong to the exchange itself:

  • A unit naming nothing the project words, or a plural form the locale doesn't use, is translation.exchange_unit_unknown, with the nearest id.
  • A file that isn't XLIFF 2.x or JSON, isn't well formed, holds a unit twice or in several segments, or was translated from another default language is translation.exchange_file_invalid.

Both are command errors, so the command exits 1: it ran, and the file was wrong. All the reasons are listed at once, and nothing is written. An in-progress locale may be translated in part, as its language file may. Exporting and importing with nothing changed writes nothing, and says Already up to date.

text
Wrote domains/shipping/translations/pl_pl.jadpo    farewell              added    order_shipped.formal  added    order_shipped.casual  added    parcels.one           added    parcels.few           added    parcels.many          added    parcels.other         addedWrote domains/shipping/translations/sources.jadpo_recordImported pl_pl: 7 addednext: jadpo check  confirms the imported wording

Wording that changed while it was being translated

XLIFF carries what each translation was made from. So a unit whose <source> isn't today's default wording, because the English changed while the service worked, is written all the same and recorded as made from that other wording. It is stale, counted as stale in the summary, and jadpo check fails with translation.stale until it is translated again, as for wording changed by hand. This is the one case where import doesn't refuse, because the work isn't lost and translation.stale already says what to do. JSON doesn't carry its source, so its translations are taken as made from today's wording.

Your app's own data, such as product names or articles, isn't a translation: model translated content as ordinary fields. Library packages' translations are in Jadpo's and packages' text.

Markdown for agents: /docs/exchanging-translations.md.Built from Jadpo main at a707efb.