Skip to content

feat: add Chinese localization#865

Closed
JhihJian wants to merge 11 commits into
ActivityWatch:masterfrom
JhihJian:codex/chinese-localization-clean
Closed

feat: add Chinese localization#865
JhihJian wants to merge 11 commits into
ActivityWatch:masterfrom
JhihJian:codex/chinese-localization-clean

Conversation

@JhihJian

Copy link
Copy Markdown

Summary

  • Add a lightweight i18n layer with English and Simplified Chinese locale files
  • Add a persisted display language setting with validation and a settings UI selector
  • Localize navigation, settings, activity views, visualizations, reports, tools, and common UI text

Test Plan

  • npm test -- --runInBand
  • npm run build
  • npm run tauri build -- --bundles nsis from aw-tauri (built aw-tauri_0.1.0_x64-setup.exe for Windows verification)

Notes

  • Followed ActivityWatch contributing guidance and used Conventional Commit-style commit messages.
  • aw-tauri cargo clippy -- -D warnings currently fails on pre-existing Rust warnings (child_pid, collapsible_match), unrelated to this web UI localization PR.

@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a lightweight custom i18n layer to aw-webui, introducing English and Simplified Chinese locale files, a persisted language setting with validation, and a LanguageSettings UI panel. Translations are delivered via a Vue.observable-backed t()/$t() function installed globally, with English fallback for missing keys.

  • Core i18n module (src/i18n/index.ts): prototype-safe key resolution, named interpolation, setLocale/getLocale, and installI18n() that attaches $t to Vue.prototype. Tests cover reactivity, fallback, immutability, and prototype-chain safety.
  • Settings integration: language: SupportedLocale added to the Pinia settings store with validation on both server and localStorage paths; App.vue sets the locale on startup and watches for changes; LanguageSettings.vue provides the UI selector.
  • Broad template migration: ~60 Vue components and several imperative D3/JS modules updated to replace hardcoded English strings with $t() or t() calls.

Confidence Score: 4/5

The change is safe to merge; the core i18n module, locale files, settings persistence, and template migrations are all solid. The one concrete defect — activity view tab names staying in the old language after a language switch — is a UX regression on the changed path but does not affect data integrity or break any existing functionality.

The i18n module is well-designed and thoroughly tested. Settings store language validation handles edge cases (unsupported locales, boolean-like values) from both server and localStorage. The main defect is that localizeView bakes translated view names into Pinia state at load time; when the user switches language mid-session, activity view tab labels remain in the pre-switch language because viewsStore.load() is never re-invoked. Everything else — templates, tooltips, D3 status messages, and the settings UI — works as intended.

src/stores/views.ts — the localizeView/serializeView design needs either a reload hook on language change or a template-side computed approach to avoid stale tab names.

Important Files Changed

Filename Overview
src/i18n/index.ts Lightweight custom i18n layer using Vue.observable for reactivity; clean dot-notation key resolution with prototype-safe hasOwnProperty, English fallback, and named interpolation.
src/i18n/locales/en.ts Comprehensive English locale covering all UI namespaces; includes a test namespace used only for fallback testing, which should be documented to prevent accidental production use.
src/i18n/locales/zh-CN.ts Full Simplified Chinese translation with DeepPartial typing for safe partial coverage; quality is high and all UI namespaces are represented.
src/stores/views.ts Adds localizeView/serializeView pair to translate view tab names at load time; stale view names persist in Pinia state after a language switch because the views store is not reloaded when locale changes.
src/stores/settings.ts Adds language: SupportedLocale to state with proper validation for both server and localStorage paths; post-loop isSupportedLocale check correctly handles non-string and unrecognized locale values.
src/views/settings/LanguageSettings.vue New settings panel with a locale selector; calls setLocale immediately on change and persists via store update; correctly renders only when settings are loaded.
src/App.vue Sets locale from persisted settings on beforeCreate and adds a watcher to keep the active locale in sync with store changes.
src/util/datasets.ts Adds labelKey to dataset objects for i18n; however buildBarchartDatasetActive still has label: 'Total time' hardcoded — if any consumer reads label directly it will always be English.
test/unit/i18n.test.node.ts Thorough test suite covering locale resolution, fallback, interpolation, prototype-chain safety, Vue reactivity, and immutability of SUPPORTED_LOCALES.
test/unit/store/settings.test.node.ts Adds language-specific store tests including fallback for unsupported/boolean-like values from both server and localStorage; App language watcher sync is also tested.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User selects language
LanguageSettings.vue] --> B[setLocale - i18n state]
    A --> C[useSettingsStore.update language value]
    B --> D[Vue.observable state.locale updated]
    D --> E[$t and t calls in Vue templates re-evaluate]
    D --> F[D3 imperative renders NOT re-triggered]
    C --> G[settingsStore.save then .load]
    G --> H[Settings reloaded from server/localStorage]
    H --> I[App.vue watcher fires setLocale again]
    H --> J[viewsStore NOT reloaded, view.name strings stay stale]
    J --> K[Activity view tab names show old language until page reload]
    E --> L[Nav, settings, tooltips update immediately]
Loading

Reviews (1): Last reviewed commit: "feat: localize remaining interface text" | Re-trigger Greptile

Comment thread src/stores/views.ts
Comment on lines +85 to +99
function localizeView(view: View): View {
return {
...view,
elements: view.elements.map(element => ({ ...element })),
name: view.nameKey ? t(view.nameKey) : view.name,
};
}

function serializeView(view: View): View {
return {
...view,
elements: view.elements.map(element => ({ ...element })),
name: view.nameKey ? translate(view.nameKey, 'en') : view.name,
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 View names baked into state — stale after a language switch

localizeView calls t(view.nameKey) once at load time and stores the result as a plain string in Pinia state. Because Pinia does not re-run that call when the i18n locale changes, the activity-view tab labels (Summary, Window, Browser, Editor) remain in the old language after the user changes language in Settings — they only refresh if the page is reloaded or viewsStore.load() is called again explicitly. LanguageSettings.vue only triggers useSettingsStore().update(), which reloads the settings store but does not re-invoke viewsStore.load(), so the stale names persist for the entire session.

Comment thread src/util/datasets.ts
Comment on lines 64 to +66
{
label: 'Total time',
labelKey: 'visualizationStatus.totalTime',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The label field here is what Chart.js uses directly in legends and tooltips. Unless the consuming component explicitly reads labelKey and calls $t(), the chart will always display "Total time" regardless of the active locale. The labelKey mechanism only works if every consumer respects it, making the translation incomplete by default.

Suggested change
{
label: 'Total time',
labelKey: 'visualizationStatus.totalTime',
{
label: 'visualizationStatus.totalTime',
labelKey: 'visualizationStatus.totalTime',

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread src/i18n/index.ts
Comment on lines +29 to +31
const state = Vue.observable<{ locale: SupportedLocale }>({
locale: DEFAULT_LOCALE,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 D3-rendered text is not reactively updated on locale change

t() is called at draw time inside D3 imperative functions (summary.ts, periodusage.ts, timeline.ts, timeline-simple.ts). Unlike Vue templates, Vue.observable reactivity does not cause these D3 renders to re-run when the locale changes. SVG status messages ("No data", "Today", etc.) stay in the old language after a language switch until the next data update or manual refresh. This is an inherent limitation of calling t() imperatively outside the Vue rendering cycle — worth documenting so future contributors know to trigger a re-render after locale changes.

Comment thread src/i18n/locales/zh-CN.ts
Comment on lines +1 to +6
import type en from './en';

type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 test namespace present in English but intentionally absent from zh-CN

The English locale defines a test.onlyEnglish key specifically to exercise the English-fallback path in i18n.test.node.ts. This is fine by design — satisfies DeepPartial<typeof en> allows it — but if anyone adds real UI-facing keys under test in the future without checking zh-CN, they will silently fall back to English. A brief comment inside en.ts (e.g. // test-only keys, not shown in the UI) would prevent accidental production use of this namespace.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@TimeToBuildBob

Copy link
Copy Markdown
Contributor

Superseded by #896 which consolidates zh-CN translations from this PR with #855's vue-i18n infrastructure into a single PR. Thank you for the comprehensive translation work!

TimeToBuildBob added a commit to TimeToBuildBob/aw-webui that referenced this pull request Jul 3, 2026
Consolidates i18n work from PR ActivityWatch#855 (vue-i18n with uk/de/ru) and PR ActivityWatch#865
(zh-CN translations) into a single clean PR on latest master.

Includes:
- vue-i18n@8 infrastructure with TypeScript locale files
- Language picker in Settings with localStorage persistence
- Browser locale detection on first visit
- Locale files: en, uk, de, ru, zh-CN
- Core views migrated to $t() calls
- Locale validation script and unit tests

Co-Authored-By: NureRykushBohdan
Co-Authored-By: JhihJian
@TimeToBuildBob

Copy link
Copy Markdown
Contributor

Thanks for the Chinese localization work! It was incorporated with author attribution into PR #896, which consolidates #865 and #855 (uk/de/ru) into a single clean vue-i18n implementation per Erik's request. #896 is CI-green and ready for review/merge.

If you're happy with the attribution, feel free to close this PR — the work lives on in #896.

ErikBjare pushed a commit that referenced this pull request Jul 3, 2026
* feat(i18n): add vue-i18n with en, uk, de, ru, zh-CN locales

Consolidates i18n work from PR #855 (vue-i18n with uk/de/ru) and PR #865
(zh-CN translations) into a single clean PR on latest master.

Includes:
- vue-i18n@8 infrastructure with TypeScript locale files
- Language picker in Settings with localStorage persistence
- Browser locale detection on first visit
- Locale files: en, uk, de, ru, zh-CN
- Core views migrated to $t() calls
- Locale validation script and unit tests

Co-Authored-By: NureRykushBohdan <bohdan.rykush@nure.ua>
Co-Authored-By: JhihJian <jhihjian@foxmail.com>

* fix(i18n): update package-lock.json with vue-i18n@8 entry

* style(i18n): apply prettier formatting (single quotes, trailing commas)

* fix(i18n): wire zh-CN locale

* fix(i18n): fix lint error - replace empty arrow function with jest.fn() in locale test
@ErikBjare

Copy link
Copy Markdown
Member

Merged as part of #896, thanks for contributing! ❤️

@ErikBjare ErikBjare closed this Jul 3, 2026
TimeToBuildBob added a commit to TimeToBuildBob/aw-webui that referenced this pull request Jul 3, 2026
…Watch#896)

* feat(i18n): add vue-i18n with en, uk, de, ru, zh-CN locales

Consolidates i18n work from PR ActivityWatch#855 (vue-i18n with uk/de/ru) and PR ActivityWatch#865
(zh-CN translations) into a single clean PR on latest master.

Includes:
- vue-i18n@8 infrastructure with TypeScript locale files
- Language picker in Settings with localStorage persistence
- Browser locale detection on first visit
- Locale files: en, uk, de, ru, zh-CN
- Core views migrated to $t() calls
- Locale validation script and unit tests

Co-Authored-By: NureRykushBohdan <bohdan.rykush@nure.ua>
Co-Authored-By: JhihJian <jhihjian@foxmail.com>

* fix(i18n): update package-lock.json with vue-i18n@8 entry

* style(i18n): apply prettier formatting (single quotes, trailing commas)

* fix(i18n): wire zh-CN locale

* fix(i18n): fix lint error - replace empty arrow function with jest.fn() in locale test
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.

3 participants