Formats

Updated: 4 weeks ago · 6 min read

The CLI translates eighteen file formats. The format is inferred from the file extension; set format on a files[] entry to override it — or for the three formats that always need it (yaml-openapi, yaml-root-key, android).

FormatExtensionsformat valueNotes
JSON.jsonjsonKey/value. Supports key controls.
JSONC.jsoncjsoncJSON with comments. Comments survive and double as translator notes.
YAML.yaml, .ymlyamlGeneric YAML. Every string value is translated; keys and structure preserved.
OpenAPI YAML.yaml, .ymlyaml-openapiOpenAPI specs. Set format explicitly — plain .yaml auto-detects as yaml.
Locale-root-key YAML.yaml, .ymlyaml-root-keyLocale is the YAML root key (Rails config/locales). The root key is rewritten to the target locale. Set format explicitly.
Markdown.mdmdProse translated; frontmatter opt-in.
MDX.mdxmdxMarkdown + JSX. Component props opt-in.
Markdoc.mdocmarkdocMarkdown + tags. Frontmatter + tag attributes.
TypeScript.ts, .mts, .ctstypescriptLocale modules (export default { … }). String literals translated; code preserved.
Gettext PO.popomsgstr translated; msgid, comments, and headers preserved.
Flutter ARB.arbflutterString values translated; @-metadata and {placeholders} preserved.
Android.xmlandroidstrings.xml. Set format explicitly — .xml isn't auto-detected.
Xcode strings.stringsxcode-stringsValues translated; keys preserved.
Xcode String Catalog.xcstringsxcode-xcstringsOne file holds every locale — targets are written back into the same file (see below).
Xcode stringsdict.stringsdictxcode-stringsdictPlural strings translated; format control keys preserved.
XLIFF.xlf, .xliffxliff<source> kept, <target> written per unit. Versions 1.2 and 2.0.
SubRip.srtsrtSubtitle text translated; indices and timecodes untouched.
PHP.phpphpLaravel return [ … ]. String values translated; keys, numbers, and structure preserved.

See it end to end

The demo project is a small sandbox covering the document and data formats — JSON, JSONC, Markdown, MDX, Markdoc, and OpenAPI YAML — each with the exact config it needs. Clone it with npx degit lingodotdev/lingo.dev/demo/new-cli my-lingo-demo and run a push. The app and framework formats are not in it — for those, Example projects has a full repository per framework, and the per-format snippets below cover the config.

bash
npx degit lingodotdev/lingo.dev/demo/new-cli my-lingo-demo
cd my-lingo-demo

JSON and JSONC#

Plain key/value translation. Every string value is translated unless a key control says otherwise.

json
{ "pattern": "content/en/app.json", "lockedKeys": ["meta.version"] }

JSONC additionally preserves comments, which the engine reads as context — see translator notes.

json
{ "pattern": "content/en/settings.jsonc", "preservedKeys": ["featureFlags"] }

Markdown, MDX, and Markdoc#

Body prose is translated by default. Frontmatter and embedded components are not translated unless you opt in.

Frontmatter#

List the frontmatter fields to translate with translateFrontmatterFields:

json
{
  "pattern": "content/en/guide.md",
  "translateFrontmatterFields": ["title", "description"]
}

MDX component props#

For MDX, translate specific props on specific components with translateComponentProps:

json
{
  "pattern": "content/en/landing.mdx",
  "translateFrontmatterFields": ["title"],
  "translateComponentProps": [{ "component": ["Hero", "Callout"], "props": ["title", "body"] }]
}

This translates the title and body props on <Hero> and <Callout> and leaves all other props untouched.

Markdoc#

Markdoc works like Markdown, with frontmatter and tag attributes preserved:

json
{
  "pattern": "content/en/changelog.mdoc",
  "translateFrontmatterFields": ["title"]
}

YAML#

Generic YAML is auto-detected from .yaml/.yml — every string value is translated, keys and structure preserved:

json
{ "pattern": "content/en/strings.yaml" }

OpenAPI specs are a special case: they share the .yaml extension but need an explicit format so the engine translates only human-facing fields (summaries, descriptions) and leaves schema keys, paths, and operation IDs intact:

json
{ "pattern": "content/en/api.yaml", "format": "yaml-openapi" }

Rails-style locale files are the other special case: the locale is the YAML root key (en:) and the target file must be rooted at the target locale (es:). Set format explicitly for those too:

json
{ "pattern": "config/locales/en.yml", "format": "yaml-root-key" }

App and framework formats#

PO, Flutter ARB, Xcode .strings, Xcode .stringsdict, TypeScript locale modules, XLIFF, SubRip, and PHP all follow the same rule: translatable text is translated, everything structural is preserved (keys, IDs, metadata, placeholders, timecodes, code, formatting). Each is auto-detected from its extension — just point a pattern at the source file:

json
{ "pattern": "lib/l10n/app_en.arb" }
{ "pattern": "locales/en.po" }
{ "pattern": "Localizable.strings" }
{ "pattern": "l10n/en.xlf" }

One needs an explicit format because its extension is too ambiguous to auto-detect:

json
{ "pattern": "res/values/strings.xml", "format": "android" }

Xcode String Catalogs#

A .xcstrings (String Catalog) file holds all locales in a single file. There's no per-locale output path — the CLI reads the source locale and writes every target back into the same file:

json
{ "pattern": "Localizable.xcstrings" }

Strings flagged "shouldTranslate": false in the catalog are skipped — the CLI leaves them untranslated and untouched.

Which formats accept a key scope#

lingo push --key re-translates named keys and nothing else. It needs keys that are stable names and a file that can simply not contain a key, which rules out the document formats and the plural dictionaries:

Key scopes workKey scopes are refused
json, jsonc, yaml, yaml-root-key, po, flutter, android, xcode-strings, xcode-xcstrings, xliff, php, typescriptmd, mdx, markdoc, html, srt, yaml-openapi, xcode-stringsdict

For md, mdx, markdoc, html, srt and yaml-openapi a unit is addressed by its position in the document — a section ordinal, a node path, a subtitle cue number — so its key shifts as soon as anything above it is edited, and a scope aimed at one would select the wrong string or none. xcode-stringsdict is refused for the opposite reason: its keys are plural categories that the file needs in order to stay a valid plural dictionary, so they can never be left out.

lingo push drops refused files from a key-scoped run with a warning rather than failing it, so a push may still mix them with key-value files; push them without --key.

Positional members inside the supported formats behave the same way and keep their source text under a scope: array elements, Android <string-array> items, and <plurals> quantities.

Output paths#

The pattern names your source file; every target path is derived from it. Four rules apply, in order:

  1. A whole path segment or filename that is the locale — the usual case. content/en/app.jsoncontent/de/app.json, locales/en.jsonlocales/de.json.
  2. A locale at the tail of a segment or filename, for the layouts that spell it that way — Android, Xcode .strings/.stringsdict, Flutter, and yaml-root-key. res/values-en/res/values-de/, app_en.arbapp_de.arb, devise.en.ymldevise.de.yml.
  3. A platform's reserved name for the default language, with no locale in the path at all. Android's bare res/values/ becomes res/values-de/, and Xcode's Base.lproj becomes de.lproj.
  4. The String Catalog, where the target path is the source path, because one file holds every locale.

Android is the one platform whose target paths are not BCP 47: the CLI writes the resource qualifier Android actually reads, so pt-BR lands in values-pt-rBR/ and zh-Hans in values-b+zh+Hans/.

If none of the rules match — the source locale appears nowhere in the path — the CLI falls back to inventing a <locale>/ directory next to the file, and lingo push tells you it guessed. Treat that warning as a sign the pattern is wrong. See Configuration.

Coming from the legacy CLI?#

Most of the legacy CLI's formats have landed in the current CLI (PO, XLIFF, Android/Xcode strings, Flutter ARB, and more — see the table above). A few less-common formats (e.g. CSV, HTML, MJML, .properties) aren't supported yet; until yours lands, the legacy CLI docs cover it.