Formats
Custom formats
Store messages in any file format by defining a codec that reads and writes your files.
The built-in formats are po and json. When your messages live in a different file format, a codec teaches the CLI how to read and write your files:
.eloqnt/config.ts
import {defineConfig} from '@eloqnt/cli';export default defineConfig({messages: {format: {codec: './.eloqnt/StructuredJSONCodec.ts',extension: '.json'}// ...}});
The codec can live at any path in your project. It can also come from an installed package like @eloqnt/format-apple-xcstrings, in which case you reference it by its package name.
One file per locale
Most formats store one file per locale (e.g. en.json, es.json, etc.).
A codec for them implements decode and encode, created with defineCodec:
.eloqnt/StructuredJSONCodec.ts
import {defineCodec} from '@eloqnt/cli';type Entry = {message: string; description?: Array<string>};export default defineCodec(() => ({decode(content) {const data = JSON.parse(content) as Record<string, Entry>;return Object.entries(data).map(([id, entry]) => ({id,message: entry.message,description: entry.description ?? [],references: []}));},encode(messages) {const data: Record<string, Entry> = {};for (const message of messages) {data[message.id] = {message: message.message,description: message.description};}return JSON.stringify(data, null, 2);}}));
This example reads a JSON format that stores message descriptions next to each value:
messages/en.json
{"HomePage.title": {"message": "Welcome, {name}!","description": ["Greeting for the user when the app starts."]}}
A similar codec can be used to implement other formats, like:
- Java properties files
- Laravel JSON messages
All locales in one file
Some formats keep every locale in a single shared file, like Apple's .xcstrings.
A codec declares this with file: 'shared' and implements decodeAll and encodeAll in place of decode and encode:
.eloqnt/SharedJSONCodec.ts
import {defineCodec} from '@eloqnt/cli';type Messages = Record<string, string>;export default defineCodec(() => {// Keeps the parsed file across decode and encode,// so unknown fields survive a write untouchedlet parsed: Record<string, Messages> = {};return {// This codec writes all locale-specific// translations to a single filefile: 'shared',decodeAll(content) {parsed = JSON.parse(content) as Record<string, Messages>;return Object.fromEntries(Object.entries(parsed).map(([locale, messages]) => [locale,Object.entries(messages).map(([id, message]) => ({id,message,description: [],references: []}))]));},encodeAll(messagesByLocale) {const data = {...parsed};for (const [locale, messages] of Object.entries(messagesByLocale)) {data[locale] = Object.fromEntries(messages.map((message) => [message.id, message.message]));}return JSON.stringify(data, null, 2);}};});
This example reads a JSON file that nests messages under a locale key:
messages.json
{"en": {"HomePage.title": "Welcome!"},"de": {"HomePage.title": "Willkommen!"}}
With a shared file, messages.path points at the file itself and contains no {locale} placeholder, while the format continues to provide the extension:
.eloqnt/config.ts
import {defineConfig} from '@eloqnt/cli';export default defineConfig({messages: {path: './messages',locales: 'infer',sourceLocale: 'en',format: {codec: './.eloqnt/SharedJSONCodec.ts',extension: '.json'}}});
A few things to know about shared-file codecs:
- The CLI creates one codec instance per file and calls
decodeAllbeforeencodeAllwhenever the file exists on disk. State you keep in the closure (likeparsedabove) is how other content in the file survives a write. - When the file doesn't exist yet,
encodeAllruns without a priordecodeAlland renders the file from scratch. - With
locales: 'infer', the locales are read from inside the file.
Converting structured values
Some formats store plurals as structured data rather than as part of the message text:
messages/en.json
{"itemsCount": {"one": "One item","other": "# items"}}
In that case, convert the structure to an ICU plural on decode:
{itemsCount, plural, one {One item} other {# items}}
… and transform it back to the structured form on encode.
This enables checks like lint rules to function correctly on the message.
Preconfigured formats
The following formats are preconfigured, and ready to use: