Understanding the i18n Architecture in OpenScreen: How Locales Are Loaded and Switched
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, 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 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, 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 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:
-
Bundle Time – Vite executes
import.meta.globinloader.ts, embedding all translation files into the JavaScript bundle as a staticmessagesobject. -
Application Start – The
I18nProvidercomponent callsgetInitialLocale()to read fromlocalStorageand validate the stored value againstSUPPORTED_LOCALES, defaulting toenif invalid. -
Component Mount – React components consume the context through
useI18n()oruseScopedT(namespace), receiving a memoizedtfunction 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 listens for the set-locale event, invoking setMainLocale(locale) from 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:
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:
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:
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:
node scripts/i18n-check.mjs
Summary
- OpenScreen uses a file-based i18n architecture defined in
src/i18n/config.tsandsrc/i18n/loader.ts, avoiding heavy external dependencies while maintaining type safety. - Messages load at build time via Vite's
import.meta.globeager imports, creating a synchronous lookup object available to both renderer and main processes without network requests. - Locale switching occurs through
setLocalein the React context, which updates state, persists tolocalStorage, sets the HTMLlangattribute, and notifies Electron via IPC. - The main process synchronizes through the
set-localeIPC handler, updatingcurrentLocaleinelectron/i18n.tsand rebuilding native menus withmainTtranslations. - Validation is enforced by
scripts/i18n-check.mjsto 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 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, and run scripts/i18n-check.mjs to validate key parity. The Vite glob import in 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 and pass the locale string manually. In the main process, import currentLocale from electron/i18n.ts to read the active language, though modifications should use the IPC set-locale handler to ensure UI synchronization across both processes.
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 →