Skip to content

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

Plugin 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

  1. Type Safety: Compile-time validation of translation keys
  2. Auto-complete: IDE suggests available keys
  3. Refactoring: Rename keys safely across codebase
  4. Documentation: Types serve as documentation
  5. Error Prevention: Catch missing translations early

Plugin Options

OptionTypeDefaultDescription
translationsDirstring-Path to translation files directory
outputFilestring-Output path for generated types
watchbooleantrueWatch 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

  1. Organize by domain - Group related translations
  2. Use descriptive keys - Make intent clear
  3. Keep values simple - Avoid complex HTML in translations
  4. Provide fallbacks - Handle missing translations gracefully
  5. Review generated types - Ensure they match expectations
  6. 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 dev

Missing Keys

bash
# Ensure all translation files have same structure
# Run validation script
node scripts/validate-translations.js