Craft Agents OSS i18n System Architecture: How to Add a New Locale

Craft Agents OSS uses i18next with react-i18next for runtime translation and a derivative-free locale registry that automatically generates all supported language constants, requiring only a JSON file and registry entry to add a new locale.

Craft Agents OSS implements a robust internationalization system built on i18next and react-i18next. The architecture centers on a single-source-of-truth locale registry in packages/shared/src/i18n/registry.ts that eliminates manual wiring by automatically deriving supported language codes, i18next resources, and date-fns locale mappings. This design ensures that adding support for a new language requires minimal changes while maintaining consistency across the Electron renderer and main processes.

Understanding the i18n System Architecture

The internationalization stack follows a layered, derivative-free approach designed for maintainability. Once a locale entry is added to the registry, all derived constants generate automatically.

Locale Files and Translation Keys

Translation files use flat-dot-notation JSON stored in packages/shared/src/i18n/locales/{code}.json. The English file (en.json) serves as the master reference, with all other locales maintaining identical key structures to satisfy parity tests.

The Locale Registry (Single Source of Truth)

The LOCALE_REGISTRY in packages/shared/src/i18n/registry.ts maps each language code to its native name, translation messages, and corresponding date-fns locale. This central map automatically populates SUPPORTED_LANGUAGE_CODES and LANGUAGES in packages/shared/src/i18n/languages.ts without manual updates.

i18next Initialization

The setupI18n.ts file initializes i18next using resources built dynamically from the registry, configures fallback languages, and integrates the language detector for the renderer process.

React Integration and Non-React Usage

UI components under packages/ui/src/**/*.tsx access translations via the useTranslation() hook from react-i18next. Non-React code uses i18n.t() inside functions only, never at module load time, to avoid initialization race conditions.

Cross-Process Persistence

The renderer stores the selected language in localStorage under the key i18nextLng. The main process hydrates this value from preferences.uiLanguage at startup, ensuring synchronization between the Electron main and renderer processes according to the implementation in apps/electron/src/main/index.ts and packages/shared/src/config/preferences.ts.

How to Add a New Locale

Adding a new language to Craft Agents OSS requires five steps:

  1. Create the locale JSON file by copying the English reference and translating all keys while preserving the flat-dot structure:

    cp packages/shared/src/i18n/locales/en.json packages/shared/src/i18n/locales/<new-code>.json
  2. Import the JSON and its date-fns locale in packages/shared/src/i18n/registry.ts:

    import frMessages from "./locales/fr.json";
    import { fr as frDateLocale } from "date-fns/locale/fr";
  3. Add a registry entry in LOCALE_REGISTRY providing the native name, messages, and date locale:

    export const LOCALE_REGISTRY = {
      // existing entries...
      fr: {
        nativeName: "Français",
        messages: frMessages,
        dateLocale: frDateLocale,
      },
    } satisfies Record<string, LocaleEntry>;
  4. Run the validation test suite to verify key parity across all locales:

    bun run validate:ci
  5. Commit the changes. The CI pipeline runs lint:i18n:sorted and lint:i18n:coverage to enforce key order and usage coverage.

Implementation Examples

Configuring the Locale Registry

When adding French support, the packages/shared/src/i18n/registry.ts file requires both the translation messages and the date-fns locale for date formatting:

import frMessages from "./locales/fr.json";
import { fr as frDateLocale } from "date-fns/locale/fr";

export const LOCALE_REGISTRY = {
  // other locales...
  fr: {
    nativeName: "Français",
    messages: frMessages,
    dateLocale: frDateLocale,
  },
} satisfies Record<string, LocaleEntry>;

Using Translations in React Components

Components access the t function via the react-i18next hook:

import { useTranslation } from "react-i18next";

export function SettingsHeader() {
  const { t } = useTranslation();
  return <h1>{t("settings.title")}</h1>;
}

Accessing i18n Outside React

For utility functions or main process code, call i18n.t() inside functions to avoid initialization issues:

import { i18n } from "@craft-agent/shared/i18n";

export function formatError(errorKey: string) {
  // Must be called inside a function, not at module top-level
  return i18n.t(`errors.${errorKey}`);
}

Critical Files for i18n Management

Summary

  • Craft Agents OSS uses a derivative-free i18n architecture centered on a single registry in packages/shared/src/i18n/registry.ts.
  • Adding a new locale requires only creating a JSON translation file and registering it with its date-fns locale in the registry.
  • The system automatically generates SUPPORTED_LANGUAGE_CODES, LANGUAGES, and i18next resources without manual updates.
  • Always use useTranslation() in React components and i18n.t() inside functions only for non-React code.
  • Run bun run validate:ci to ensure key parity across all locale files before committing.

Frequently Asked Questions

What happens if I forget to add a translation key to the new locale file?

The CI pipeline executes scripts/check-i18n-parity.ts, which validates that every locale file contains the exact same keys as en.json. The test will fail if any keys are missing or extra, preventing incomplete translations from merging into the main branch.

Can I use nested JSON objects instead of flat-dot-notation for translations?

No, the architecture requires flat-dot-notation keys (e.g., "settings.title" rather than nested objects) to maintain consistency with the i18next configuration and the automated parity checking scripts used in the build pipeline.

How does the application handle language selection across the Electron main and renderer processes?

The renderer persists the selected language in localStorage under the key i18nextLng, while the main process hydrates this value from preferences.uiLanguage at startup, ensuring both processes remain synchronized according to the implementation in apps/electron/src/main/index.ts and packages/shared/src/config/preferences.ts.

Do I need to manually update the supported languages list when adding a new locale?

No, the SUPPORTED_LANGUAGE_CODES and LANGUAGES exports in packages/shared/src/i18n/languages.ts are auto-generated from the LOCALE_REGISTRY keys. Once you add the entry to the registry, all derived constants update automatically without additional manual wiring.

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 →