GlotFox translation file format
Version 1. This is the on-disk format shared by the CLI, the API and Git sync. The extractor and the type generator are built from this spec.
Key words MUST, SHOULD and MAY are used as in RFC 2119.
1. Directory layout
glotfox.config.json
locales/
en/ <- source locale
common.json
common.meta.json <- optional descriptions (source locale only)
checkout.json
de/
common.json
checkout.json
pt-BR/
common.json
- One directory per locale:
locales/<locale>/. One file per namespace:<namespace>.json. <locale>is a BCP 47 tag, case as written in the config (en,de,pt-BR,zh-Hant).<namespace>matches[a-z0-9][a-z0-9_-]*. The default namespace iscommon.- The root (
locales/) is configurable viaoutput.dir(section 6); the layout can be changed withoutput.pattern. - Files are UTF-8, LF line endings, no BOM, 2-space indent, trailing newline.
- Keys MUST be sorted alphabetically (by Unicode code point) when written by GlotFox tools, to keep Git diffs stable.
- The source locale is the locale keys are authored in. It is the source of truth for the set of keys: target locales may lack keys (untranslated), but a key that is absent from the source locale is orphaned and reported by the CLI.
2. Namespace file
A namespace file is a single flat JSON object. Each member maps a key to an ICU MessageFormat string.
{
"cart.items": "{count, plural, one {# item} other {# items}}",
"cart.title": "Your cart",
"greeting": "Hello, {name}!"
}
- The top-level value MUST be an object; values MUST be strings. Nested objects, arrays, numbers and
nullare invalid. - Duplicate keys are invalid.
- In target locales, an empty string means "present but not yet translated"; in the source locale it is an error.
- A key's full identity is
<namespace>:<key>(e.g.checkout:cart.title). In code, the namespace may be omitted forcommon.
2.1 Key naming rules
- Keys are dot-separated segments:
segment(.segment)*. - A segment matches
[A-Za-z][A-Za-z0-9_-]*(must start with a letter). - Recommended: lowerCamelCase or kebab-case segments, grouped by feature or screen:
settings.profile.saveButton. - Max key length is 128 characters; max 8 segments.
- A key MUST NOT be both a leaf and a prefix of another key (
cartandcart.titlecannot coexist). - The dots are purely a naming convention: files are flat, so there is no unflattening ambiguity. Tools MAY group by prefix for display and for generated types.
- Keys are case-sensitive.
- Full regex:
^[A-Za-z][A-Za-z0-9_-]*(\.[A-Za-z][A-Za-z0-9_-]*){0,7}$.
3. Values: ICU MessageFormat
Values use ICU MessageFormat. Supported constructs:
3.1 Plain text
{ "welcome": "Welcome back" }
3.2 Placeholders (arguments)
{ "greeting": "Hello, {name}!" }
Argument names match [A-Za-z_][A-Za-z0-9_]*. Positional (numeric) arguments are discouraged.
3.3 Typed arguments
{
"invoice.total": "Total: {amount, number, currency}",
"invoice.due": "Due {date, date, medium}",
"event.time": "Starts at {time, time, short}"
}
Supported types: number (styles integer, percent, currency, or a skeleton ::...), date and time (styles short, medium, long, full, or a skeleton ::...).
3.4 Plural
{
"cart.items": "{count, plural, =0 {Your cart is empty} one {# item} other {# items}}"
}
- Categories are the CLDR categories
zero,one,two,few,many,other, plus exact matches=N. otheris REQUIRED. Each target locale MUST supply the categories its language requires (e.g. Russian:one,few,many,other).#is the number, formatted for the locale.offset:Nis supported.- Ordinals use
selectordinalwith the same syntax.
3.5 Select
{
"invite": "{gender, select, female {She invited you} male {He invited you} other {They invited you}}"
}
other is REQUIRED.
3.6 Nesting
Plural and select may be nested:
{
"share": "{gender, select, other {{count, plural, one {They shared # photo} other {They shared # photos}}}}"
}
3.7 Escaping
- A literal
{or}is written inside single quotes:'{','}'. - A literal apostrophe is
''. A lone'not before{,}, or#(inside a plural) is literal. - Example:
"It''s '{'not'}' a placeholder"rendersIt's {not} a placeholder.
3.8 Rich text tags (optional)
Simple tags without attributes are allowed: "Read the <link>docs</link>". The runtime maps the tag name to a component or function.
3.9 Validation and types
The type generator derives, from each source message, the set of arguments and their types:
| ICU construct | Generated TypeScript type |
|---|---|
{x} | string | number |
{x, number...}, plural, selectordinal | number |
{x, date...}, {x, time...} | Date | number |
{x, select, a {..} b {..} other {..}} | 'a' | 'b' | (string & {}) |
<tag>..</tag> | (chunks) => ReactNode (or framework equivalent) |
A target-locale message is valid when it parses, uses the same argument names as the source (it MAY omit an argument but MUST NOT introduce one), has compatible argument types, and has all required plural/select branches (other).
4. Descriptions and context
Translators and the AI need context. It is stored outside the translation file so translation files remain plain key -> string maps (simple to load at runtime and to diff).
locales/<source>/<namespace>.meta.json:
{
"cart.items": {
"description": "Shown under the cart icon. `count` is the number of line items.",
"maxLength": 40,
"screenshot": "docs/screens/cart.png"
},
"greeting": { "description": "Header greeting on the dashboard." }
}
- Only the source locale directory has
.meta.jsonfiles; they are ignored in other locales. - Members:
description(string, SHOULD be set),maxLength(integer, optional),screenshot(relative path or URL, optional). Unknown members are preserved. - Keys in the meta file MUST exist in the source namespace file; otherwise they are orphaned and reported.
- The extractor writes
descriptionfrom a code comment or option next to the call when present (e.g.t('cart.title', { description: '...' })) and never overwrites a manually edited description unless--forceis given. - The API stores description/context as a field on each key and reads/writes this file during Git sync.
5. Extraction
The extractor scans files matched by include/exclude (section 6) for translation calls and writes keys and source strings into the source locale's namespace files. It also adds new keys, reports keys that are no longer used (removed only with --prune), and creates missing target-locale files (with empty values or omitting keys, per output.emptyValues, default omit).
6. Config file: glotfox.config.json
Located at the project root (the nearest ancestor of the working directory containing it). JSON, UTF-8.
{
"$schema": "https://glotfox.app.dukauskas.lt/schema/config.v1.json",
"version": 1,
"sourceLocale": "en",
"targetLocales": ["de", "fr", "pt-BR"],
"defaultNamespace": "common",
"include": ["src/**/*.{ts,tsx,js,jsx,vue,svelte}"],
"exclude": ["**/*.test.*", "**/node_modules/**"],
"output": {
"dir": "locales",
"pattern": "{locale}/{namespace}.json",
"format": "json",
"emptyValues": "omit",
"types": "src/glotfox.d.ts"
}
}
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
$schema | string | no | — | Editor hint only. |
version | integer | no | 1 | Config schema version. |
sourceLocale | string | yes | — | BCP 47 tag. |
targetLocales | string[] | yes | — | May be empty. Must not contain sourceLocale or duplicates. |
defaultNamespace | string | no | "common" | Namespace for keys without a prefix. |
include | string[] | no | ["src/**/*"] | Globs, relative to the config file, for the extractor. |
exclude | string[] | no | ["**/node_modules/**"] | Applied after include. |
output.dir | string | no | "locales" | Root of locale directories. |
output.pattern | string | no | "{locale}/{namespace}.json" | Must contain {locale} and {namespace}. |
output.format | string | no | "json" | "json", "po" or "yaml" (see section 7). Set a matching output.pattern; its default follows the format (.json, .po, .yaml). |
output.emptyValues | "omit" | "empty" | no | "omit" | How untranslated keys are written in target locales. |
output.types | string | null | no | null | Path of the generated TypeScript declaration file; null disables it. |
- Globs use standard syntax (
*,**,?,{a,b}), with/separators on all platforms. - Unknown fields are an error, to catch typos (except
$schema). - Relative paths are resolved from the directory containing the config file.
7. Other formats: PO and YAML
Select a format with output.format (and a matching output.pattern, e.g. {locale}/{namespace}.po). All formats hold the same model (key, ICU message); glotfox extract, check, push, pull and GitHub sync/import work with each. Descriptions always stay in the JSON file <namespace>.meta.json. Files are written with sorted keys and a trailing newline, so parse then serialise gives identical output.
7.1 PO (gettext)
Each key is one entry: msgctxt is the key, msgid repeats it and msgstr is the ICU message. Entries without msgctxt use msgid as the key. Comments, flags and the header are ignored when reading. Gettext plural entries (msgid_plural, msgstr[n]) are read as an ICU {count, plural, …} message (categories chosen by nplurals; %d becomes #); they are written back as the ICU message in msgstr.
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Language: en\n"
msgctxt "cart.items"
msgid "cart.items"
msgstr "{count, plural, one {# item} other {# items}}"
7.2 YAML
Dotted keys become nested maps (cart.items is items under cart), values are double-quoted strings. Reading accepts plain, double- and single-quoted strings and comments; lists, anchors, flow collections and block scalars are rejected. A key cannot also be a prefix of another key.
cart:
items: "{count, plural, one {# item} other {# items}}"
8. Versioning
This is format version 1. Breaking changes bump version in the config; tools MUST refuse a config whose version they don't know.