Skip to content

feat: add Chinese i18n support with vue-i18n#846

Open
WanderLandWalker wants to merge 6 commits into
ActivityWatch:masterfrom
WanderLandWalker:feat/chinese-i18n
Open

feat: add Chinese i18n support with vue-i18n#846
WanderLandWalker wants to merge 6 commits into
ActivityWatch:masterfrom
WanderLandWalker:feat/chinese-i18n

Conversation

@WanderLandWalker
Copy link
Copy Markdown

This PR adds full Chinese (zh) language support to aw-webui:

  • Installs vue-i18n v8 (Vue 2 compatible)
  • Creates src/locales/en.json and src/locales/zh.json with all UI strings
  • Sets up vue-i18n in main.js with browser language auto-detection
  • Adds locale setting to Pinia settings store
  • Replaces all hardcoded English strings with () in every .vue view and component
  • Adds language switcher in Settings → Theme
  • Adds Chinese README (README.zh.md) with language switcher

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 27, 2026

Greptile Summary

This PR adds full Chinese (zh) i18n support to aw-webui by installing vue-i18n v8, creating locale files for English and Chinese, wiring up auto-detection in main.js, and replacing hardcoded strings across all 30+ Vue components.

  • Locale files & setup: src/locales/en.json and zh.json (265 keys each) are added; vue-i18n is initialised in main.js with localStorage/browser-language detection; a language switcher is added to the Theme settings panel.
  • Buckets.vue structural regression: During the string-replacement refactor, Pug indentation was inadvertently broken — b-button was de-nested out of b-button-group and b-dropdown ended up as a deep child of a text node, which is illegal in Pug and will break the actions column in the bucket table.
  • CategoryBuilder.vue non-functional link: The "Settings" link in desc3 is constructed as a <router-link> HTML string and passed through mustache {{ }} interpolation, which escapes the tags, rendering them as plain visible text instead of a navigable link.

Confidence Score: 3/5

Not safe to merge as-is — the Pug indentation regression in Buckets.vue breaks the per-bucket action buttons, and CategoryBuilder.vue has a non-functional settings link.

The Pug indentation change in Buckets.vue removes b-button from its b-button-group container and places b-dropdown as a child of a text node, breaking the actions column for every user. The CategoryBuilder.vue desc3 link is silently broken. Both issues affect visible, commonly-used pages.

src/views/Buckets.vue (lines 81-86) and src/views/settings/CategoryBuilder.vue (desc3 link rendering)

Important Files Changed

Filename Overview
src/views/Buckets.vue i18n strings added throughout, but Pug indentation was broken during refactor — b-button and b-dropdown are now outside b-button-group, and two hardcoded English strings remain untranslated.
src/views/settings/CategoryBuilder.vue Settings link in desc3 is passed as a router-link HTML string through mustache interpolation which escapes it, rendering the link as plain text.
src/main.js Adds vue-i18n setup with locale detection; operator-precedence bug (flagged in prior thread) means localStorage preference is ignored.
src/locales/en.json 265 translation keys added; duplicate notFound.* keys overwrite the first block silently (flagged in prior thread).
src/locales/zh.json Chinese translations matching en.json; duplicate notFound keys issue mirrors en.json.
src/views/settings/Theme.vue Adds language switcher that correctly updates $i18n.locale and persists to localStorage and settings store.
src/stores/settings.ts Adds locale field to state interface and default value; locale is managed separately from $i18n via Theme.vue changeLanguage.
src/components/Footer.vue Correctly uses i18n component with named slots for heart/developers; remaining links use $t safely.
package-lock.json vue-i18n locked to registry.npmmirror.com (non-official Chinese mirror) instead of registry.npmjs.org — flagged in prior thread.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[App startup / main.js] --> B{localStorage locale?}
    B -- yes --> C[Use stored locale]
    B -- no --> D{navigator.language starts with zh?}
    D -- yes --> E[locale = zh]
    D -- no --> F[locale = en]
    C --> G[new VueI18n instance]
    E --> G
    F --> G
    G --> H[Vue app mounted]
    H --> I[Theme.vue language switcher]
    I --> J[changeLanguage called]
    J --> K[Update $i18n.locale]
    J --> L[localStorage.setItem locale]
    J --> M[settingsStore.update locale]
Loading

Reviews (5): Last reviewed commit: "merge: resolve conflicts with upstream/m..." | Re-trigger Greptile

Comment thread src/main.js Outdated
import en from './locales/en.json';
import zh from './locales/zh.json';
const i18n = new VueI18n({
locale: localStorage.getItem('locale') || navigator.language.startsWith('zh') ? 'zh' : 'en',
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 Operator precedence bug — localStorage preference is always ignored. JavaScript evaluates || before ?:, so this expression parses as (localStorage.getItem('locale') || navigator.language.startsWith('zh')) ? 'zh' : 'en'. Any non-null value in localStorage (including 'en') will make the whole ternary return 'zh', so a user who previously switched to English will always get Chinese on reload.

Suggested change
locale: localStorage.getItem('locale') || navigator.language.startsWith('zh') ? 'zh' : 'en',
locale: localStorage.getItem('locale') || (navigator.language.startsWith('zh') ? 'zh' : 'en'),

Comment thread src/components/Footer.vue Outdated
a(href="https://activitywatch.net/donate/", target="_blank")
icon(name="heart" scale=0.75 style="fill: #E55")
| by the #[a(href="http://activitywatch.net/contributors/") ActivityWatch developers]
| {{ $t('footer.madeWith', { developers: '<a href="http://activitywatch.net/contributors/">' + $t('footer.developers') + '</a>' }) }}
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 Two problems with this single line. First, {heart} is defined in the en.json message ("Made with {heart} by the {developers}") but no heart parameter is passed to $t, so the literal string {heart} will appear in the rendered output. Second, the developers value contains raw HTML, but {{ }} mustache interpolation escapes HTML — the <a> tag will be shown as plain text, not a clickable link. Use v-html on a span, or use the <i18n> component with slot-based interpolation to mix Vue components (icon, anchor) into translated strings.

Suggested change
| {{ $t('footer.madeWith', { developers: '<a href="http://activitywatch.net/contributors/">' + $t('footer.developers') + '</a>' }) }}
i18n(path="footer.madeWith" tag="span")
template(v-slot:heart)
a(href="https://activitywatch.net/donate/", target="_blank")
icon(name="heart" scale=0.75 style="fill: #E55")
template(v-slot:developers)
a(href="http://activitywatch.net/contributors/") {{ $t('footer.developers') }}

Comment thread src/locales/en.json
Comment on lines +228 to +229
"notFound.title": "Woops, this page was not found!",
"notFound.message": "Try navigating back where you came from.",
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 Duplicate translation keys silently overwrite earlier definitions. Both en.json and zh.json define notFound.title and notFound.message twice — once around line 175 and again here at line 228. JSON parsers silently keep only the last value, so the NotFound.vue component will show "Woops, this page was not found!" / "Try navigating back..." instead of the first set of values, and the notFound.goHome key (only in the first block) is unreachable under the second declaration of notFound.title. Rename one set of keys (e.g. to notFound2.*) or merge them into a single block.

Comment thread package-lock.json
Comment on lines +26199 to +26205
"node_modules/vue-i18n": {
"version": "8.28.2",
"resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-8.28.2.tgz",
"integrity": "sha512-C5GZjs1tYlAqjwymaaCPDjCyGo10ajUphiwA922jKt9n7KPpqR7oM1PCwYzhB/E7+nT3wfdG3oRre5raIT1rKA==",
"deprecated": "v9 and v10 no longer supported. please migrate to v11. about maintenance status, see https://vue-i18n.intlify.dev/guide/maintenance.html",
"license": "MIT"
},
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 security Package resolved from a third-party mirror registry. vue-i18n is locked to registry.npmmirror.com while every other package in this lock file resolves from registry.npmjs.org. Contributors and CI pipelines will fetch this package from a non-official Chinese CDN mirror. The integrity hash provides some protection, but supply-chain policy for this project likely expects all packages to come from the official registry. Regenerate the lock file with the default registry (npm install --registry https://registry.npmjs.org) before merging.

@0xbrayo
Copy link
Copy Markdown
Member

0xbrayo commented May 27, 2026

Erik seems to support fluent-vue in favour of vue-i18n #2 (comment)

div(v-if="loading")
| Loading...
| {{ $t('categoryBuilder.loading') }}
div(v-else)
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 security User-controlled category name interpolated into v-html without escaping. category.join(' > ') contains user-supplied text. If a category is named something like <img src=x onerror=alert(1)>, the injected HTML executes in the context of the page. Use {{ }} mustache interpolation instead, which HTML-escapes output by default, or escape the category string before building the translated message.

@WanderLandWalker
Copy link
Copy Markdown
Author

Erik seems to support fluent-vue in favour of vue-i18n #2 (comment)Erik 似乎更支持使用 fluent-vue 而不是 vue-i18n #2 (comment)

Thanks for the feedback! I chose vue-i18n because it's the most widely adopted i18n solution in the Vue ecosystem with the largest community. If the project prefers fluent-vue, I'd be happy to migrate — the locale files (en.json/zh.json) can be easily adapted to Fluent's .ftl format in a follow-up PR. The translation content is the valuable part; the library is just plumbing.

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