Skip to content

feat: add i18n support#96

Open
windyy0 wants to merge 2 commits into
Lampese:mainfrom
windyy0:feat/i18n
Open

feat: add i18n support#96
windyy0 wants to merge 2 commits into
Lampese:mainfrom
windyy0:feat/i18n

Conversation

@windyy0

@windyy0 windyy0 commented Jul 4, 2026

Copy link
Copy Markdown

Summary

  • Add English and Simplified Chinese localization
  • Support automatic system-language detection and manual language selection
  • Localize the main window, tray window, and native menus
  • Add a locale manifest and i18n validation script

Implementation

  • Use i18next and react-i18next to manage frontend translations
  • Store locale resources under locales and register them through manifest.json
  • Use the shared language preference to select translations for native Rust menus
  • Persist the selected language in application settings and restore it on startup
  • Support a system mode that follows the operating system locale
  • Add check-i18n.mjs to validate locale manifests, resource files, and translation key structure during builds
  • Support language selection in both Tauri desktop and browser dashboard modes

Testing

  • pnpm build
  • cargo test

@emreertunc emreertunc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR — solid work overall. Build (pnpm build incl. check-i18n), cargo test (21/21), locale key parity (253/253 incl. placeholders), settings backward compatibility (#[serde(default)]), and command registration (both lib.rs and web.rs) all check out. A few issues before I can approve:

Blocking

  1. "system" mode can resolve to different languages in the UI vs. native menus. The frontend resolves from navigator.languages (src/i18n/index.ts) while the backend resolves from sys_locale (src-tauri/src/i18n.rs), with the zh-CN/zh-SG/zh-Hans matching logic duplicated in both. When the two inputs disagree (e.g. WebView language list ["zh-TW", "zh-CN"], or macOS where WKWebView's navigator.language may not match the OS locale), the window renders in one language and the menus/tray in another. Suggestion: make the backend's resolved_code the single source of truth and have the frontend consume it (e.g. return it from get_app_language / include it in the language-changed payload), and drop the TS matcher.
  2. The English label for the system-language option is the lowercase literal "system" (locales/en-US/ui.json language.system, locales/en-US/native.json systemLanguage). It reads as an untranslated placeholder in both the dropdown and the native Settings > Language menu; zh-CN correctly says 跟随系统. Should be e.g. "System Default".

Minor (worth fixing here, but not deal-breakers)

  1. In AccountUsageStats.tsx, t was added to loadStats's dependency array, so every language switch re-fetches usage stats from the ChatGPT backend for each open panel. Previously fetches only happened on account/panel changes.
  2. Numbers lost thousands separators: formatNumber (Intl) was replaced with raw i18next interpolation for stats.runs / stats.days - 12345 now renders as "12345 run" instead of "12,345 runs".
  3. src/main.tsx / src/tray-main.tsx gate the first render on initializeI18n() (an IPC round-trip); if it hangs or i18next init rejects, the window stays blank (void renderApp() swallows the error). Since resources are bundled, consider rendering immediately and applying the language when it resolves.

Optional / follow-up

  • tray.rs build_menu reads the settings file 2–3× per rebuild (which happens every 60s), and in "system" mode each t() call triggers a sys_locale OS query (~30 per menu build). Resolve the language once per rebuild and pass it down.
  • Some TrayMenu.tsx callbacks capture t without listing it in deps → stale-language error text after a switch.
  • Duplication that could be consolidated later: three near-identical reset/duration formatters (TrayMenu, UsageBar, AccountUsageStats), the "chatgpt_backend" sentinel hardcoded in 5 places, and dead English default dialog titles in platform.ts (all callers pass t() now).

@windyy0

windyy0 commented Jul 20, 2026

Copy link
Copy Markdown
Author

Thanks for the detailed review. I've rebased onto the latest main and addressed the blocking and minor issues:

  1. The backend resolvedCode is now the single source of truth for UI and native menus.
  2. Changed the English system-language label to System Default.
  3. Language switching no longer refetches account usage stats.
  4. Restored locale-aware number formatting for runs and days.
  5. The app and tray now render before async language initialization to avoid a blank window.

@emreertunc emreertunc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the original review items are addressed correctly. I found one remaining startup race introduced by rendering before async language initialization.

applyLanguage() only notifies listeners that are already registered, while App subscribes to the language preference in a passive useEffect. If the backend response arrives after the initial render but before that effect runs, the translations switch correctly but the language selector keeps its initial system value, even when the saved preference is an explicit language. I reproduced this timing in a production-mode React check.

Please make the subscription provide the current snapshot immediately, or use useSyncExternalStore for the language preference.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants