Skip to content
Comvi

Vite plugin

View .md

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.

Terminal window
npm install -D @comvi/vite-plugin

The peer dependency is vite 5, 6, 7 or 8.

vite.config.ts
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.

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.

src/locales/en.json
{
"welcome": "Hello, {name}!",
"items": "{count, plural, one {# item} other {# items}}",
"greeting": "Hi"
}
src/locales/errors/en.json
{
"NOT_FOUND": "Not found"
}
src/types/i18n.d.ts
/**
* 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 augmentation
import '@comvi/core';
declare module '@comvi/core' {
interface TranslationKeys {
"errors:NOT_FOUND": never;
"greeting": never;
"items": { "count": number };
"welcome": { "name": string };
}
}
export {};
src/usage.ts
import { i18n } from './i18n';
i18n.t('welcome', { name: 'Alice' });
i18n.t('greeting');
i18n.t('NOT_FOUND', { ns: 'errors' });
src/usage-wrong.ts
import { i18n } from './i18n';
i18n.t('welcome'); // 'name' is required
i18n.t('welcome', { name: 42 }); // number is not assignable to string
i18n.t('typo'); // unknown key

Keys 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.

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.