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:
-
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 -
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"; -
Add a registry entry in
LOCALE_REGISTRYproviding 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>; -
Run the validation test suite to verify key parity across all locales:
bun run validate:ci -
Commit the changes. The CI pipeline runs
lint:i18n:sortedandlint:i18n:coverageto 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
packages/shared/src/i18n/registry.ts- Central locale registry where new languages are registered.packages/shared/src/i18n/setupI18n.ts- i18next initialization and resource configuration.packages/shared/src/i18n/languages.ts- Auto-generated list of supported codes derived from registry keys.scripts/check-i18n-parity.ts- CI validation script ensuring all locales match the English key set.scripts/lint-i18n-staged.sh- Pre-commit hook enforcing sorted keys and coverage requirements.packages/shared/CLAUDE.md- Internal documentation covering i18n workflow rules and constraints.
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-fnslocale in the registry. - The system automatically generates
SUPPORTED_LANGUAGE_CODES,LANGUAGES, and i18next resources without manual updates. - Always use
useTranslation()in React components andi18n.t()inside functions only for non-React code. - Run
bun run validate:cito 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →