Type-Safe Translations
Type-safe translations catch errors at build time instead of at runtime. Misspelled keys, missing parameters, and wrong parameter types become TypeScript errors — visible in your editor before anyone sees a broken translation in production.
Why Type Safety Matters
Section titled “Why Type Safety Matters”Without types, translation errors are silent:
// These all compile fine, but fail at runtimet('homee.title'); // Typo in key — returns "homee.title"t('greeting'); // Missing required {name} param — shows "{name}"t('cart.items', { cnt: 5 }); // Wrong param name — shows "{count}"With type-safe translations, your editor catches all of these:
t('homee.title'); // Error: key does not existt('greeting'); // Error: missing required param 'name't('cart.items', { cnt: 5 }); // Error: 'cnt' is not assignable, did you mean 'count'?-
Install the Comvi CLI
Terminal window npm install -D @comvi/cli -
Generate types from your Comvi project schema
Terminal window npx comvi typegenThis reads
.comvirc.json, fetches the translation key schema from Comvi, and generates a TypeScript declaration file. By default, it outputs tosrc/types/i18n.d.ts. -
Wire types into your i18n instance
The CLI generates a
.d.tsfile that augments@comvi/core’sTranslationKeysinterface. When you import that file, all frameworks automatically gain type safety fort():src/i18n.ts import { createI18n } from '@comvi/react';import { FetchLoader } from '@comvi/plugin-fetch-loader';// Import the generated types to augment TranslationKeysimport './types/i18n';export const i18n = createI18n({locale: 'en',fallbackLocale: 'en',}).use(FetchLoader({cdnUrl: 'https://cdn.comvi.io/your-project-id',}));src/i18n.ts import { createI18n } from '@comvi/vue';import { FetchLoader } from '@comvi/plugin-fetch-loader';import './types/i18n';export const i18n = createI18n({locale: 'en',fallbackLocale: 'en',}).use(FetchLoader({cdnUrl: 'https://cdn.comvi.io/your-project-id',}));src/i18n.ts import { createNextI18n } from '@comvi/next';import { FetchLoader } from '@comvi/plugin-fetch-loader';import './types/i18n';export const { i18n, routing } = createNextI18n({locales: ['en', 'de', 'fr'],defaultLocale: 'en',fallbackLocale: 'en',}).use(FetchLoader({cdnUrl: 'https://cdn.comvi.io/your-project-id',}));For Nuxt, ensure the generated types are imported in your setup file:
nuxt.config.ts export default defineNuxtConfig({modules: ['@comvi/nuxt'],comvi: {defaultLocale: 'en',fallbackLanguage: 'en',cdnUrl: 'https://cdn.comvi.io/your-project-id',},});comvi.setup.ts import { defineComviSetup } from '@comvi/nuxt/runtime/setup';import './types/i18n';export default defineComviSetup(({ i18n }) => {i18n.registerLoader({en: () => import('./locales/en.json'),});});src/i18n.ts import { createI18n } from '@comvi/solid';import { FetchLoader } from '@comvi/plugin-fetch-loader';import './types/i18n';export const i18n = createI18n({locale: 'en',fallbackLocale: 'en',}).use(FetchLoader({cdnUrl: 'https://cdn.comvi.io/your-project-id',}));src/i18n.ts import { createI18n } from '@comvi/svelte';import { FetchLoader } from '@comvi/plugin-fetch-loader';import './types/i18n';export const i18n = createI18n({locale: 'en',fallbackLocale: 'en',}).use(FetchLoader({cdnUrl: 'https://cdn.comvi.io/your-project-id',}));src/i18n.ts import { createI18n } from '@comvi/core';import { FetchLoader } from '@comvi/plugin-fetch-loader';import './types/i18n';export const i18n = createI18n({locale: 'en',fallbackLocale: 'en',}).use(FetchLoader({cdnUrl: 'https://cdn.comvi.io/your-project-id',})); -
Use
t()with full autocompleteconst { t } = useI18n();t('hello'); // OK — no params requiredt('greeting', { name: 'Alice' }); // OK — name is string | numbert('cart.items', { count: 5 }); // OK — count is numbert('greeting'); // Error: missing param 'name't('typo.key'); // Error: key does not exist
Generated Type Structure
Section titled “Generated Type Structure”The CLI parses your translation files and extracts ICU parameter types. Given these translation files:
{ "hello": "Hello!", "greeting": "Welcome, {name}!", "cart.items": "{count, plural, =0 {No items} one {# item} other {# items}}"}{ "page.title": "Dashboard", "stats.users": "{count, plural, one {# user} other {# users}} online"}The CLI generates a .d.ts that augments @comvi/core:
import '@comvi/core';
declare module '@comvi/core' { interface TranslationKeys { 'hello': never; 'greeting': { name: string }; 'cart.items': { count: number }; 'dashboard:page.title': never; 'dashboard:stats.users': { count: number }; }}
export {};How to read this:
- Keys map to their required parameters
nevermeans no parameters are needed{ name: string }means anameparameter is required{ count: number }meanscountis required and must be a number (used inplural)- Namespaced keys use the
namespace:keyformat (keys in the default namespace have no prefix)
Autocomplete in Action
Section titled “Autocomplete in Action”When you type t(', your editor suggests all valid keys:
t('h|')// ^ autocomplete shows:// hello// greetingWhen you select a key with parameters, your editor tells you what is required:
t('greeting', { | })// ^ autocomplete shows:// name: stringNamespaced keys can be translated by passing an explicit namespace:
const { t } = useI18n();t('page.title', { ns: 'dashboard' });Namespace-Scoped Types
Section titled “Namespace-Scoped Types”Namespace support differs by framework. Solid has namespace-bound typing for useI18n('dashboard'). React, Vue, Nuxt, and Svelte accept a namespace at runtime, but their useI18n(ns) return types are not narrowed by that argument; use the explicit { ns: 'dashboard' } parameter when you need type-checked namespaced keys across all bindings.
const { t } = useI18n();
t('hello'); // OK — default namespacet('page.title', { ns: 'dashboard' }); // OK — dashboard:page.titlet('stats.users', { ns: 'dashboard', count: 5 }); // OK — dashboard:stats.usersCI Integration
Section titled “CI Integration”Run type generation in your CI pipeline to catch missing translations before they reach production:
- name: Pull translations run: npx comvi pull
- name: Generate translation types run: npx comvi typegen
- name: Type check run: npx tsc --noEmitIf a developer adds a t('new.key') call but the key does not exist in the translation files, the type check fails and blocks the build.
Watch Mode
Section titled “Watch Mode”During development, regenerate types automatically when the project schema changes:
npx comvi typegen --watchThis subscribes to Comvi schema updates over SSE and regenerates the type definition whenever keys or parameters change upstream.
Handling Dynamic Keys
Section titled “Handling Dynamic Keys”Sometimes you need to construct a key dynamically — for example, translating an enum value or building a key from a variable. TypeScript cannot validate dynamic strings, so you need to assert the type:
type TranslationKey = keyof import('@comvi/core').TranslationKeys;
// Option 1: Type assertionconst statusKey = `order.status.${order.status}` as TranslationKey;t(statusKey);
// Option 2: Mapped lookup (safer)const statusKeys = { pending: 'order.status.pending', shipped: 'order.status.shipped', delivered: 'order.status.delivered',} as const satisfies Record<string, TranslationKey>;
t(statusKeys[order.status]);Custom Type Augmentation
Section titled “Custom Type Augmentation”If you add translations at runtime (for example, from a CMS or A/B test), you can extend the generated types with module augmentation:
declare module '@comvi/core' { interface TranslationKeys { 'experiment.headline': { variant: string }; 'experiment.cta': never; }}This merges your custom keys into the TranslationKeys interface so t() recognizes them.
CLI Options
Section titled “CLI Options”npx comvi typegen [options]| Option | Default | Description |
|---|---|---|
--config, -c | .comvirc.json | Path to config file |
--watch | false | Watch for file changes and regenerate (or watch TMS for schema changes) |
--check | false | CI mode: exit 1 if types are outdated |
Alternative: Vite Plugin for Local Files
Section titled “Alternative: Vite Plugin for Local Files”If your translations live in local files (not synced from the TMS), use the @comvi/vite-plugin for zero-config type generation:
npm install -D @comvi/vite-pluginimport { defineConfig } from 'vite';import vue from '@vitejs/plugin-vue';import { comviTypes } from '@comvi/vite-plugin';
export default defineConfig({ plugins: [ vue(), comviTypes({ translations: './src/locales', output: './src/types/i18n.d.ts', defaultNs: 'default', }), ],});The plugin watches your translation directory and regenerates types on every save, with no CLI invocation needed.