feat: add Chinese i18n support with vue-i18n#846
Conversation
Greptile SummaryThis 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
Confidence Score: 3/5Not 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
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]
Reviews (5): Last reviewed commit: "merge: resolve conflicts with upstream/m..." | Re-trigger Greptile |
| 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', |
There was a problem hiding this comment.
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.
| locale: localStorage.getItem('locale') || navigator.language.startsWith('zh') ? 'zh' : 'en', | |
| locale: localStorage.getItem('locale') || (navigator.language.startsWith('zh') ? 'zh' : 'en'), |
| 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>' }) }} |
There was a problem hiding this comment.
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.
| | {{ $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') }} |
| "notFound.title": "Woops, this page was not found!", | ||
| "notFound.message": "Try navigating back where you came from.", |
There was a problem hiding this comment.
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.
| "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" | ||
| }, |
There was a problem hiding this comment.
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.
|
Erik seems to support fluent-vue in favour of vue-i18n #2 (comment) |
| div(v-if="loading") | ||
| | Loading... | ||
| | {{ $t('categoryBuilder.loading') }} | ||
| div(v-else) |
There was a problem hiding this comment.
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.
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. |
This PR adds full Chinese (zh) language support to aw-webui: