LogoRobo.js

Configuration

Plugin options, MF2 syntax reference, type generation, and error handling.

Plugin options

The plugin accepts a single configuration option.

Prop

Type

Configure it via a plugin config file:

i18n.mjsPlugin config
config/plugins/robojs/i18n.mjs
export default {
	defaultLocale: 'en-US'
}
config/plugins/robojs/i18n.mjs
export default {
	defaultLocale: 'en-US'
}

Or inline in your Robo config:

config/robo.mjs
export default {
	plugins: [
		['@robojs/i18n', { defaultLocale: 'en-US' }]
	]
}
config/robo.mjs
export default {
	plugins: [
		['@robojs/i18n', { defaultLocale: 'en-US' }]
	]
}

If no config is provided, defaultLocale falls back to 'en-US' without a warning.

MF2 syntax reference

The plugin supports these MessageFormat 2 annotations. Parameter types are inferred automatically during type generation.

AnnotationExampleInferred type
Plain variable{$name}string
:number{$count :number}number
:integer{$count :integer}number
:decimal{$amount :decimal}number
:cardinal{$items :cardinal}number
:ordinal{$rank :ordinal}number
:date{$ts :date}Date | number
:time{$ts :time}Date | number
:datetime{$ts :datetime}Date | number

Date and time style options

The :date, :time, and :datetime annotations support style options passed to Intl.DateTimeFormat.

{$ts :date style=short}
{$ts :time style=medium}
{$ts :datetime dateStyle=full timeStyle=short}

Available styles for :date and :time: short, medium, long, full. When omitted, medium is used.

For :datetime, use dateStyle and timeStyle separately.

Pluralization

Use .match blocks for plural-sensitive messages:

.input {$count :number}
.match $count
  one {{You have {$count} item}}
  *   {{You have {$count} items}}

The plural category (one, other, few, many, etc.) follows the locale's CLDR plural rules.

Type generation

On startup and when running npx i18n, the plugin scans /locales/**/*.json and generates a TypeScript declaration file with these types:

TypeDescription
LocaleUnion of all locale strings (e.g., 'en-US' | 'es-ES')
LocaleKeyUnion of all namespaced keys (e.g., 'app:hello' | 'app:pets.count')
LocaleParamsMapMap of each key to its parameter type
ParamsFor<K>Parameter type for a given key
ReturnOf<K>string for scalar keys, string[] for array keys
LocaleIsArrayMapTracks whether each key returns an array

Generated file location

The types file is written to generated/types.d.ts inside the plugin package directory. Import the re-exported types from @robojs/i18n rather than referencing the generated file directly.

// Do this
import type { Locale, LocaleKey, ParamsFor } from '@robojs/i18n'

// Don't reference the generated path directly
// Do this

// Don't reference the generated path directly

When types are regenerated

Types are regenerated in two scenarios:

  1. Robo startup — The start lifecycle hook (src/robo/start.ts) calls loadLocales(), which scans files and writes types.
  2. CLI — Running npx i18n triggers the same process.

Type widening

When the same parameter appears with different annotations across locales, the type widens:

Locale ALocale BResulting type
numbernumbernumber
stringstringstring
numberstringnumber
Any Date | numberAnyDate | number

Array vs string keys

If all locales define a key as a string, ReturnOf<K> is string. If all define it as an array, ReturnOf<K> is string[]. If locales disagree, the return type falls back to string.

State management

The plugin stores locale data in Robo.js State under the namespace @robojs/i18n.

State keyTypeDescription
localeKeysstring[]All discovered namespaced keys
localeNamesstring[]All locale directory names
localeValuesRecord<string, Record<string, string | string[]>>Locale to key-value map

This state is ephemeral (in-memory only). Locale data is reloaded from disk on every Robo start.

Error reference

Runtime errors

ErrorConditionThrown by
Locales not loadedt() called before locales are loadedt()
Locale "X" not foundThe resolved locale doesn't exist in loaded datat()
Translation for key "X" not found in locale "Y"The namespaced key doesn't exist for the localet()
Invalid LocaleLikeInput isn't a string or object with locale/guildLocalegetLocale()
Duplicate key after flatteningA literal dotted key and nested object produce the same keyloadLocales()
No locales foundNo locale data in State when createCommandConfig() runscreateCommandConfig()

Warnings (logged, not thrown)

WarningCondition
Non-string array skippedArray value contains non-string elements
Non-string value skippedLocale JSON contains a non-string, non-array, non-object value
MF2 parse failureA message string has invalid MF2 syntax (key is skipped)

Performance

Formatter cache

Compiled MessageFormat instances are cached in memory, keyed by (locale, key, message). The cache grows with each unique combination. Clear it during tests or hot reloads:

import { clearFormatterCache } from '@robojs/i18n'
clearFormatterCache()
import { clearFormatterCache } from '@robojs/i18n'
clearFormatterCache()

Locale loading

All locale JSON files are read synchronously at startup via readFileSync. The entire dataset is stored in memory through the State API. For most projects with a few hundred keys per locale, this is fast (single-digit milliseconds).

Bidi characters

MessageFormat may insert Unicode bidi isolation characters (\u2068/\u2069) around interpolated values. These are invisible but can affect string comparisons in tests. Strip them when asserting:

const deBidi = (s: string) => s.replace(/\u2068|\u2069/g, '')
expect(deBidi(t('en-US', 'app:hello', { name: 'Robo' }))).toBe('Hello Robo!')
const deBidi = (s) => s.replace(/\u2068|\u2069/g, '')
expect(deBidi(t('en-US', 'app:hello', { name: 'Robo' }))).toBe('Hello Robo!')

Next Steps

On this page