i18n & Hooks
Internationalization and roughly 75 custom hooks.
Supported Languages
| Code | Language | Native Name |
|---|---|---|
en | English (default) | English |
zh-TW | Chinese (Traditional) | 繁體中文 |
zh-CN | Chinese (Simplified) | 简体中文 |
ja | Japanese | 日本語 |
ko | Korean | 한국어 |
fr | French | Français |
de | German | Deutsch |
es | Spanish | Español |
pt | Portuguese | Português |
it | Italian | Italiano |
ru | Russian | Русский |
i18n Configuration
The i18n system is configured in src/lib/i18n.ts and src/lib/languageConfig.ts:
- Library: i18next with react-i18next bindings
- Detection: Custom locale resolution via
resolveInitialLanguage()(usesnavigator.languageswith fuzzy locale matching) - Storage: Language preference saved to localStorage (
ryos:language,ryos:language-initialized) - Fallback: English (
en) as default fallback language - Runtime switching:
applyLanguage()ensures resources are loaded beforei18n.changeLanguage() - Chinese variants:
zh-Hant,zh-TW,zh-HK, andzh-MOresolve to Traditional Chinese;zh-Hans,zh-CN, andzh-SGresolve to Simplified Chinese
Locale Bundle Loading
- Eager default locale: English (
en) translation bundle is imported at startup
- Lazy non-default locales: Other locale JSON files are dynamically imported on demand (
localeLoaders) - Deduplicated loading: Concurrent requests for the same locale share a single promise (
loadingLanguages) - Bootstrap flow:
initializeI18n()initializes i18n, resolves initial language, lazy-loads if needed, then applies language
Translation File Structure
Translations are stored in src/lib/locales/{lang}/translation.json:
src/lib/locales/
├── en/translation.json
├── zh-TW/translation.json
├── zh-CN/translation.json
├── ja/translation.json
├── ko/translation.json
├── fr/translation.json
├── de/translation.json
├── es/translation.json
├── pt/translation.json
├── it/translation.json
└── ru/translation.json
Translation Key Patterns
| Pattern | Description | Example |
|---|---|---|
common.* | Shared UI elements (menus, dialogs, buttons) | common.menu.file |
common.auth.* | Shared login, signup, and recovery dialog copy | common.auth.recovery.description |
apps.{appId}.* | App-specific translations | apps.finder.name |
apps.{appId}.menu.* | App menu items | apps.ipod.menu.addSong |
apps.{appId}.help.* | App help content | apps.chats.help.chatWithRyo |
apps.{appId}.dialogs.* | App dialog text | apps.videos.dialogs.clearPlaylistTitle |
apps.{appId}.status.* | Status text and inline state labels | apps.ipod.status.shuffleOn |
apps.{appId}.toasts.* | Sonner toast titles and descriptions | apps.preview.toasts.imported |
apps.{appId}.empty.* | Empty-state titles, descriptions, and hints | apps.preview.empty.title |
apps.{appId}.ariaLabels.* | Accessibility labels | apps.ipod.ariaLabels.playPause |
apps.{appId}.speech.* | Spoken feedback labels | apps.calculator.speech.keys.plus |
apps.{appId}.conversion.* | Calculator-style conversion labels | apps.calculator.conversion.categories.length |
apps.{appId}.angle.* | Calculator angle-mode labels | apps.calculator.angle.degShort |
apps.{appId}.widgets.* | Widget translations (Dashboard) | apps.dashboard.widgets.clock |
apps.calendar.* | Calendar views, events, sidebar | apps.calendar.views.month |
apps.contacts.* | Address book, groups, fields | apps.contacts.groups.all |
apps.dashboard.* | Dashboard widgets, dictionary, stocks | apps.dashboard.stocks.loading |
settings.* | Settings panel translations | settings.language.title |
components.* | Shared component translations | components.linkPreview.openIpod |
spotlight.* | Spotlight Search UI and sections | spotlight.placeholder, spotlight.sections.apps |
Runtime formatting
The JSON catalogs are only part of localization. The International pane in Control Panels stores the active language and time-zone preferences, then the app layer formats runtime data from those settings:
| File | Role |
|---|---|
src/stores/useLanguageStore.ts | UI language selection, store hydration, and applyLanguage() dispatch |
src/lib/languageConfig.ts | Browser-language detection and ryos:language persistence |
src/lib/timezoneConfig.ts | Localized time-zone regions, cities, offsets, and aliases |
src/utils/formatCountryDisplay.ts | Localized country names for analytics and location UI |
src/apps/infinite-pc/presetI18n.ts | Virtual PC preset names and descriptions under apps.pc.presets.* |
Useful focused checks for this layer are tests/test-timezone-config.test.ts, tests/test-format-country-display.test.ts, and tests/test-account-join-date-label.test.ts.
Maintenance Workflow
Use the package scripts in package.json when adding or auditing localized UI:
# Find candidate hardcoded strings in a scoped path or glob
bun run i18n:extract --dir=src/apps/calculator
bun run i18n:extract --pattern "src/apps/calculator/**/*.{ts,tsx}"
# Add new English keys, then sync missing keys to every locale as [TODO]
bun run i18n:sync:mark-todo
# Machine-translate [TODO] keys after exporting the Google API key
export GOOGLE_GENERATIVE_AI_API_KEY="$(grep GOOGLE_GENERATIVE_AI_API_KEY .env.local | cut -d'"' -f2)"
bun run i18n:translate
bun run i18n:translate:dry-run
# Validate locale shape and remaining candidate strings
bun run i18n:sync:dry-run
bun run i18n:audit
bun run i18n:find-untranslated
i18n:translate does not auto-load .env.local; export GOOGLE_GENERATIVE_AI_API_KEY first. Review machine translations before committing because UI length, tone, and product terminology still need maintainer judgment.
Always add new keys to the English locale JSON file before using them in t() calls. A key that appears only as a defaultValue fallback still renders in English, but i18n:sync:* and i18n:audit cannot discover it, so other locales never receive the string.
Use common.* for shared UI and app-independent dialogs. The login, signup, password, and recovery dialogs live under common.auth.*, even when an app opens them.
Keep toast copy under apps.{appId}.toasts.* and empty-state copy under apps.{appId}.empty.* when the text belongs to one app. Put shared toast phrasing in common.* only when multiple apps intentionally reuse the same user-facing message.
i18n:audit is the terminology gate. It checks placeholder parity, [TODO] leftovers, CLDR plural forms, and Apple UI terminology pulled from scripts/apple-ui-terminology.ts. Use bun run i18n:audit:fix only for terminology drift that the script can repair safely, then rerun the audit. Some glossary misses need manual locale JSON edits because the right word depends on the app, menu, or dialog context.
Standalone English menu and control labels should follow Apple's title-casing style. Media labels such as Repeat All and Repeat One, auth labels such as Sign In, and inclusive system labels such as Main Volume are enforced through ENGLISH_STYLE_EXPECTATIONS in scripts/apple-ui-terminology.ts and tests/test-translation-audit.test.ts.
Run bun run i18n:apple-glossary before auditing only when the Apple terminology source data changes. The command streams the pinned raw macOS 26.1 bundle JSON that powers applelocalization-web, selects the dominant exact-match translation for each ryOS desktop locale, and refreshes the glossary input used by the audit script. It requires network access and does not modify the app locale catalogs themselves. For a resumable refresh, pass --cache-dir /tmp/ryos-apple-glossary (about 3 GB), or pass --source-dir /path/to/data/macos/26.1 to use an existing checkout of applelocalization-tools.
Key Hooks
Window & App Management:| Hook | Purpose |
|---|---|
useLaunchApp | Launch apps with multi-window support |
useWindowManager | Drag, resize, snap-to-edge |
useWindowInsets | Theme-dependent constraints |
| Hook | Purpose |
|---|---|
useSound | Web Audio API playback |
useChatSynth | Chat typing sounds |
useTerminalSounds | Terminal feedback |
useTtsQueue | Text-to-speech queue |
useAudioRecorder | Audio recording |
| Hook | Purpose |
|---|---|
useIsMobile | Mobile detection (<768px) |
useIsPhone | Phone detection (<640px) |
useMediaQuery | CSS media query hook |
Using i18n
import { useTranslation } from "react-i18next";
const { t } = useTranslation();
// Simple key lookup
t("common.menu.file") // "File"
t("apps.finder.name") // "Finder"
// With interpolation
t("common.dialog.aboutApp", { appName: "Finder" }) // "About Finder"
t("apps.ipod.dialogs.addedSongsToTop", { count: 5, plural: "s" })
// Pluralization
t("apps.ipod.menuItems.playlistTrackCount", { count: 1 }) // "1 song"
t("apps.ipod.menuItems.playlistTrackCount", { count: 5 }) // "5 songs"
Plural keys should use CLDR suffixes such as _one and _other; locales with more forms, such as Russian, may also need _few.
Helper Functions and Hooks
| Function | Purpose |
|---|---|
getTranslatedAppName(appId) | Get localized app name (theme-aware) |
getTranslatedAppDescription(appId) | Get localized app description |
getTranslatedFolderName(path) | Get localized folder name from path |
App help dialogs use useTranslatedHelpItems(appId, helpItems) from src/hooks/useTranslatedHelpItems.ts. The hook keeps the app-provided icons and replaces titles/descriptions from apps.{appId}.help.{key}.title and .description.
When adding help rows, keep the source helpItems array and the app's help key list in APP_HELP_I18N_KEYS in the same order. Long app-specific lists can live in src/apps/{app}/helpKeys.ts and be spread into APP_HELP_I18N_KEYS. tests/test-help-i18n-alignment.test.ts checks every registered app so row counts and English help keys cannot drift silently.