ryOS ryOS / Docs
GitHub Launch

i18n & Hooks

Internationalization and roughly 75 custom hooks.

Supported Languages

CodeLanguageNative Name
enEnglish (default)English
zh-TWChinese (Traditional)繁體中文
zh-CNChinese (Simplified)简体中文
jaJapanese日本語
koKorean한국어
frFrenchFrançais
deGermanDeutsch
esSpanishEspañol
ptPortuguesePortuguês
itItalianItaliano
ruRussianРусский

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() (uses navigator.languages with 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 before i18n.changeLanguage()
  • Chinese variants: zh-Hant, zh-TW, zh-HK, and zh-MO resolve to Traditional Chinese; zh-Hans, zh-CN, and zh-SG resolve 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

PatternDescriptionExample
common.*Shared UI elements (menus, dialogs, buttons)common.menu.file
common.auth.*Shared login, signup, and recovery dialog copycommon.auth.recovery.description
apps.{appId}.*App-specific translationsapps.finder.name
apps.{appId}.menu.*App menu itemsapps.ipod.menu.addSong
apps.{appId}.help.*App help contentapps.chats.help.chatWithRyo
apps.{appId}.dialogs.*App dialog textapps.videos.dialogs.clearPlaylistTitle
apps.{appId}.status.*Status text and inline state labelsapps.ipod.status.shuffleOn
apps.{appId}.toasts.*Sonner toast titles and descriptionsapps.preview.toasts.imported
apps.{appId}.empty.*Empty-state titles, descriptions, and hintsapps.preview.empty.title
apps.{appId}.ariaLabels.*Accessibility labelsapps.ipod.ariaLabels.playPause
apps.{appId}.speech.*Spoken feedback labelsapps.calculator.speech.keys.plus
apps.{appId}.conversion.*Calculator-style conversion labelsapps.calculator.conversion.categories.length
apps.{appId}.angle.*Calculator angle-mode labelsapps.calculator.angle.degShort
apps.{appId}.widgets.*Widget translations (Dashboard)apps.dashboard.widgets.clock
apps.calendar.*Calendar views, events, sidebarapps.calendar.views.month
apps.contacts.*Address book, groups, fieldsapps.contacts.groups.all
apps.dashboard.*Dashboard widgets, dictionary, stocksapps.dashboard.stocks.loading
settings.*Settings panel translationssettings.language.title
components.*Shared component translationscomponents.linkPreview.openIpod
spotlight.*Spotlight Search UI and sectionsspotlight.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:

FileRole
src/stores/useLanguageStore.tsUI language selection, store hydration, and applyLanguage() dispatch
src/lib/languageConfig.tsBrowser-language detection and ryos:language persistence
src/lib/timezoneConfig.tsLocalized time-zone regions, cities, offsets, and aliases
src/utils/formatCountryDisplay.tsLocalized country names for analytics and location UI
src/apps/infinite-pc/presetI18n.tsVirtual 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:
HookPurpose
useLaunchAppLaunch apps with multi-window support
useWindowManagerDrag, resize, snap-to-edge
useWindowInsetsTheme-dependent constraints
Audio System:
HookPurpose
useSoundWeb Audio API playback
useChatSynthChat typing sounds
useTerminalSoundsTerminal feedback
useTtsQueueText-to-speech queue
useAudioRecorderAudio recording
Device Detection:
HookPurpose
useIsMobileMobile detection (<768px)
useIsPhonePhone detection (<640px)
useMediaQueryCSS 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

FunctionPurpose
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.