AI Integration

Translations (i18n)

Translate friendly labels and messages per locale, with the source text as fallback.

Experimental The enrichment workflow works today; the generated file shape and the commands may still change between minor versions.

Your FriendlyText<T> map is the source language, so there is no separate message catalog. A translation is one optional file per locale with the same shape, and any blank string in it falls back to the source text.

Configuring Locales

List your locales in the tsconfig plugin entry that holds the rest of your configuration.

{
  "compilerOptions": {
    "plugins": [
      {
        "name": "mion",
        "i18n": {
          "sourceLocale": "en",
          "locales": ["es", "pl", "pt-BR"]
        }
      }
    ]
  }
}

Translation Workflow

Each locale runs the same enrichment loop: scaffold, fill the blanks, keep in sync.

# scaffold the Polish translation files; existing files are left alone
mion enrich --i18n pl

# re-sync after a change, keeping everything you wrote
mion enrich --i18n pl --update

# every configured locale in one go
mion enrich --i18n all --update

# CI gate: fail if any translation is missing, untranslated, or out of date
mion enrich --i18n all --require-complete

# clear the comments left by removed fields
mion enrich --i18n pl --prune

enrich --i18n --no-emit reports a missing file, unfilled blanks, an out of date translation, and leftovers waiting for a prune. By default these are warnings and the command succeeds: add --require-complete to fail that run, or set i18n.strict: true to fail every run. Rendering at runtime stays lenient either way.

Translation Files

Translations live under <genDir>/enriched/i18n/<locale>/, with the same paths as the friendly files. A regional tag like pt-BR works as a folder name.

src/.mion/enriched/
  friendly/src/models/user.ts      friendlyUser (the source language)
  mock/src/models/user.ts          mockUser (mock data is never translated)
  i18n/pl/src/models/user.ts       pl_friendlyUser (Polish)
  i18n/pt-BR/src/models/user.ts    pt_BR_friendlyUser (Brazilian Portuguese)

Translating a Map

Each locale file exports one FriendlyText<T> const per type, prefixed with the locale. The scaffold starts with every string blank and marked @todo, never a copy of your source text. Placeholder tokens work as in the source map, and a field in rt$default mode has one string to translate.

import type {FriendlyText} from '@mionjs/run-types';
import type {User} from './user';

// @todo: generated skeleton — fill in real data, then delete this line
export const pl_friendlyUser: FriendlyText<User> = {
  rt$label: '',
  rt$errors: {type: ''},
  name: {
    rt$label: '',
    rt$errors: {
      type: '',
      minLength: {one: '', few: '', many: '', other: ''},
      maxLength: {one: '', few: '', many: '', other: ''},
    },
  },
  age: {
    rt$label: '',
    rt$errors: {
      type: '',
      min: {one: '', few: '', many: '', other: ''},
      max: {one: '', few: '', many: '', other: ''},
    },
  },
  isActive: {rt$label: '', rt$errors: {type: ''}},
  tags: {
    rt$label: '',
    rt$errors: {type: ''},
    rt$items: {rt$label: '', rt$errors: {type: ''}},
  },
  profile: {
    rt$label: '',
    rt$errors: {type: ''},
    email: {
      rt$label: '',
      rt$errors: {
        type: '',
        minLength: {one: '', few: '', many: '', other: ''},
        maxLength: {one: '', few: '', many: '', other: ''},
        pattern: '',
      },
    },
    score: {
      rt$label: '',
      rt$errors: {
        type: '',
        min: {one: '', few: '', many: '', other: ''},
        max: {one: '', few: '', many: '', other: ''},
      },
    },
  },
};

Keeping Translations in Sync

The friendly map and every locale file are generated from your type, never from each other. After a change, enrich --i18n pl --update adds a new message as a blank, keeps a removed one as a comment until --prune, and never loses what you wrote. Renaming a type renames its consts in every locale too.

Plural Forms per Locale

Each count rule (minLength, maxLength, min, max, lt, gt) is scaffolded with exactly the plural forms of that file's language: English gets one and other, Polish gets one, few, many and other. Other rules (type, pattern, and the rest) stay a single string.

// in the English source map
minLength: {
  one: '$[label] needs at least $[val] character',
  other: '$[label] needs at least $[val] characters',
},

The form is picked per locale with the built-in plural rules (Intl.PluralRules). Only other is required: a form you leave out or blank uses it, and if other is blank too, the source is used. A plain string also works when one wording fits every count.

The count is the rule's limit, not what the user typed. For minLength 3, the plural form and $[val] follow the 3, never the length of the received text.

Rendering Translated Messages

createFriendlyTextI18n is the locale-aware createFriendlyText: pass the source map and the translations to bundle, and you get the same renderer.

import {
  createFriendlyTextI18n,
  createGetValidationErrorsFn,
} from '@mionjs/run-types';
import type {User} from './user';
import {friendlyUser} from './friendly-user';
import {es_friendlyUser} from './i18n-es';
import {pl_friendlyUser} from './i18n-pl';

const getUserErrors = createGetValidationErrorsFn<User>();

const friendly = createFriendlyTextI18n<User>(friendlyUser, {
  locale: 'pl',
  translations: {es: es_friendlyUser, pl: pl_friendlyUser},
});

const badInput: unknown = {name: 'A', age: 200};
friendly.errors(getUserErrors(badInput));
// → Polish messages, falling back to your source text wherever a blank was left

Only the locales you import end up in your bundle. If your source maps are not in English, pass sourceLocale (default en) so their plurals use the right rules.

Switching the Locale at Runtime

locale also accepts any object with a string value, such as a Vue ref. The renderer itself is not reactive: call errors() on each render, or wrap the call in computed().

import {createFriendlyTextI18n} from '@mionjs/run-types';
import type {User} from './user';
import {friendlyUser} from './friendly-user';
import {pl_friendlyUser} from './i18n-pl';

// a plain object here, a Vue ref in a Vue app
const locale = {value: 'en'};

export const friendly = createFriendlyTextI18n<User>(friendlyUser, {
  locale, // read again on every render
  translations: {pl: pl_friendlyUser},
});

locale.value = 'pl'; // the next errors() call renders in Polish

Locale Matching

The renderer tries the exact tag, then trims it (pt-BR finds pt), then any tag with the same base language (zh-Hant finds zh-Hans). If nothing matches, it uses the source map. A missing translation never throws, and resolveLocale(locale, translations) exports the same logic.

Money in Messages

To show a money limit, declare the field with the Currency format and set currency on the renderer, not in the type or the template. Any ISO code works (EUR, USD, PLN), and like locale it can be a {value} ref you switch at runtime.

import type * as TF from '@mionjs/run-types/formats';

export interface Order {
  total: TF.Currency<{max: 10000}>;
}

$[val] then renders the amount for the active locale. Without currency it renders a plain localized number and never guesses a symbol. An app with one fixed currency can write the symbol into its templates and keep a plain number type.

Dates in Messages

Date limits need no setup. A field with any date format (a date string, a native Date bound, a Temporal type) renders its $[val] as a date in the active locale. A relative limit like now-P1D stays as written.

Copyright © 2026