Vite plugin
With a Vite project, an i18n instance from the Quick start and
translation JSON on disk, let @comvi/vite-plugin watch that JSON and write the declaration
that types every t() call. It runs in-process with no API key and no network. For catalogs
that live on the Comvi platform, use comvi typegen instead; both emit the
same file.
Install
Section titled “Install”npm install -D @comvi/vite-pluginpnpm add -D @comvi/vite-pluginyarn add -D @comvi/vite-pluginbun add -d @comvi/vite-pluginThe peer dependency is vite 5, 6, 7 or 8.
Configure
Section titled “Configure”import { defineConfig } from 'vite';import { comviTypes } from '@comvi/vite-plugin';
export default defineConfig({ plugins: [ comviTypes({ translations: './src/locales', output: './src/types/i18n.d.ts', defaultNs: 'common', }), ],});The plugin generates once on buildStart and again whenever a .json file under
translations is added, changed or removed. Both paths resolve against the Vite root. Put
output inside your tsconfig.json include; most src/**/* setups already cover it.
This is a Vite plugin, not a Comvi plugin: it goes in vite.config.ts, never in .use(),
and it never touches the i18n instance. Its output is a .d.ts that augments the
TranslationKeys interface, so the runtime bundle is unchanged and the compiler is what sees
the result.
Options
Section titled “Options”| Option | Default | Behaviour |
|---|---|---|
translations |
required | Directory holding the translation JSON. |
output |
'./src/types/i18n.d.ts' |
Where the declaration is written; parent directories are created. |
fileTemplate |
'{namespace}/{languageTag}.json' |
Maps a file path to a language and a namespace. A file the template does not match is read as the default namespace, so a root en.json works with no configuration. |
defaultNs |
'default' |
Keys from this namespace lose their prefix. Set it to what your runtime uses. |
strictParams |
true |
Parameters required; false emits them as optional. |
augment |
from package.json |
The module the declaration augments. |
Keep colons out of key names. A root key such as "errors:NOT_FOUND" next to an
errors/en.json holding "NOT_FOUND" becomes the same emitted key once the default prefix
is stripped, and the emitter throws Translation key collision after stripping default namespace … instead of dropping one.
What gets generated
Section titled “What gets generated”{ "welcome": "Hello, {name}!", "items": "{count, plural, one {# item} other {# items}}", "greeting": "Hi"}{ "NOT_FOUND": "Not found"}/** * Auto-generated translation keys, augmenting '@comvi/core' * DO NOT EDIT — this file is generated from your translation schema */
// Import to ensure module is resolved before augmentationimport '@comvi/core';
declare module '@comvi/core' { interface TranslationKeys { "errors:NOT_FOUND": never; "greeting": never; "items": { "count": number }; "welcome": { "name": string }; }}
export {};import { i18n } from './i18n';
i18n.t('welcome', { name: 'Alice' });i18n.t('greeting');i18n.t('NOT_FOUND', { ns: 'errors' });import { i18n } from './i18n';
i18n.t('welcome'); // 'name' is requiredi18n.t('welcome', { name: 42 }); // number is not assignable to stringi18n.t('typo'); // unknown keyKeys come out in code-unit order and double-quoted, never means no parameters, and nested
JSON flattens to dot keys. Parameter types come from ICU syntax: a bare {name} is string,
a plural or selectordinal count is number, a select variable is string. When two
locales disagree, the union is emitted and number wins. A message that uses an argument
type @comvi/core does not implement, such as {v, number}, gets no parameters at all, and
the plugin logs a warning naming the key, the file and the argument type. The declaration
augments @comvi/core when package.json declares it, otherwise the first framework binding
it declares, and it carries no timestamp, so an unchanged catalog produces no diff.
Dev and build
Section titled “Dev and build”| Mode | Behaviour |
|---|---|
vite dev |
Generates on startup, then on every JSON add, change or delete. No restart needed. |
vite build |
Generates once before bundling. A generation error fails the build; in dev it is only logged. |
Unchanged output is not written, so TypeScript does not re-check the project for nothing. The
comparison is against the last content this process emitted, so a deleted or hand-edited file
stays that way until a catalog change or a dev-server restart. Overlapping saves collapse into
one queued re-run. Every failure logs [@comvi/vite-plugin] Failed to generate types: …: a
missing translations directory (ENOENT), a malformed JSON file, a key collision, or a
parameter type outside string and number. In dev the previous declaration stays on disk,
so the editor keeps the last good types until the JSON parses again.
Read next
Section titled “Read next”- Type-check translation keys: how the augmentation reaches
useI18n(), andInferKeys. - comvi typegen: the same declarations from the platform schema.
- Pluralize with ICU: the compiler that renders what these types describe.