i18n Plugin Package
The @repo/vite-plugin-i18n-types package is a custom Vite plugin that generates TypeScript types for internationalization (i18n) keys.
Overview
This plugin:
- Watches translation files for changes
- Generates TypeScript types for translation keys
- Provides type-safe translation key usage
- Ensures compile-time validation of i18n keys
Structure
packages/vite-plugin-i18n-types/
├── src/
│ ├── index.ts # Main plugin implementation
│ ├── generator.ts # Type generation logic
│ └── types.ts # Plugin type definitions
│
├── package.json
└── tsconfig.jsonPlugin Implementation
typescript
// src/index.ts
import type { Plugin } from "vite";
import { generateTypes } from "./generator";
import fs from "node:fs";
import path from "node:path";
export interface I18nPluginOptions {
translationsDir: string;
outputFile: string;
watch?: boolean;
}
export function vitePluginI18nTypes(options: I18nPluginOptions): Plugin {
const { translationsDir, outputFile, watch = true } = options;
let watcher: fs.FSWatcher | null = null;
return {
name: "vite-plugin-i18n-types",
configResolved() {
// Generate types on plugin load
generateTypes(translationsDir, outputFile);
// Watch for changes in development
if (watch) {
watcher = fs.watch(translationsDir, { recursive: true }, () =>
generateTypes(translationsDir, outputFile),
);
}
},
buildEnd() {
// Clean up watcher
if (watcher) {
watcher.close();
}
},
};
}Type Generation
typescript
// src/generator.ts
import fs from "node:fs";
import path from "node:path";
interface TranslationKeys {
[key: string]: string | TranslationKeys;
}
export function generateTypes(
translationsDir: string,
outputFile: string,
): void {
// Read translation files (e.g., en.json, es.json)
const files = fs.readdirSync(translationsDir);
const jsonFiles = files.filter((f) => f.endsWith(".json"));
if (jsonFiles.length === 0) {
console.warn("No translation files found");
return;
}
// Parse first translation file as reference
const refFile = path.join(translationsDir, jsonFiles[0]);
const translations: TranslationKeys = JSON.parse(
fs.readFileSync(refFile, "utf-8"),
);
// Generate type definitions
const types = generateTypeDefinitions(translations);
// Write to output file
const content = `// Auto-generated by vite-plugin-i18n-types
// Do not edit manually
${types}
`;
fs.writeFileSync(outputFile, content);
console.log(`Generated i18n types: ${outputFile}`);
}
function generateTypeDefinitions(obj: TranslationKeys, prefix = ""): string {
const keys: string[] = [];
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === "string") {
keys.push(` | '${fullKey}'`);
} else if (typeof value === "object") {
keys.push(...generateTypeDefinitions(value, fullKey).split("\n"));
}
}
if (prefix === "") {
return `export type I18nKey =\n${keys.join("\n")};\n`;
}
return keys.join("\n");
}Usage
In Vite Config
typescript
// apps/client-frontend/vite.config.ts
import { defineConfig } from "vite";
import { vitePluginI18nTypes } from "@repo/vite-plugin-i18n-types";
export default defineConfig({
plugins: [
vitePluginI18nTypes({
translationsDir: "./src/locales",
outputFile: "./src/types/i18n.gen.ts",
watch: true,
}),
],
});Translation Files
json
// src/locales/en.json
{
"common": {
"welcome": "Welcome",
"logout": "Logout",
"save": "Save"
},
"courses": {
"title": "My Courses",
"noCourses": "No courses found",
"create": "Create Course"
},
"errors": {
"notFound": "Not found",
"serverError": "Server error"
}
}Generated Types
typescript
// src/types/i18n.gen.ts (auto-generated)
export type I18nKey =
| "common.welcome"
| "common.logout"
| "common.save"
| "courses.title"
| "courses.noCourses"
| "courses.create"
| "errors.notFound"
| "errors.serverError";Type-Safe Usage
typescript
// In your components
import type { I18nKey } from "./types/i18n.gen";
function translate(key: I18nKey): string {
// Your translation logic
return translations[key];
}
// ✅ Valid - TypeScript knows these keys exist
translate("common.welcome");
translate("courses.title");
// ❌ TypeScript error - key doesn't exist
translate("invalid.key");Integration with i18n Libraries
With react-i18next
typescript
import { useTranslation } from 'react-i18next';
import type { I18nKey } from './types/i18n.gen';
function MyComponent() {
const { t } = useTranslation();
// Type-safe translation
const title = t('courses.title' as I18nKey);
return <h1>{title}</h1>;
}With Custom Hook
typescript
import type { I18nKey } from "./types/i18n.gen";
export function useI18n() {
const [locale, setLocale] = useState("en");
const translations = getTranslations(locale);
const t = (key: I18nKey): string => {
const keys = key.split(".");
let value: any = translations;
for (const k of keys) {
value = value?.[k];
}
return value || key;
};
return { t, locale, setLocale };
}Benefits
- Type Safety: Compile-time validation of translation keys
- Auto-complete: IDE suggests available keys
- Refactoring: Rename keys safely across codebase
- Documentation: Types serve as documentation
- Error Prevention: Catch missing translations early
Plugin Options
| Option | Type | Default | Description |
|---|---|---|---|
translationsDir | string | - | Path to translation files directory |
outputFile | string | - | Output path for generated types |
watch | boolean | true | Watch for changes in development |
Advanced Features
Parameterized Translations
typescript
// Translation with parameters
{
"greeting": "Hello, {name}!",
"itemCount": "You have {count} items"
}
// Usage
t('greeting', { name: 'John' }); // "Hello, John!"
t('itemCount', { count: 5 }); // "You have 5 items"Pluralization
json
{
"items": {
"zero": "No items",
"one": "One item",
"other": "{count} items"
}
}typescript
// Generate types for plural forms
export type I18nKey = "items.zero" | "items.one" | "items.other";Nested Objects
json
{
"pages": {
"home": {
"title": "Home",
"subtitle": "Welcome to our platform"
},
"about": {
"title": "About Us",
"content": "Learn more about us"
}
}
}typescript
// Generates all nested paths
export type I18nKey =
| "pages.home.title"
| "pages.home.subtitle"
| "pages.about.title"
| "pages.about.content";Best Practices
- Organize by domain - Group related translations
- Use descriptive keys - Make intent clear
- Keep values simple - Avoid complex HTML in translations
- Provide fallbacks - Handle missing translations gracefully
- Review generated types - Ensure they match expectations
- Version control - Commit generated types for consistency
Troubleshooting
Types Not Updating
bash
# Manually trigger generation
touch src/locales/en.json
# Restart Vite dev server
pnpm devMissing Keys
bash
# Ensure all translation files have same structure
# Run validation script
node scripts/validate-translations.jsRelated Documentation
- Frontend Components - Using i18n in components
- Architecture Overview - Plugin in build pipeline