How @extension/i18n Delivers Type-Safe Internationalization for Chrome Extensions

The @extension/i18n package generates TypeScript types directly from your default locale JSON, exposing a single t() function that validates message keys at compile time while automatically switching between local JSON files in development and Chrome's native i18n API in production.

The chrome-extension-boilerplate-react-vite repository includes a dedicated i18n package that eliminates runtime translation errors through compile-time type checking. By deriving message keys directly from the default locale file and swapping implementations based on the build environment, it provides seamless type-safe internationalization for both development and production contexts.

Compile-Time Type Safety from the Message Catalog

Type safety begins in packages/i18n/lib/types.ts, where the package imports the default English locale file as a TypeScript constant and derives all valid message keys from its structure.

// packages/i18n/lib/types.ts
import enMessage from '../locales/en/messages.json';

export type MessageKeyType = keyof typeof enMessage;   // "greeting" | "error_network" | ...
export type LocalesJSONType = typeof enMessage;       // Full shape of locale JSON

MessageKeyType becomes the source of truth for every translation helper. Because TypeScript validates keys against this type at compile time, any typo in a message key triggers an immediate type error before the code runs. This approach guarantees that only strings actually present in locales/en/messages.json can be passed to the translation function.

Dual-Mode Implementation Strategy

The package maintains two separate implementations that share the same type-safe API surface. Depending on the build environment, the system selects either the development or production version.

Development Mode with Local JSON

In development, packages/i18n/lib/i18n-dev.ts imports the selected locale JSON directly and performs placeholder substitution using a lightweight runtime routine. The core translate function accepts a key and optional substitutions, then replaces placeholders like $1 or $2 with the provided values.

// packages/i18n/lib/i18n-dev.ts
const translate = (
  key: keyof LocalesJSONType,
  substitutions?: string | string[]
): string => {
  // Placeholder replacement logic
  let text = localeJSON[key]?.message || '';
  // ... substitution handling
  return text;
};

export const t = (...args: Parameters<typeof translate>) =>
  removePlaceholder(translate(...args));

The exported t function wraps translate and sanitizes any remaining $n placeholders that were not substituted, ensuring clean output during local testing.

Production Mode with Chrome API

For production builds, packages/i18n/lib/i18n-prod.ts proxies calls directly to Chrome's native internationalization API while preserving the strict MessageKeyType constraint.

// packages/i18n/lib/i18n-prod.ts
export const t = (key: MessageKeyType, substitutions?: string | string[]) =>
  chrome.i18n.getMessage(key, substitutions);

This implementation delegates placeholder handling to the browser, leveraging Chrome's optimized native routines while maintaining the same compile-time guarantees.

Build-Time Selection and Locale Resolution

The package uses build scripts to seamlessly swap these implementations and resolve the correct locale files without manual configuration.

Environment-Based File Swapping

packages/i18n/lib/prepare-build.ts determines which implementation file becomes the active entry point based on the IS_DEV environment flag.

// packages/i18n/lib/prepare-build.ts
const isDev = process.env.IS_DEV === 'true';
const sourceFile = isDev ? 'i18n-dev.ts' : 'i18n-prod.ts';
// Copies selected file to lib/i18n.ts

The rest of the codebase imports from @extension/i18n/lib/i18n.ts, remaining environment-agnostic while the build process handles the underlying swap.

Dynamic Locale Import Rewriting

For development, packages/i18n/lib/set-related-locale-import.ts dynamically rewrites the import statement inside the i18n file to load the appropriate locale JSON based on the developer's environment variable or host browser settings.

// packages/i18n/lib/set-related-locale-import.ts
// Rewrites import to: import localeJSON from '../locales/ko/messages.json';

This ensures that even when running locally, the system loads the correct language bundle (e.g., Korean or Spanish) rather than defaulting to English. The supported language codes are enumerated in packages/i18n/lib/consts.ts, providing a single source of truth for validation.

Practical Usage Examples

Developers consume the package through a single, unified import that works identically in both environments.

// src/components/Welcome.tsx
import { t } from '@extension/i18n';

export function Welcome({ userName }: { userName: string }) {
  // TypeScript error if "welcome_message" is not in locales/en/messages.json
  const welcome = t('welcome_message', userName);
  return <h1>{welcome}</h1>;
}

// Multiple placeholders using array substitutions
const errorMsg = t('error_multi', ['network', 'timeout']);
// Chrome replaces $1 with "network" and $2 with "timeout" in production

Because the t function accepts MessageKeyType, your IDE provides autocomplete for all valid message keys, and the TypeScript compiler blocks any undefined strings from reaching runtime.

Summary

  • Type derivation from JSON: MessageKeyType and LocalesJSONType in types.ts generate compile-time constraints from the default locale file.
  • Dual implementations: i18n-dev.ts handles local JSON loading and placeholder substitution, while i18n-prod.ts delegates to chrome.i18n.getMessage.
  • Build-time selection: prepare-build.ts swaps the active implementation based on the IS_DEV flag, ensuring the correct code path loads for each environment.
  • Dynamic locale resolution: set-related-locale-import.ts rewrites imports to load the proper language file during development, while consts.ts maintains the canonical list of supported locales.

Frequently Asked Questions

How does the package prevent typos in translation keys?

The package imports the default locale JSON as a TypeScript type and creates MessageKeyType using keyof typeof enMessage. Any string passed to the t() function must satisfy this type, causing TypeScript to flag invalid keys immediately during development.

What handles placeholder substitution in development versus production?

In development, the translate function inside i18n-dev.ts performs manual placeholder replacement for $n syntax. In production, the t() function in i18n-prod.ts passes substitutions directly to chrome.i18n.getMessage, which handles replacement using Chrome's native engine.

How does the build process select the correct i18n implementation?

prepare-build.ts checks the IS_DEV environment variable and copies either i18n-dev.ts or i18n-prod.ts to lib/i18n.ts. This allows the rest of the application to import from a stable path while the underlying logic switches automatically.

Can I add support for new languages without modifying the package source?

Yes. Add new locale directories under packages/i18n/locales/ following the en/messages.json structure. For automatic import resolution during development, ensure the language code is recognized by the logic in set-related-locale-import.ts or listed in consts.ts if you require strict validation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →