Skip to content

feat(i18n): cross-platform runtime i18n (en/zh/ja) - #8

Merged
do-md merged 4 commits into
mainfrom
feat/i18n
Jul 7, 2026
Merged

feat(i18n): cross-platform runtime i18n (en/zh/ja)#8
do-md merged 4 commits into
mainfrom
feat/i18n

Conversation

@do-md

@do-md do-md commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

Adds cross-platform internationalization (English / Chinese / Japanese) to DOMD. The language follows the system / browser locale — there is no in-app switcher. English is the default fallback.

Design: the Web build and the Tauri webview share one react-i18next runtime setup (no [lang] routing — static export forbids Proxy/redirects/cookies). The native Rust menu reuses the same locales/*.json via include_str!, so React and Rust share one dictionary (one source, two consumers).

Commits

  • phase 1 — client-side runtime i18n: common/i18n/ (i18next singleton, I18nProvider with switch-after-mount to avoid hydration mismatch, navigator-based detection, locales/{en,zh,ja}.json); wired ~60 hardcoded strings across editor / updater / chat / playground / landing / save-document / errors. Shared @do-md/* packages stay i18n-free.
  • phase 2 — editor CSS: localize the genuinely-visible "Toggle"/"Empty toggle" placeholders via html[lang]-keyed overrides. The code-block copy button ("Copy code"/"Copied!") is icon-only / dead CSS in the shipped @do-md/core-react package, so it is deliberately left alone (overriding would show phantom text English never shows).
  • phase 3 — native menu + macOS localization: menu_i18n module + build_app_menu + get_system_locale / set_locale commands (sys-locale for detection); Info.plist CFBundleLocalizations so macOS auto-localizes standard menu items and native dialogs. The frontend invokes set_locale to keep the native menu in sync.
  • refactor — drop the switcher: language follows the system locale only; navigator-only detection; both the webview and the Rust menu key off the system locale.

Language resolution

zh* → Chinese, ja* → Japanese, anything else → English. The frontend walks navigator.languages and picks the first supported tag; the Rust menu normalizes the system locale the same way; fallbackLng: en covers any missing key.

Verification

  • cargo check passes; frontend tsc clean on changed files.
  • SSR renders no raw keys across /, /editor, /playground.
  • Browser: system zh-CN auto-renders Chinese with no hydration / console errors.

Follow-ups (not in this PR)

  • Native menu runtime behavior (menu language, set_menu rebuild, CFBundleLocalizations) needs a real desktop build to verify.
  • The code-block copy labels are dead CSS upstream; fixing them properly needs an @do-md/core-react release.
  • The landing README body still uses the existing pickByLocale (navigator-based) — consistent with the system-locale behavior, not wired to a shared store.

Cross-platform i18n: the Web build and the Tauri webview share one
react-i18next runtime-switching setup. Reject Next.js [lang] routing
(static export forbids Proxy/redirects/cookies) in favor of pure
client-side runtime detection, matching native desktop UX (Typora/VSCode).

- Add common/i18n/: i18next singleton + I18nProvider (switch-after-mount to
  avoid hydration mismatch) + useLocale hook + config (localStorage >
  navigator detection) + locales/{en,zh,ja}.json (structurally identical)
- Add a DaisyUI language switcher in the landing nav
- Wire editor/updater/chat/playground/landing/save-document/errors (~60
  hardcoded strings); components use useTranslation, non-component modules
  use i18n.t
- Keep shared packages pure: @do-md/* pull in no i18n library
- Extract landing server-component nav/footer into client components

Verified: tsc clean on changed files; SSR renders no raw keys; browser
auto-switches to the system locale with no hydration or console errors.
@do-md/core-react is a published npm package whose ::after content text is
baked into its shipped CSS. Adding CSS custom-property hooks upstream would
need a release, so override the content at the app level, keyed on the
html[lang] attribute that I18nProvider keeps in sync with the active locale.

Finding: the code-block copy button ("Copy code"/"Copied!") is icon-only in
the shipped package - dead CSS (the nested :after resolves to a descendant
of an empty div and renders nothing). Overriding it would add phantom text
that the English UI never shows, so we deliberately do not. Only the
genuinely-visible "Toggle"/"Empty toggle" placeholders are localized.
Confirmed via browser getComputedStyle.
The native layer reuses the same translation source: Rust embeds
locales/*.json via include_str! and reads the menu.* namespace, so React and
Rust share one dictionary (one source, two consumers).

- Cargo: add sys-locale for system-language detection
- lib.rs: menu_i18n module (normalize / system_locale / t) + build_app_menu
  (generic, reused by setup and the set_locale command) + get_system_locale
  and set_locale commands. The menu is built from the system locale at
  launch; item ids stay stable so the single on_menu_event handler survives
  a rebuild
- frontend: syncNativeLocale invokes set_locale under Tauri to rebuild the
  native menu; the provider syncs on mount so the menu matches the webview
- Info.plist: CFBundleLocalizations (en/zh-Hans/ja) + AllowMixedLocalizations
  so macOS auto-localizes standard menu items (Copy/Paste/About/Quit) and
  native dialogs
- locales: add the menu.* keys, three files kept structurally identical

Verified: cargo check passes; frontend tsc clean; web-side syncNativeLocale
is a no-op. Native menu runtime behavior needs a real desktop build to
verify (the desktop shell cannot launch in this environment).
Remove the in-app language switch UI and localStorage persistence; the
language now follows the system / browser locale entirely.

- Delete common/components/language-switcher.tsx and
  common/i18n/use-locale.ts
- Remove the switcher from the landing header
- config.resolveInitialLocale: navigator-only detection (drop the
  localStorage branch); remove switcher-only exports (LOCALE_NAMES,
  LOCALE_STORAGE_KEY, isLocale)
- provider: inline syncNativeLocale, resolve the locale on mount and sync
  the native menu; no runtime switching

The Rust native menu already builds from sys-locale, so both sides now key
off the system locale. Verified: tsc clean, no dangling references, web
follows the system locale with no console errors.
@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
domd Ready Ready Preview, Comment Jul 7, 2026 6:28am

@do-md
do-md merged commit 3806234 into main Jul 7, 2026
2 checks passed
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