diff --git a/.agents/skills/i18n-requirements/SKILL.md b/.agents/skills/i18n-requirements/SKILL.md new file mode 100644 index 0000000..b3484c9 --- /dev/null +++ b/.agents/skills/i18n-requirements/SKILL.md @@ -0,0 +1,42 @@ +--- +name: i18n-requirements +description: Use when adding or changing frontend UI text, locale support, translation JSON, the static i18n scanner, language selection, or native tray labels in this repository; keeps source messages, generated catalogs, translations, and runtime locale registration consistent. +--- + +# I18n Requirements + +## Source Messages + +- Treat `zh-CN` as the only source locale. +- Write user-visible frontend text as Chinese source literals in scanned files under `frontend/src/`. +- Do not branch on locale or hard-code English, Japanese, Russian, or other translated UI text in components or state modules. +- Let `frontend/plugins/static-i18n-plugin.js` replace source literals with runtime helpers. Do not hand-write generated message IDs in application code. +- Keep internal matching tokens out of the catalog. Use a regex for Chinese protocol/error matching instead of a user-visible string literal when the text is not intended for display. +- Do not place ordinary user-visible source messages under `frontend/src/i18n/`; the scanner excludes that directory. Native language names in `LOCALE_OPTIONS` are an intentional exception. + +## Generated Catalogs + +- Treat `frontend/src/i18n/generated/catalog.json` and the source-locale entries as scanner output. Do not manually edit catalog references or message IDs. +- Run `npm run build` from `frontend/` after changing UI text. The build must run with `--scan` and update every locale file. +- Preserve every placeholder exactly across locales, including `{0}`, `{1}`, newlines, and formula fragments such as `${1}`. +- Provide a non-empty translation for every catalog key in every non-source locale. Do not rely on the Chinese fallback for completed locale support. + +## Adding A Locale + +Update all of these integration points together: + +- `SUPPORTED_LOCALES` in `frontend/plugins/static-i18n-plugin.js`. +- `SUPPORTED_LOCALES` and `LOCALE_OPTIONS` in `frontend/src/i18n/config.js`. +- The locale JSON import, `localeMessages`, and primary-language mapping in `frontend/src/i18n/runtime.js`. +- `frontend/src/i18n/locales/.json` with the complete catalog key set. +- Native tray labels in `internal/app/runner.go`. + +## Verification + +After the scan build: + +1. Confirm `npm run build` succeeds. +2. Confirm every locale JSON has the same keys as `catalog.json`. +3. Confirm non-source locale files contain no empty values. +4. Confirm translated placeholders match the source entry placeholders. +5. Run the build twice when scanner behavior changed and confirm generated files are stable. diff --git a/.agents/skills/i18n-requirements/agents/openai.yaml b/.agents/skills/i18n-requirements/agents/openai.yaml new file mode 100644 index 0000000..1c7f6b5 --- /dev/null +++ b/.agents/skills/i18n-requirements/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "I18n Requirements" + short_description: "Keep UI translations and generated catalogs in sync" + default_prompt: "Use $i18n-requirements to update localized UI text safely." diff --git a/build/config.yml b/build/config.yml index 47e43d8..40f7761 100644 --- a/build/config.yml +++ b/build/config.yml @@ -8,7 +8,7 @@ info: description: "Cursor助手" copyright: "© 2026, Cursor助手" comments: "Cursor助手" - version: "0.0.40" + version: "0.0.41" dev_mode: root_path: . diff --git a/build/darwin/Info.dev.plist b/build/darwin/Info.dev.plist index 9aa2288..398dcfd 100644 --- a/build/darwin/Info.dev.plist +++ b/build/darwin/Info.dev.plist @@ -17,9 +17,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.0.40 + 0.0.41 CFBundleVersion - 0.0.40 + 0.0.41 LSMinimumSystemVersion 12.0.0 LSUIElement diff --git a/build/darwin/Info.plist b/build/darwin/Info.plist index 9a6bcd0..f10b5d2 100644 --- a/build/darwin/Info.plist +++ b/build/darwin/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.0.40 + 0.0.41 CFBundleVersion - 0.0.40 + 0.0.41 LSMinimumSystemVersion 12.0.0 LSUIElement diff --git a/build/linux/nfpm/nfpm.yaml b/build/linux/nfpm/nfpm.yaml index 585c306..dae8463 100644 --- a/build/linux/nfpm/nfpm.yaml +++ b/build/linux/nfpm/nfpm.yaml @@ -6,7 +6,7 @@ name: "Cursor助手" arch: ${GOARCH} platform: "linux" -version: "0.0.40" +version: "0.0.41" section: "default" priority: "extra" maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}> @@ -24,24 +24,24 @@ contents: - src: "./build/linux/Cursor助手.desktop" dst: "/usr/share/applications/Cursor助手.desktop" -# Default dependencies for the GTK4 + WebKitGTK 6.0 stack (Ubuntu 24.04+ / Debian 13+) +# Default dependencies for Debian 12/Ubuntu 22.04+ with WebKit 4.1 depends: - - libgtk-4-1 - - libwebkitgtk-6.0-4 + - libgtk-3-0 + - libwebkit2gtk-4.1-0 -# Distribution-specific overrides for different package formats +# Distribution-specific overrides for different package formats and WebKit versions overrides: - # RPM packages for Fedora / RHEL / AlmaLinux / Rocky Linux + # RPM packages for RHEL/CentOS/AlmaLinux/Rocky Linux (WebKit 4.0) rpm: depends: - - gtk4 - - webkitgtk6.0 - - # Arch Linux packages + - gtk3 + - webkit2gtk4.1 + + # Arch Linux packages (WebKit 4.1) archlinux: depends: - - gtk4 - - webkitgtk-6.0 + - gtk3 + - webkit2gtk-4.1 # scripts section to ensure desktop database is updated after install scripts: @@ -50,26 +50,13 @@ scripts: # preremove: "./build/linux/nfpm/scripts/preremove.sh" # postremove: "./build/linux/nfpm/scripts/postremove.sh" -# If you build your app with -tags gtk3 (legacy WebKit2GTK 4.1 stack — supported through v3.0.x, removed in v3.1), -# replace the depends/overrides above with these: -# -# depends: -# - libgtk-3-0 -# - libwebkit2gtk-4.1-0 -# overrides: -# rpm: -# depends: -# - gtk3 -# - webkit2gtk4.1 -# archlinux: -# depends: -# - gtk3 -# - webkit2gtk-4.1 -# # replaces: # - foobar # provides: # - bar +# depends: +# - gtk3 +# - libwebkit2gtk # recommends: # - whatever # suggests: diff --git a/build/windows/info.json b/build/windows/info.json index cb9464d..febe3aa 100644 --- a/build/windows/info.json +++ b/build/windows/info.json @@ -1,10 +1,10 @@ { "fixed": { - "file_version": "0.0.40" + "file_version": "0.0.41" }, "info": { "0000": { - "ProductVersion": "0.0.40", + "ProductVersion": "0.0.41", "CompanyName": "Cursor助手", "FileDescription": "Cursor助手", "LegalCopyright": "© 2026, Cursor助手", diff --git a/build/windows/nsis/wails_tools.nsh b/build/windows/nsis/wails_tools.nsh index 66c4232..f311b0f 100644 --- a/build/windows/nsis/wails_tools.nsh +++ b/build/windows/nsis/wails_tools.nsh @@ -14,7 +14,7 @@ !define INFO_PRODUCTNAME "Cursor助手" !endif !ifndef INFO_PRODUCTVERSION - !define INFO_PRODUCTVERSION "0.0.40" + !define INFO_PRODUCTVERSION "0.0.41" !endif !ifndef INFO_COPYRIGHT !define INFO_COPYRIGHT "© 2026, Cursor助手" @@ -27,16 +27,8 @@ !endif !define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" -!ifndef WAILS_INSTALL_SCOPE - !define WAILS_INSTALL_SCOPE "machine" -!endif - !ifndef REQUEST_EXECUTION_LEVEL - !if "${WAILS_INSTALL_SCOPE}" == "user" - !define REQUEST_EXECUTION_LEVEL "user" - !else - !define REQUEST_EXECUTION_LEVEL "admin" - !endif + !define REQUEST_EXECUTION_LEVEL "admin" !endif RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" @@ -123,40 +115,23 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" WriteUninstaller "$INSTDIR\uninstall.exe" SetRegView 64 - !if "${WAILS_INSTALL_SCOPE}" == "user" - WriteRegStr HKCU "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" - WriteRegStr HKCU "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" - WriteRegStr HKCU "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" - WriteRegStr HKCU "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" - WriteRegStr HKCU "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" - WriteRegStr HKCU "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" + WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" - ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 - IntFmt $0 "0x%08X" $0 - WriteRegDWORD HKCU "${UNINST_KEY}" "EstimatedSize" "$0" - !else - WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" - WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" - WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" - WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" - WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" - WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" - - ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 - IntFmt $0 "0x%08X" $0 - WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" - !endif + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" !macroend !macro wails.deleteUninstaller Delete "$INSTDIR\uninstall.exe" SetRegView 64 - !if "${WAILS_INSTALL_SCOPE}" == "user" - DeleteRegKey HKCU "${UNINST_KEY}" - !else - DeleteRegKey HKLM "${UNINST_KEY}" - !endif + DeleteRegKey HKLM "${UNINST_KEY}" !macroend !macro wails.setShellContext diff --git a/build/windows/wails.exe.manifest b/build/windows/wails.exe.manifest index 95dd33b..eb696bf 100644 --- a/build/windows/wails.exe.manifest +++ b/build/windows/wails.exe.manifest @@ -1,6 +1,6 @@ - + diff --git a/frontend/package.json b/frontend/package.json index 628db1e..6a1ff35 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,8 +5,8 @@ "type": "module", "scripts": { "dev": "vite", - "build:dev": "node ./scripts/run-vite-build.mjs --minify false --mode development", - "build": "node ./scripts/run-vite-build.mjs --mode production", + "build:dev": "node ./scripts/run-vite-build.mjs --scan --minify false --mode development", + "build": "node ./scripts/run-vite-build.mjs --scan --mode production", "preview": "vite preview" }, "dependencies": { diff --git a/frontend/plugins/static-i18n-plugin.js b/frontend/plugins/static-i18n-plugin.js index 1d136d0..52191ba 100644 --- a/frontend/plugins/static-i18n-plugin.js +++ b/frontend/plugins/static-i18n-plugin.js @@ -11,7 +11,7 @@ import { normalizePath } from "vite"; const traverse = traverseModule.default ?? traverseModule; const SOURCE_LANGUAGE = "zh-CN"; -const SUPPORTED_LOCALES = ["zh-CN", "en-US", "ja-JP"]; +const SUPPORTED_LOCALES = ["zh-CN", "en-US", "ja-JP", "ru-RU"]; const HAN_REGEX = /\p{Script=Han}/u; const JS_HELPERS = { localized: "__i18nLocalized", @@ -490,22 +490,34 @@ function walkTemplateNode(node, visitor) { } } -function transformVueTemplate(templateCode, filePath, rootDir) { +function resolveAbsoluteLoc(sourceCode, sourceOffset, relativeOffset) { + const absoluteOffset = sourceOffset + relativeOffset; + const prefix = sourceCode.slice(0, absoluteOffset); + const lastLineBreak = prefix.lastIndexOf("\n"); + return { + line: prefix.split("\n").length, + column: absoluteOffset - lastLineBreak, + }; +} + +function transformVueTemplate(templateCode, filePath, rootDir, options = {}) { const ast = parseTemplate(templateCode, { comments: true }); const replacements = []; const records = []; + const sourceCode = options.sourceCode ?? templateCode; + const sourceOffset = options.sourceOffset ?? 0; + const resolveLoc = (node, offset = 0) => + resolveAbsoluteLoc(sourceCode, sourceOffset, node.loc.start.offset + offset); walkTemplateNode(ast, (node, parent) => { if (node.type === 2 && containsHan(node.content)) { + const canonical = node.content.trim(); const record = buildMessageRecord( filePath, rootDir, - node.content.trim(), + canonical, 0, - { - line: node.loc.start.line, - column: node.loc.start.column + 1, - }, + resolveLoc(node, node.content.indexOf(canonical)), ); const replacement = createTextNodeReplacement(node.loc.source, record); if (!replacement) { @@ -527,10 +539,7 @@ function transformVueTemplate(templateCode, filePath, rootDir) { rootDir, node.value.content, 0, - { - line: node.loc.start.line, - column: node.loc.start.column + 1, - }, + resolveLoc(node), ); records.push(record); replacements.push({ @@ -552,10 +561,7 @@ function transformVueTemplate(templateCode, filePath, rootDir) { node.content, filePath, rootDir, - { - line: node.loc.start.line, - column: node.loc.start.column + 1, - }, + resolveLoc(node), ); if (!transformed.changed) { return; @@ -585,7 +591,10 @@ function transformVueSFC(code, filePath, rootDir) { let changed = false; if (descriptor.template) { - const templateResult = transformVueTemplate(descriptor.template.content, filePath, rootDir); + const templateResult = transformVueTemplate(descriptor.template.content, filePath, rootDir, { + sourceCode: code, + sourceOffset: descriptor.template.loc.start.offset, + }); records.push(...templateResult.records); if (templateResult.changed) { changed = true; diff --git a/frontend/src/components/HomeMetricsCard.vue b/frontend/src/components/HomeMetricsCard.vue index 986bcb8..c815b73 100644 --- a/frontend/src/components/HomeMetricsCard.vue +++ b/frontend/src/components/HomeMetricsCard.vue @@ -273,7 +273,6 @@ const hasHomeAd = computed(() => normalizedHomeAds.value.length > 0);
- 刷新统计