feat(i18n): add English and Chinese localization - #44
Conversation
📝 WalkthroughWalkthrough新增系统语言、English 和简体中文支持。前端页面、实时状态、设置流程、调试页面及 Tauri 托盘和窗口标题均接入运行时国际化。更新界面改为状态模型驱动。 Changes运行时国际化
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant AppConfig
participant SettingsPage
participant I18n
participant LiveStatusController
participant sync_native_i18n
participant Tauri
AppConfig->>SettingsPage: 提供 language 配置
SettingsPage->>I18n: 调用 setLanguage()
I18n->>SettingsPage: 翻译页面和更新状态
SettingsPage->>LiveStatusController: 调用 refreshLanguage()
SettingsPage->>sync_native_i18n: 发送 nativeMessages()
sync_native_i18n->>Tauri: 更新托盘菜单和窗口标题
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/i18n/index.ts`:
- Around line 5-6: 更新 MessageParameters 及 t 的类型定义,使其根据 MessageKey
对应消息中的占位符名称推导必需参数,并拒绝缺少参数或错误参数名;不含占位符的消息应允许无参数调用。对 data-i18n
等动态键,仅允许无占位符键,或提供独立的动态翻译 API;补充覆盖缺少参数和错误参数名的 `@ts-expect-error` 编译期测试。
In `@src/i18n/messages.json`:
- Line 95: Update the Simplified Chinese value for the “No events received since
launch” message in the messages localization object from the incomplete text to
“本次启动后尚未收到事件”, leaving the English translation unchanged.
In `@src/pet-debug.ts`:
- Around line 22-26: 在 src/pet-debug.ts 的 initialize 函数中补充
agent-cat-config-changed 配置变更监听;当收到语言配置变化时,重新调用 setLanguage() 并执行
translateDocument(),保持初始化时的现有语言设置与原生消息同步逻辑不变。
In `@src/settings.ts`:
- Around line 199-204: Update applyLanguage so that after translateDocument(),
it restores `#current-version` from the actual current-version value and
re-renders the update card using its existing state. Preserve all download,
installation, and error statuses while refreshing only the localized runtime
text.
- Around line 331-338: 在更新完成处理逻辑中,使用翻译函数 t 将 installButton 的文本设置为 t("Install and
Restart"),并确保该消息键已加入语言消息定义;将此赋值放在 installButton.hidden = false 之前,以便按钮显示时使用当前语言。
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: ca7b9062-3650-4a22-8af3-a2f6d0fc6058
📒 Files selected for processing (19)
e2e/readme-screenshots.screenshot.tse2e/settings.e2e.tse2e/status.e2e.tssrc-tauri/src/config.rssrc-tauri/src/lib.rssrc/i18n/index.test.tssrc/i18n/index.tssrc/i18n/messages.jsonsrc/live-status.tssrc/main.tssrc/pet-debug.htmlsrc/pet-debug.tssrc/settings.htmlsrc/settings.tssrc/status.htmlsrc/status.tssrc/styles.csssrc/types.tstsconfig.json
| export type MessageKey = keyof typeof messages; | ||
| export type MessageParameters = Record<string, string | number>; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 查找含命名占位符但未传第二个参数的直接调用。
rg -nP --glob '*.ts' '\bt\(\s*["'\''][^"'\'']*\{[A-Za-z][A-Za-z0-9]*\}[^"'\'']*["'\'']\s*\)' src e2e
# 检查翻译 API 和全部调用点,以便更新动态键调用。
ast-grep outline src/i18n/index.ts --items all
rg -n -C 2 --glob '*.ts' '\bt\s*\(' src e2eRepository: LRainner/AgentCat
Length of output: 155
🏁 Script executed:
set -euo pipefail
printf '%s\n' '--- tracked i18n files ---'
git ls-files | rg '(^|/)(i18n|.*translation|.*locale|.*messages).*|src/i18n/index\.ts$' || true
printf '%s\n' '--- src/i18n/index.ts ---'
cat -n src/i18n/index.ts
printf '%s\n' '--- translation API definitions and calls ---'
rg -n -C 3 --glob '*.{ts,tsx}' '(^|[^[:alnum:]_])t\s*[:=]|\bt\s*\(|MessageKey|MessageParameters|data-i18n' src e2e || true
printf '%s\n' '--- i18n outline ---'
ast-grep outline src/i18n/index.ts --items all || trueRepository: LRainner/AgentCat
Length of output: 41545
🏁 Script executed:
set -euo pipefail
printf '%s\n' '--- message keys and placeholders ---'
python3 - <<'PY'
import json, re
from pathlib import Path
messages = json.loads(Path("src/i18n/messages.json").read_text())
pattern = re.compile(r"\{([A-Za-z][A-Za-z0-9]*)\}")
for key, value in messages.items():
texts = value if isinstance(value, dict) else {"value": value}
names = sorted(set(n for text in texts.values() for n in pattern.findall(text)))
if names:
print(f"{key!r}: {names}")
PY
printf '%s\n' '--- i18n tests ---'
cat -n src/i18n/index.test.ts
printf '%s\n' '--- project TypeScript configuration ---'
git ls-files | rg '(^|/)(tsconfig[^/]*|package\.json|vite\.config[^/]*)$' | xargs -r -n1 sh -c 'echo "--- $0"; cat -n "$0"'
printf '%s\n' '--- data-i18n attributes in tracked files ---'
rg -n --glob '!src/i18n/messages.json' 'data-(i18n|i18n-title|i18n-aria-label|i18n-placeholder)=' . || true
printf '%s\n' '--- all literal t calls containing placeholders without a second argument ---'
python3 - <<'PY'
import re
from pathlib import Path
call = re.compile(r'\bt\(\s*(["\'])(.*?)\1\s*(?:,|\))', re.S)
for path in [Path(p) for p in __import__("subprocess").check_output(["git","ls-files","src","e2e"], text=True).splitlines()]:
if path.suffix != ".ts":
continue
text = path.read_text()
for m in call.finditer(text):
if "{" in m.group(2) and m.group(0).rstrip().endswith(")"):
print(f"{path}:{text.count(chr(10), 0, m.start())+1}: {m.group(0).replace(chr(10),' ')}")
PYRepository: LRainner/AgentCat
Length of output: 20380
将命名参数绑定到 MessageKey。
MessageParameters 当前接受任意键,t 在缺少参数时会保留 {name}。请从 MessageKey 提取占位符名称,并要求包含占位符的键提供对应参数。对 data-i18n 等动态键,仅允许不含占位符的键,或提供单独的动态翻译 API。添加 @ts-expect-error 编译期测试,覆盖缺少参数和错误参数名。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/i18n/index.ts` around lines 5 - 6, 更新 MessageParameters 及 t 的类型定义,使其根据
MessageKey 对应消息中的占位符名称推导必需参数,并拒绝缺少参数或错误参数名;不含占位符的消息应允许无参数调用。对 data-i18n
等动态键,仅允许无占位符键,或提供独立的动态翻译 API;补充覆盖缺少参数和错误参数名的 `@ts-expect-error` 编译期测试。
| pendingUpdate = update; | ||
| card.dataset.state = "ready"; | ||
| icon.textContent = "\u2713"; | ||
| title.textContent = `v${update.version} 已准备好`; | ||
| detail.textContent = "更新包已下载并通过签名验证,可以安全安装。"; | ||
| title.textContent = t("v{version} is ready", { version: update.version }); | ||
| detail.textContent = t("The update package was downloaded and passed signature verification. It is safe to install."); | ||
| progressBar.style.width = "100%"; | ||
| button.hidden = true; | ||
| installButton.hidden = false; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
显示安装按钮前设置当前语言文本。
下载完成后,代码只显示 #install-update。该按钮的初始 HTML 文本是中文,因此英语会话会显示“安装并重启”。
在设置 installButton.hidden = false 前,使用 t("Install and Restart") 写入按钮文本,并确保该消息键存在。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/settings.ts` around lines 331 - 338, 在更新完成处理逻辑中,使用翻译函数 t 将 installButton
的文本设置为 t("Install and Restart"),并确保该消息键已加入语言消息定义;将此赋值放在 installButton.hidden =
false 之前,以便按钮显示时使用当前语言。
|
PR Title: feat(i18n): add English and Chinese localization Commit: 本次 PR(fix(i18n): preserve runtime state on language changes)将设置页更新卡片渲染重构为基于 updatePresentation 判别联合状态机的集中式 renderUpdatePresentation(),并在 applyLanguage() 中于 translateDocument() 之后重渲染更新卡片与 #current-version,从而在切换语言时既保留下载/安装等运行时状态、又完成新语言的翻译。逐一核对各状态(idle/available/checking/current/downloading/ready/check-error/installing/install-error)的按钮显隐/禁用、进度条宽度、progress 显隐与 card.data-state,均与旧行为一致;translateDocument() 先于 renderUpdatePresentation() 执行,避免更新卡片上 data-i18n 默认文案覆盖运行时状态,顺序正确,未发现设置页回归。新增 e2e 用例覆盖“下载完成后切换语言仍保持 ready 状态”。pet-debug 页新增 agent-cat-config-changed 监听以实时应用语言;messages.json 修复一处中文文案。主要问题:pet-debug 的实时语言应用不完整——loadPet() 烘焙进 #debug-validation 的动态译文与原生 i18n(sync_native_i18n)不会随语言切换刷新,导致调试窗口出现新旧语言混杂。 |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
src/settings.ts (1)
432-435: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value下载失败与检查失败共用同一文案。
update.download()抛出异常时也会进入该 catch,界面显示“Unable to check for updates”。此时检查已成功,失败发生在下载阶段。用户看到的原因描述不准确。可以增加
download-error状态与对应消息键,区分两个阶段。重试路径不变。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/settings.ts` around lines 432 - 435, 区分检查阶段与下载阶段的失败状态:在涉及 update.download() 的 catch 中将 updatePresentation.phase 设置为 download-error,并增加对应的界面消息键;保留检查失败使用 check-error,且不要改变现有重试路径。e2e/settings.e2e.ts (1)
297-312: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win该测试未覆盖安装按钮文本的动态写入路径。
第 311 行断言的按钮文本来自
settings.html的静态翻译,renderUpdatePresentation的ready分支不写入该文本。因此测试无法发现install-error或installing之后重新进入ready时按钮文本陈旧的问题(见src/settings.ts第 347-355 行)。可以增加一个用例:先触发安装失败,再重新检查更新,然后断言按钮文本为安装文案。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@e2e/settings.e2e.ts` around lines 297 - 312, Extend the update-state coverage near the existing “keeps the downloaded update state when switching languages” test by exercising an install failure, then checking for updates again and returning to the ready state. Assert that the install button text is rewritten to the current install label after recovery, covering the dynamic ready branch in renderUpdatePresentation rather than relying only on static settings.html translation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/settings.ts`:
- Around line 347-355: 在 src/settings.ts#L347-L355 的 renderUpdatePresentation
ready 分支中写入 installButton.textContent = t("Install and Restart"),确保重新进入 ready
状态时显示正确文案;在 e2e/settings.e2e.ts#L297-L312
增加覆盖该路径的测试:先触发安装失败,再重新检查更新,并断言安装按钮文本为安装文案。
- Around line 323-328: Prevent the `#check-update` action from running before
initialize() has assigned currentVersion, either by binding its listener only
after initialization or by guarding the handler until initialization completes.
Ensure the "current" branch displays the initialized version and cannot render
the empty initial value.
---
Nitpick comments:
In `@e2e/settings.e2e.ts`:
- Around line 297-312: Extend the update-state coverage near the existing “keeps
the downloaded update state when switching languages” test by exercising an
install failure, then checking for updates again and returning to the ready
state. Assert that the install button text is rewritten to the current install
label after recovery, covering the dynamic ready branch in
renderUpdatePresentation rather than relying only on static settings.html
translation.
In `@src/settings.ts`:
- Around line 432-435: 区分检查阶段与下载阶段的失败状态:在涉及 update.download() 的 catch 中将
updatePresentation.phase 设置为 download-error,并增加对应的界面消息键;保留检查失败使用
check-error,且不要改变现有重试路径。
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 77e05b33-9d73-4c97-bdbd-c80d00e6c456
📒 Files selected for processing (4)
e2e/settings.e2e.tssrc/i18n/messages.jsonsrc/pet-debug.tssrc/settings.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/pet-debug.ts
- src/i18n/messages.json
| case "current": | ||
| icon.textContent = "\u2713"; | ||
| title.textContent = t("You are up to date"); | ||
| detail.textContent = t("The current version is v{version}. No updates are available.", { version: currentVersion }); | ||
| checkButton.textContent = t("Check Again"); | ||
| break; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 查找 currentVersion 的声明与赋值
rg -nP -C3 '\bcurrentVersion\b' src/settings.tsRepository: LRainner/AgentCat
Length of output: 2786
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- declarations and initialization flow ---'
sed -n '100,230p' src/settings.ts
printf '%s\n' '--- update rendering flow ---'
sed -n '280,350p' src/settings.ts
sed -n '380,420p' src/settings.ts
printf '%s\n' '--- renderUpdatePresentation call sites ---'
rg -n -C4 '\brenderUpdatePresentation\b|\binitialize\(\)' src/settings.ts
printf '%s\n' '--- static ordering check ---'
python3 - <<'PY'
from pathlib import Path
text = Path("src/settings.ts").read_text()
for name in ("let currentVersion = \"\";", "function initialize", "function applyLanguage", "function renderUpdatePresentation", "void initialize()"):
positions = [i for i in range(len(text)) if text.startswith(name, i)]
print(name, positions)
PYRepository: LRainner/AgentCat
Length of output: 15089
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- imports and version provider ---'
sed -n '1,100p' src/settings.ts
rg -n -C5 '\bgetVersion\b|\bcheckForUpdates\b|`#check-update`|addEventListener\("click"' src/settings.ts
printf '%s\n' '--- event binding and module startup ---'
sed -n '820,960p' src/settings.ts
printf '%s\n' '--- getVersion definitions and return types ---'
rg -n -C6 'function getVersion|const getVersion|export .*getVersion|invoke<.*version|app.*version|version' src -g '*.ts'Repository: LRainner/AgentCat
Length of output: 50373
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- update button markup ---'
rg -n -C4 'check-update|install-update|update-status-card' . -g '*.html' -g '*.tsx' -g '*.jsx'
printf '%s\n' '--- route and page rendering ---'
sed -n '230,280p' src/settings.ts
sed -n '760,825p' src/settings.ts
printf '%s\n' '--- focused call sequence ---'
python3 - <<'PY'
from pathlib import Path
text = Path("src/settings.ts").read_text().splitlines()
for i, line in enumerate(text, 1):
if any(token in line for token in (
'let currentVersion = ""',
'async function initialize',
'[config, currentVersion, petDirectoryInfo]',
'applyLanguage(config.language)',
'`#check-update`',
'async function checkForUpdates',
'updatePresentation = { phase: "current" }',
'renderUpdatePresentation();',
'void initialize()',
)):
print(f"{i}: {line}")
PYRepository: LRainner/AgentCat
Length of output: 8522
在初始化完成前禁用更新检查
#check-update 在 initialize() 完成前已绑定且可点击。若此时检查不到更新,"current" 分支会使用初始值 "",显示为 v。在 currentVersion 赋值后再绑定监听器,或在赋值前阻止检查。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/settings.ts` around lines 323 - 328, Prevent the `#check-update` action
from running before initialize() has assigned currentVersion, either by binding
its listener only after initialization or by guarding the handler until
initialization completes. Ensure the "current" branch displays the initialized
version and cannot render the empty initial value.
| case "ready": | ||
| icon.textContent = "\u2713"; | ||
| title.textContent = t("v{version} is ready", { version: updatePresentation.version }); | ||
| detail.textContent = t("The update package was downloaded and passed signature verification. It is safe to install."); | ||
| progressBar.style.width = "100%"; | ||
| progressElement.hidden = false; | ||
| checkButton.hidden = true; | ||
| installButton.hidden = false; | ||
| break; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
ready 分支缺少安装按钮文本写入,测试也未覆盖该路径。 renderUpdatePresentation 的公共重置不清除 installButton.textContent,只有 installing 与 install-error 分支写入文本。因此从这些状态重新进入 ready 时,按钮显示上一次的文本。
src/settings.ts#L347-L355: 在ready分支加入installButton.textContent = t("Install and Restart");。e2e/settings.e2e.ts#L297-L312: 增加用例,先触发安装失败,再重新检查更新,然后断言安装按钮文本为安装文案。
📍 Affects 2 files
src/settings.ts#L347-L355(this comment)e2e/settings.e2e.ts#L297-L312
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/settings.ts` around lines 347 - 355, 在 src/settings.ts#L347-L355 的
renderUpdatePresentation ready 分支中写入 installButton.textContent = t("Install and
Restart"),确保重新进入 ready 状态时显示正确文案;在 e2e/settings.e2e.ts#L297-L312
增加覆盖该路径的测试:先触发安装失败,再重新检查更新,并断言安装按钮文本为安装文案。
| type SpriteInspection = { unusedCells: number; nonTransparentPixels: number; transparent: boolean }; | ||
|
|
||
| renderer.onFrame = (frame) => { info.textContent = `模式: ${frame.mode}\n行: ${frame.row}\n列: ${frame.column}\n帧: ${frame.frame}${frame.angle === undefined ? "" : `\n角度: ${String(frame.angle).padStart(3, "0")}°`}`; }; | ||
| function applyLanguage(config: AppConfig): void { |
There was a problem hiding this comment.
pet-debug 页切换语言后动态校验信息与原生标题不会随语言更新
本次改动为 pet-debug 页新增了 agent-cat-config-changed 监听,在语言变化时调用 applyLanguage() 实时应用新语言。但该 applyLanguage() 仅执行 setLanguage() 与 translateDocument()。translateDocument() 只处理带 data-i18n 的静态元素;而 loadPet() 通过 validation.innerHTML = ... + t(...) 写入的宠物校验信息(如「尺寸正确」「未使用格完全透明」等)是在加载时烘焙的译文,不带有 data-i18n,切换语言后不会刷新。此外,settings.ts 的 applyLanguage() 会调用 sync_native_i18n 同步原生窗口标题等,而 pet-debug 的 applyLanguage() 未调用,因此调试窗口的原生标题(Agent Cat Animation Tester)也不会更新。结果是打开调试窗口时切换语言,会出现静态文案已翻译、而 #debug-validation 校验块与原生标题仍停留在旧语言的中英文混杂状态,与本次“保留运行时状态并重新翻译”的目标不一致。
Problem code:
Changed code at src/pet-debug.ts:18-22
Recommendation:
在 pet-debug.ts 的 applyLanguage() 中补齐动态内容与原生 i18n 的刷新:与 settings.ts 保持一致调用 void invoke("sync_native_i18n", { value: nativeMessages() }),并在 pets 已加载时对当前宠物重新执行 loadPet(Number(select.value)) 以重渲染 #debug-validation 中的译文(或将这些文案改为 data-i18n 以复用 translateDocument)。
Suggested diff:
diff --git a/src/pet-debug.ts b/src/pet-debug.ts
--- a/src/pet-debug.ts
+++ b/src/pet-debug.ts
@@ function applyLanguage(config: AppConfig): void {
setLanguage(config.language);
translateDocument();
+ void invoke("sync_native_i18n", { value: nativeMessages() });
+ if (pets.length) void loadPet(Number(select.value));
}🤖 I have created a release *beep* *boop* --- ## [1.8.0](v1.7.0...v1.8.0) (2026-08-07) ### Features * **i18n:** add English and Chinese localization ([#44](#44)) ([1594d3e](1594d3e)) * **status:** allow dismissing live bubbles ([#42](#42)) ([5c91d5e](5c91d5e)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **新功能** * 支持英文和中文本地化。 * 支持关闭实时气泡显示。 * **文档** * 更新 1.8.0 版本变更记录,并补充上述功能说明。 * **版本更新** * 产品版本已升级至 1.8.0。 <!-- end of auto-generated comment: release notes by coderabbit.ai -->
背景
Agent Cat 目前的用户界面文案主要直接写在 HTML、TypeScript 和 Rust 原生菜单中,无法根据用户语言切换。这个 PR 引入一套轻量的 i18n 基础设施,首期支持英文和简体中文,并保持现有配置和多窗口行为兼容。
主要改动
校验与兼容性
验证
Summary by CodeRabbit
新功能
测试