Skip to content

feat: Add i18n (internationalization) support for multi-language UI#73

Open
ryouka0731 wants to merge 8 commits intostandardagents:mainfrom
ryouka0731:feature/i18n-support
Open

feat: Add i18n (internationalization) support for multi-language UI#73
ryouka0731 wants to merge 8 commits intostandardagents:mainfrom
ryouka0731:feature/i18n-support

Conversation

@ryouka0731
Copy link
Copy Markdown
Contributor

Summary

Adds internationalization (i18n) support to dmux, allowing users to switch between English and Japanese from settings.

Closes #72

Changes

Core i18n Infrastructure

  • Added src/i18n/index.ts with I18nManager class for locale management
  • Added src/i18n/locales/en.json - English translations
  • Added src/i18n/locales/ja.json - Japanese translations

Settings Integration

  • Added language?: 'en' | 'ja' field to DmuxSettings type
  • Added language selector to settings UI (first option in settings list)
  • Added getLocalizedSettingDefinitions() function for translated UI labels

UI Integration

  • Language setting is applied reactively in DmuxApp via useEffect
  • Replaced hardcoded commit option labels with i18n translations
  • Updated PopupManager to use localized setting definitions

Technical Improvements

  • Fixed ESM compatibility: uses import.meta.url instead of __dirname
  • Dynamic translation loading: scans locales directory automatically
  • Fallback to English if translation key not found
  • Parameter interpolation support: t('key', { param: value })

How to Use

  1. Open settings (S key)
  2. Select 'Language' (first option)
  3. Choose 'English' or '日本語'

Future Improvements

  • Add more languages
  • Translate remaining hardcoded UI strings
  • Extract all UI text to translation files

- Add i18n infrastructure with locale management (src/i18n/)
- Create English (en) and Japanese (ja) translation files
- Add language setting to DmuxSettings type
- Add language selector to settings UI
- Apply language setting in DmuxApp on startup
- Replace hardcoded commit option labels with i18n translations

This allows users to switch between English and Japanese from settings.
- Fix ESM compatibility: use import.meta.url instead of __dirname
- Make translation loading dynamic (scan locales directory)
- Add reactive settings updates with useMemo
- Add getLocalizedSettingDefinitions() for translated UI labels
- Update PopupManager to use localized setting definitions
@justin-schroeder
Copy link
Copy Markdown
Collaborator

This is a great idea, but causes some significant issues:

  1. The packaged/runtime build is broken because the new i18n loader expects JSON files that the build does not ship. src/i18n/index.ts:28 reads locales/.json from disk at runtime, but the package build is only tsc and the published files list only dist/**/ and native/**/* in package.json:37 and package.json:45. I verified this directly: after pnpm run build, importing dist/i18n/index.js throws ENOENT for dist/i18n/locales, and npm pack includes package/dist/i18n/index.js but no locale JSON files. That means a release build falls back to raw keys instead of translations.

  2. The PR makes dmux settings effectively non-reactive after save because settings is memoized against the wrong state. In src/DmuxApp.tsx:115, settings is derived with useMemo, but the dependency is projectSettings from src/hooks/useProjectSettings.ts:14, which tracks test/dev project settings rather than dmux settings. The settings popup persists dmux settings through src/hooks/useInputHandling.ts:1255. Result: changing language, enabled agents, footer tips, default agent, etc. will not reliably update the live TUI until restart or some unrelated state change. That undermines the main feature of the PR.

  3. Importing i18n into src/utils/settingsManager.ts:27 adds filesystem-backed side effects to every SettingsManager consumer, including non-UI code. Tests still passed, but pnpm run test emitted repeated translation-load errors from mocked fs modules because src/i18n/index.ts:24 now does synchronous locale loading at module import time. That is not a security issue, but it is a maintainability and test-isolation regression.

ryouka0731 and others added 6 commits May 2, 2026 04:42
# Conflicts:
#	src/DmuxApp.tsx
#	src/services/PopupManager.ts
#	src/utils/settingsManager.ts
… side effects

Replaces fs/JSON-at-runtime loading with statically imported TypeScript
locale modules. This addresses two of the three review items on PR standardagents#73:

- Translation files are now part of the TypeScript build output, so
  release builds no longer fall back to raw keys (locales were missing
  from package.json `files` previously).
- SettingsManager and other non-UI consumers no longer trigger fs reads
  at module import time, fixing the test-isolation regression seen with
  mocked fs modules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces src/i18n/locales/en.json with en.ts so translations ship as
part of dist/i18n/locales after tsc — the JSON file was not included
in the published package's `files` allowlist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces src/i18n/locales/ja.json with ja.ts so translations ship as
part of dist/i18n/locales after tsc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers the public i18n contract: default locale, locale switching,
unknown-locale handling, nested key lookups, fallback to English for
missing keys, key-as-default behavior for unknown keys, parameter
interpolation, getAvailableLocales immutability, and translation
parity for a representative key set across en/ja.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ones

The previous tests for English fallback and {param} interpolation only
exercised hand-written regex copies of the implementation, not the
public t() API. They also covered just 10 representative keys.

Replaces them with:
- vi.doMock-driven tests that load i18n with controlled asymmetric
  locales to truly drive the en-fallback path through t().
- Interpolation tests that resolve placeholders through t() with a
  bundled placeholder string, including numeric coercion, missing
  values left as-is, and the no-interpolation-on-unknown-keys contract.
- A deep en/ja key-tree parity check (every leaf path must exist on
  both sides) and a full round-trip that asserts every bundled key
  resolves to a non-key string in both locales.
- A source-level guard ensuring src/i18n/index.ts never reintroduces
  fs / readFileSync / readdirSync imports — the regression vector
  flagged by PR standardagents#73 review item 3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ryouka0731
Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review! All three items are addressed; the branch is now merged with upstream/main (state: MERGEABLE / CLEAN) at HEAD d99f008.

1. Locale JSONs missing from the published build

Replaced src/i18n/locales/{en,ja}.json with .ts modules and switched src/i18n/index.ts to static imports (import en from './locales/en.js', import ja from './locales/ja.js'). The TypeScript build now emits dist/i18n/locales/{en,ja}.js, which is already covered by "files": ["dist/**/*"] in package.json.

Verified end-to-end:

$ pnpm pack --pack-destination /tmp/dmux-pack
$ tar -tzf /tmp/dmux-pack/dmux-*.tgz | grep i18n
package/dist/i18n/index.js
package/dist/i18n/locales/en.js
package/dist/i18n/locales/ja.js
...
$ node -e "import('./dist/i18n/index.js').then(m => { console.log(m.t('settings.title')); m.setLocale('ja'); console.log(m.t('settings.title')); })"
Settings
設定

Commits: 2de6698, 26649b6, 08a614e.

2. Settings non-reactive after save

In the meantime upstream/main introduced the useState(settingsManager.getSettings()) + refreshDmuxSettings() pattern. useInputHandling.ts now calls refreshDmuxSettings(activeProjectRoot) after every persisted update (lines 1366 and 1385), so React re-renders on every save. I dropped this PR's broken useMemo(..., [projectSettings]) during the merge and adopted that pattern as the single source of truth. The i18n locale is then reapplied reactively via useEffect(() => setLocale(settings.language ?? 'en'), [settings.language]). Toggling Language in the settings popup now flips the UI live without a restart.

Commit: d3eb754 (merge resolution).

3. fs side effects on every SettingsManager consumer

src/i18n/index.ts no longer imports fs, fileURLToPath, or path. Locale data is bundled into the JS module graph via static imports, so importing t from settingsManager.ts is now a pure operation — no disk access at module init, no synchronous readdirSync/readFileSync under mocked-fs in tests.

To keep this property over time, __tests__/i18n.test.ts includes a source-level guard that fails if from 'fs' | 'node:fs' or readFileSync | readdirSync are ever reintroduced into the i18n source.

Commit: 2de6698.

Tests

Added __tests__/i18n.test.ts (21 cases). Highlights:

  • vi.doMock + dynamic import to inject asymmetric locales and exercise the real fallback path through t() (a key present only in en, locale set to ja, returns the en string).
  • {param} interpolation tested through t() against a mocked placeholder string — covers numeric coercion, missing param values left intact, and that interpolation does not apply when the key is unknown.
  • Deep enja key-tree parity (every leaf path must exist on both sides) and a full round-trip asserting every bundled key resolves to a non-key string in both locales.
  • The fs-import source guard described above.

pnpm run typecheck / pnpm run test / pnpm run build are all green. Happy to address anything else.

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.

I would like to be able to select a language.

2 participants