# Understanding the i18n Architecture in OpenScreen: How Locales Are Loaded and Switched

> Explore OpenScreen's i18n architecture. Learn how it efficiently loads and switches locales using Vite, React context, and a minimal translation module for seamless internationalization.

- Repository: [Sid/openscreen](https://github.com/siddharthvaddem/openscreen)
- Tags: internals
- Published: 2026-04-03

---

**OpenScreen implements a lightweight, file-based internationalization system that leverages Vite's eager imports for message bundling, a React context for renderer-side localization, and a minimal translation module for the Electron main process, enabling seamless runtime locale switching with automatic persistence and UI synchronization.**

The siddharthvaddem/openscreen repository uses a custom i18n architecture designed specifically for Electron applications with React frontends. This approach avoids heavy external dependencies by utilizing native ES module imports and a simple JSON-based message structure. Understanding how OpenScreen handles internationalization reveals an efficient pattern for managing translations across both renderer and main processes without the overhead of full-featured i18n libraries.

## Core Components of the OpenScreen i18n Architecture

### Configuration and Locale Definitions

In [`src/i18n/config.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/src/i18n/config.ts), the system defines `DEFAULT_LOCALE`, `SUPPORTED_LOCALES`, and translation namespaces. This central configuration also specifies the `LOCALE_STORAGE_KEY` used for persisting user preferences in `localStorage`, ensuring consistent locale selection across application restarts.

### Message Loading with Vite's Import Meta

The [`src/i18n/loader.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/src/i18n/loader.ts) file handles build-time message aggregation using `import.meta.glob("./locales/**/*.json", { eager: true })`. This eagerly imports every JSON translation file, constructing a nested `messages[locale][namespace]` map. The file exposes a `translate` function that resolves keys with variable interpolation and automatic fallback to the default locale when keys are missing.

### Renderer-Side Translation Context

React components access translations via [`src/contexts/I18nContext.tsx`](https://github.com/siddharthvaddem/openscreen/blob/main/src/contexts/I18nContext.tsx), which provides `I18nContext` containing the current locale, a `setLocale` updater, and the `t` translation function. The provider initializes the locale by calling `getInitialLocale()`, which checks `localStorage[LOCALE_STORAGE_KEY]` and validates against `SUPPORTED_LOCALES` before falling back to `DEFAULT_LOCALE`.

### Main Process Localization

For Electron menu items and dialog strings, [`electron/i18n.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/electron/i18n.ts) maintains a mutable `currentLocale` variable and exposes the `mainT` function. This lightweight module imports only the `common` and `dialogs` namespaces, ensuring the main process can display translated UI elements without loading the full React context or browser APIs.

## How Locales Are Loaded at Runtime

The loading flow follows a synchronous, three-stage initialization that eliminates asynchronous fetching complexity:

1. **Bundle Time** – Vite executes `import.meta.glob` in [`loader.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/loader.ts), embedding all translation files into the JavaScript bundle as a static `messages` object.

2. **Application Start** – The `I18nProvider` component calls `getInitialLocale()` to read from `localStorage` and validate the stored value against `SUPPORTED_LOCALES`, defaulting to `en` if invalid.

3. **Component Mount** – React components consume the context through `useI18n()` or `useScopedT(namespace)`, receiving a memoized `t` function that performs immediate key lookups with fallback support.

## Implementing Locale Switching

### Renderer-Side Updates

When `setLocale(newLocale)` is invoked from a component, the system executes four synchronized operations: updating React state to trigger re-renders, persisting the value to `localStorage`, setting `document.documentElement.lang` for accessibility standards, and notifying the main process via `window.electronAPI.setLocale`.

### Main Process Synchronization

The IPC handler registered in [`electron/main.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/electron/main.ts) listens for the `set-locale` event, invoking `setMainLocale(locale)` from [`electron/i18n.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/electron/i18n.ts) to update the mutable locale variable. It then rebuilds the application menu and tray menu using `setupApplicationMenu()` and `updateTrayMenu()`, ensuring native UI elements immediately reflect the new language via `mainT` translations.

## Practical Code Examples

### Accessing Translations in React Components

Use the `useScopedT` hook to retrieve namespace-specific translations with type safety:

```tsx
import { useScopedT } from '@/contexts/I18nContext';

export function SaveButton() {
  const t = useScopedT('common');
  return <button>{t('actions.save')}</button>;
}

```

### Building a Locale Switcher

Implement a language selector using the `useI18n` hook to access both the current locale and the setter:

```tsx
import { useI18n, useScopedT } from '@/contexts/I18nContext';

export function LocaleSwitcher() {
  const { locale, setLocale } = useI18n();
  const t = useScopedT('common');

  return (
    <div>
      <span>{t('actions.language')}:</span>
      {['en', 'zh-CN', 'es'].map((code) => (
        <button
          key={code}
          disabled={locale === code}
          onClick={() => setLocale(code as any)}
        >
          {t(`locales.${code}`)}
        </button>
      ))}
    </div>
  );
}

```

### Translating Electron Menus

Access translations in the main process using `mainT` with explicit namespace parameters:

```ts
import { mainT } from './i18n';

const template: Electron.MenuItemConstructorOptions[] = [
  {
    label: mainT('common', 'actions.file'),
    submenu: [
      { label: mainT('dialogs', 'unsavedChanges.loadProject') },
    ],
  },
];

```

## Validating Translation Integrity

To prevent missing translations across locales, OpenScreen includes `scripts/i18n-check.mjs`. This validation script recursively compares all JSON files in `src/i18n/locales/` to ensure identical key structures, flagging discrepancies before they reach production.

Run the checker with:

```bash
node scripts/i18n-check.mjs

```

## Summary

- **OpenScreen uses a file-based i18n architecture** defined in [`src/i18n/config.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/src/i18n/config.ts) and [`src/i18n/loader.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/src/i18n/loader.ts), avoiding heavy external dependencies while maintaining type safety.
- **Messages load at build time** via Vite's `import.meta.glob` eager imports, creating a synchronous lookup object available to both renderer and main processes without network requests.
- **Locale switching occurs through `setLocale`** in the React context, which updates state, persists to `localStorage`, sets the HTML `lang` attribute, and notifies Electron via IPC.
- **The main process synchronizes** through the `set-locale` IPC handler, updating `currentLocale` in [`electron/i18n.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/electron/i18n.ts) and rebuilding native menus with `mainT` translations.
- **Validation is enforced** by `scripts/i18n-check.mjs` to ensure translation parity across all supported languages.

## Frequently Asked Questions

### How does OpenScreen handle missing translations?

When a translation key is missing for the current locale, the `translate` function in [`src/i18n/loader.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/src/i18n/loader.ts) automatically falls back to the `DEFAULT_LOCALE` (typically `en`). This ensures the UI always displays readable text even when a specific translation is incomplete, preventing application crashes or empty strings.

### Can I add new languages to OpenScreen without modifying the core code?

Yes. Add a new JSON file following the namespace structure in `src/i18n/locales/`, update `SUPPORTED_LOCALES` in [`src/i18n/config.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/src/i18n/config.ts), and run `scripts/i18n-check.mjs` to validate key parity. The Vite glob import in [`loader.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/loader.ts) will automatically include the new locale at build time without requiring changes to the loading logic.

### Why does OpenScreen use a custom i18n solution instead of react-i18next?

The custom implementation in siddharthvaddem/openscreen prioritizes bundle size and synchronous loading for an Electron environment. By using `import.meta.glob` for eager imports and a minimal context provider, the system eliminates asynchronous loading complexity while supporting both renderer and main process translations with shared JSON files, reducing memory overhead.

### How do I access the current locale outside of React components?

For non-React contexts in the renderer, import `translate` directly from [`src/i18n/loader.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/src/i18n/loader.ts) and pass the locale string manually. In the main process, import `currentLocale` from [`electron/i18n.ts`](https://github.com/siddharthvaddem/openscreen/blob/main/electron/i18n.ts) to read the active language, though modifications should use the IPC `set-locale` handler to ensure UI synchronization across both processes.