Skip to content

refactor: Go service i18n#1575

Merged
MistEO merged 8 commits intov2from
refactor/go-i18n
Mar 24, 2026
Merged

refactor: Go service i18n#1575
MistEO merged 8 commits intov2from
refactor/go-i18n

Conversation

@MistEO
Copy link
Copy Markdown
Contributor

@MistEO MistEO commented Mar 24, 2026

Summary by Sourcery

引入可复用的 Go 服务 i18n 子系统,并迁移多个 agents 和 tasker 检查以使用集中管理的本地化消息和 HTML 模板。

Enhancements:

  • 重构 EssenceFilter、AutoStockpile、MapTracker、Resell、AutoEcoFarm、BatchAddFriends 和 VisitFriends 组件,用共享的 i18n API 和模板渲染替换硬编码字符串和内嵌 HTML。
  • 通过移除 EssenceFilter 内部的 i18n catalog,并将扩展规则的细节通过结构化字段对外表达,以供新的 i18n 层使用,从而简化 EssenceFilter 的匹配 API。
  • 在 Go 服务启动时初始化 i18n 层,包括对本地化列表分隔符和 HTML 模板辅助函数等工具的支持。

Build:

  • 更新 CI 打包脚本,从新的 locales 资源路径而不是旧的 misc 目录中获取应用图标。

Documentation:

  • 更新开发者文档和内部技能文档,以引用新的 assets/locales 目录结构,以及界面本地化文件的使用方式。

Chores:

  • 将本地化资源重组为新的 assets/locales 层级结构,将界面 JSON 与 Go 服务消息及 HTML 模板分离,并移除已废弃的内嵌 JSON/HTML 资源。
Original summary in English

Summary by Sourcery

Introduce a reusable Go service i18n subsystem and migrate multiple agents and tasker checks to use centralized localized messages and HTML templates.

Enhancements:

  • Refactor EssenceFilter, AutoStockpile, MapTracker, Resell, AutoEcoFarm, BatchAddFriends, and VisitFriends components to replace hardcoded strings and embedded HTML with the shared i18n API and template rendering.
  • Simplify EssenceFilter match API by removing its internal i18n catalog and expressing extension rule details through structured fields for use by the new i18n layer.
  • Initialize the Go-service i18n layer on startup, including support utilities like locale-aware list separators and HTML template helpers.

Build:

  • Update CI packaging scripts to source application icons from the new locales assets path instead of the legacy misc directory.

Documentation:

  • Update developer and internal skill documentation to reference the new assets/locales directory structure and usage for interface localization files.

Chores:

  • Reorganize localization assets into a new assets/locales hierarchy, separating interface JSON from Go-service messages and HTML templates, and remove obsolete embedded JSON/HTML resources.

新功能:

  • 为 Go 服务新增一个可复用的 i18n 包,包括语言检测、消息查找、列表分隔符、HTML 模板渲染以及 match-api 语言环境映射。

增强:

  • 重构多个代理组件(AutoStockpile、MapTracker、EssenceFilter、VisitFriends、Resell、AutoEcoFarm、BatchAddFriends、Tasker 警告检查器),使其使用新的 i18n API,而不是硬编码的中文字符串或内嵌 HTML。
  • 将 AutoStockpile 中的中止原因处理切换为使用共享的 i18n 目录,并移除自定义的内嵌 JSON 目录。
  • 在 Go 服务启动时初始化 i18n 子系统,使所有组件共享一致的本地化行为。
  • 调整文档以引用新的 locales 目录布局和更新后的 i18n 文件路径。

构建:

  • 更新 CI 打包脚本,从新的 locales 资源路径而非之前的 misc 目录加载应用图标。

文档:

  • 更新开发文档(中文和英文)以及内部技能文档,以反映用于界面本地化文件的新的 assets/locales 结构。

杂项:

  • 重新组织本地化资源,将界面语言环境 JSON 文件从 assets/misc/locales 移动到新的 assets/locales/interface 目录树中,并新增 assets/locales/go-service 目录树,用于 Go 端消息和 HTML 模板。
Original summary in English

Summary by Sourcery

引入可复用的 Go 服务 i18n 子系统,并迁移多个 agents 和 tasker 检查以使用集中管理的本地化消息和 HTML 模板。

Enhancements:

  • 重构 EssenceFilter、AutoStockpile、MapTracker、Resell、AutoEcoFarm、BatchAddFriends 和 VisitFriends 组件,用共享的 i18n API 和模板渲染替换硬编码字符串和内嵌 HTML。
  • 通过移除 EssenceFilter 内部的 i18n catalog,并将扩展规则的细节通过结构化字段对外表达,以供新的 i18n 层使用,从而简化 EssenceFilter 的匹配 API。
  • 在 Go 服务启动时初始化 i18n 层,包括对本地化列表分隔符和 HTML 模板辅助函数等工具的支持。

Build:

  • 更新 CI 打包脚本,从新的 locales 资源路径而不是旧的 misc 目录中获取应用图标。

Documentation:

  • 更新开发者文档和内部技能文档,以引用新的 assets/locales 目录结构,以及界面本地化文件的使用方式。

Chores:

  • 将本地化资源重组为新的 assets/locales 层级结构,将界面 JSON 与 Go 服务消息及 HTML 模板分离,并移除已废弃的内嵌 JSON/HTML 资源。
Original summary in English

Summary by Sourcery

Introduce a reusable Go service i18n subsystem and migrate multiple agents and tasker checks to use centralized localized messages and HTML templates.

Enhancements:

  • Refactor EssenceFilter, AutoStockpile, MapTracker, Resell, AutoEcoFarm, BatchAddFriends, and VisitFriends components to replace hardcoded strings and embedded HTML with the shared i18n API and template rendering.
  • Simplify EssenceFilter match API by removing its internal i18n catalog and expressing extension rule details through structured fields for use by the new i18n layer.
  • Initialize the Go-service i18n layer on startup, including support utilities like locale-aware list separators and HTML template helpers.

Build:

  • Update CI packaging scripts to source application icons from the new locales assets path instead of the legacy misc directory.

Documentation:

  • Update developer and internal skill documentation to reference the new assets/locales directory structure and usage for interface localization files.

Chores:

  • Reorganize localization assets into a new assets/locales hierarchy, separating interface JSON from Go-service messages and HTML templates, and remove obsolete embedded JSON/HTML resources.
Original summary in English

Summary by Sourcery

引入可复用的 Go 服务 i18n 子系统,并迁移多个 agents 和 tasker 检查以使用集中管理的本地化消息和 HTML 模板。

Enhancements:

  • 重构 EssenceFilter、AutoStockpile、MapTracker、Resell、AutoEcoFarm、BatchAddFriends 和 VisitFriends 组件,用共享的 i18n API 和模板渲染替换硬编码字符串和内嵌 HTML。
  • 通过移除 EssenceFilter 内部的 i18n catalog,并将扩展规则的细节通过结构化字段对外表达,以供新的 i18n 层使用,从而简化 EssenceFilter 的匹配 API。
  • 在 Go 服务启动时初始化 i18n 层,包括对本地化列表分隔符和 HTML 模板辅助函数等工具的支持。

Build:

  • 更新 CI 打包脚本,从新的 locales 资源路径而不是旧的 misc 目录中获取应用图标。

Documentation:

  • 更新开发者文档和内部技能文档,以引用新的 assets/locales 目录结构,以及界面本地化文件的使用方式。

Chores:

  • 将本地化资源重组为新的 assets/locales 层级结构,将界面 JSON 与 Go 服务消息及 HTML 模板分离,并移除已废弃的内嵌 JSON/HTML 资源。
Original summary in English

Summary by Sourcery

Introduce a reusable Go service i18n subsystem and migrate multiple agents and tasker checks to use centralized localized messages and HTML templates.

Enhancements:

  • Refactor EssenceFilter, AutoStockpile, MapTracker, Resell, AutoEcoFarm, BatchAddFriends, and VisitFriends components to replace hardcoded strings and embedded HTML with the shared i18n API and template rendering.
  • Simplify EssenceFilter match API by removing its internal i18n catalog and expressing extension rule details through structured fields for use by the new i18n layer.
  • Initialize the Go-service i18n layer on startup, including support utilities like locale-aware list separators and HTML template helpers.

Build:

  • Update CI packaging scripts to source application icons from the new locales assets path instead of the legacy misc directory.

Documentation:

  • Update developer and internal skill documentation to reference the new assets/locales directory structure and usage for interface localization files.

Chores:

  • Reorganize localization assets into a new assets/locales hierarchy, separating interface JSON from Go-service messages and HTML templates, and remove obsolete embedded JSON/HTML resources.

新功能:

  • 为 Go 服务新增一个可复用的 i18n 包,包括语言检测、消息查找、列表分隔符、HTML 模板渲染以及 match-api 语言环境映射。

增强:

  • 重构多个代理组件(AutoStockpile、MapTracker、EssenceFilter、VisitFriends、Resell、AutoEcoFarm、BatchAddFriends、Tasker 警告检查器),使其使用新的 i18n API,而不是硬编码的中文字符串或内嵌 HTML。
  • 将 AutoStockpile 中的中止原因处理切换为使用共享的 i18n 目录,并移除自定义的内嵌 JSON 目录。
  • 在 Go 服务启动时初始化 i18n 子系统,使所有组件共享一致的本地化行为。
  • 调整文档以引用新的 locales 目录布局和更新后的 i18n 文件路径。

构建:

  • 更新 CI 打包脚本,从新的 locales 资源路径而非之前的 misc 目录加载应用图标。

文档:

  • 更新开发文档(中文和英文)以及内部技能文档,以反映用于界面本地化文件的新的 assets/locales 结构。

杂项:

  • 重新组织本地化资源,将界面语言环境 JSON 文件从 assets/misc/locales 移动到新的 assets/locales/interface 目录树中,并新增 assets/locales/go-service 目录树,用于 Go 端消息和 HTML 模板。
Original summary in English

Summary by Sourcery

引入可复用的 Go 服务 i18n 子系统,并迁移多个 agents 和 tasker 检查以使用集中管理的本地化消息和 HTML 模板。

Enhancements:

  • 重构 EssenceFilter、AutoStockpile、MapTracker、Resell、AutoEcoFarm、BatchAddFriends 和 VisitFriends 组件,用共享的 i18n API 和模板渲染替换硬编码字符串和内嵌 HTML。
  • 通过移除 EssenceFilter 内部的 i18n catalog,并将扩展规则的细节通过结构化字段对外表达,以供新的 i18n 层使用,从而简化 EssenceFilter 的匹配 API。
  • 在 Go 服务启动时初始化 i18n 层,包括对本地化列表分隔符和 HTML 模板辅助函数等工具的支持。

Build:

  • 更新 CI 打包脚本,从新的 locales 资源路径而不是旧的 misc 目录中获取应用图标。

Documentation:

  • 更新开发者文档和内部技能文档,以引用新的 assets/locales 目录结构,以及界面本地化文件的使用方式。

Chores:

  • 将本地化资源重组为新的 assets/locales 层级结构,将界面 JSON 与 Go 服务消息及 HTML 模板分离,并移除已废弃的内嵌 JSON/HTML 资源。
Original summary in English

Summary by Sourcery

Introduce a reusable Go service i18n subsystem and migrate multiple agents and tasker checks to use centralized localized messages and HTML templates.

Enhancements:

  • Refactor EssenceFilter, AutoStockpile, MapTracker, Resell, AutoEcoFarm, BatchAddFriends, and VisitFriends components to replace hardcoded strings and embedded HTML with the shared i18n API and template rendering.
  • Simplify EssenceFilter match API by removing its internal i18n catalog and expressing extension rule details through structured fields for use by the new i18n layer.
  • Initialize the Go-service i18n layer on startup, including support utilities like locale-aware list separators and HTML template helpers.

Build:

  • Update CI packaging scripts to source application icons from the new locales assets path instead of the legacy misc directory.

Documentation:

  • Update developer and internal skill documentation to reference the new assets/locales directory structure and usage for interface localization files.

Chores:

  • Reorganize localization assets into a new assets/locales hierarchy, separating interface JSON from Go-service messages and HTML templates, and remove obsolete embedded JSON/HTML resources.

Copilot AI review requested due to automatic review settings March 24, 2026 09:51
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - 我发现了 4 个问题,并给出了一些整体性的反馈:

  • 新的 i18n.ToMatchAPILocale / getLocaleFromState 逻辑忽略了 RunState.InputLanguage,只使用全局 PI 语言,这会改变 essencefilter 的行为,并可能让之前依赖「按运行设置语言」的用户感到意外;建议要么保留之前的覆盖逻辑,要么在文档中明确说明 matchapi locale 现在与 PI_CLIENT_LANGUAGE 绑定。
  • i18n.Init 函数通过 os.Getwd() 加相对路径来构建 localeDir,如果二进制从不同的工作目录启动,这里就会失败;建议基于可执行文件路径或可配置的根路径,而不是进程当前工作目录(CWD)。
  • 移除 InitAbortReasonCatalog 并将 abort reason 文案查找改为 i18n.T 之后,就失去了对所有 AbortReason 值在启动时进行「非空消息」校验的能力;如果这个保证仍然重要,建议加一个轻量级的启动检查,遍历已知的原因并在键缺失时记录日志或直接报错。
给 AI Agents 的提示词
请根据这次代码评审中的评论进行修改:

## 总体评论
- 新的 `i18n.ToMatchAPILocale` / `getLocaleFromState` 逻辑忽略了 `RunState.InputLanguage`,只使用全局 PI 语言,这会改变 essencefilter 的行为,并可能让之前依赖「按运行设置语言」的用户感到意外;建议要么保留之前的覆盖逻辑,要么在文档中明确说明 matchapi locale 现在与 `PI_CLIENT_LANGUAGE` 绑定。
- `i18n.Init` 函数通过 `os.Getwd()` 加相对路径来构建 `localeDir`,如果二进制从不同的工作目录启动,这里就会失败;建议基于可执行文件路径或可配置的根路径,而不是进程当前工作目录(CWD)。
- 移除 `InitAbortReasonCatalog` 并将 abort reason 文案查找改为 `i18n.T` 之后,就失去了对所有 `AbortReason` 值在启动时进行「非空消息」校验的能力;如果这个保证仍然重要,建议加一个轻量级的启动检查,遍历已知的原因并在键缺失时记录日志或直接报错。

## 单独评论

### 评论 1
<location path="agent/go-service/pkg/i18n/i18n.go" line_range="23" />
<code_context>
+
+	DefaultLang    = LangZhCN
+	envKey         = "PI_CLIENT_LANGUAGE"
+	localeRelDir   = "misc/locales/go-service"
+	fileRefPrefix  = "@"
+)
</code_context>
<issue_to_address>
**issue (bug_risk):** Locale 目录常量与新的资源路径不匹配,导致永远无法找到 HTML/JSON 文件。

新的 locale HTML/JSON 文件位于 `assets/misc/locales/go-service/...` 下,但 `localeRelDir` 仍然是`
</issue_to_address>

### 评论 2
<location path="agent/go-service/essencefilter/integration.go" line_range="23-24" />
<code_context>
-		return matchapi.LocaleCN
-	}
-	return loc
+func getLocaleFromState(_ *RunState) string {
+	return i18n.ToMatchAPILocale()
 }

</code_context>
<issue_to_address>
**question (bug_risk):** Essencefilter 的 locale 不再从 RunState 推导,现会忽略按运行设置的输入语言。

之前的 `getLocaleFromState` 会通过 `matchapi.NormalizeInputLocale` 使用 `st.InputLanguage`,因此 essencefilter 可以使用独立于全局 UI 语言的 locale。现在它忽略了 `RunState`,始终通过 `i18n.ToMatchAPILocale()` 使用 `PI_CLIENT_LANGUAGE`,这会破坏按运行或按过滤器配置语言的能力,并导致错误的 matchapi locale。如果目标就是始终使用 `PI_CLIENT_LANGUAGE`,建议移除 `st *RunState` 参数,或者在文档中明确说明不再支持按运行覆盖。
</issue_to_address>

### 评论 3
<location path="agent/go-service/pkg/i18n/i18n.go" line_range="37-44" />
<code_context>
+)
+
+func Init() {
+	lang := strings.ToLower(strings.TrimSpace(os.Getenv(envKey)))
+	if lang == "" {
+		lang = DefaultLang
+	}
+	lang = NormalizeLang(lang)
+
+	cwd, _ := os.Getwd()
</code_context>
<issue_to_address>
**suggestion:** 语言标准化没有处理常见变体(例如 `zh-CN`),会静默回退到默认语言。

环境变量中像 `zh-CN``en-US` 这样的值很常见。由于 `NormalizeLang` 只做小写和 trim,`zh-CN` 会变成 `zh-cn`,这与允许的常量(`zh_cn``en_us` 等)不匹配,因此这些值会被视作未知并静默回退到 `DefaultLang`。建议把 `-` 规范化为 `_` 并/或显式支持 `xx-YY` 形式,或者至少在提供了无法识别的语言时输出日志,以便更容易发现配置错误。

```suggestion
func Init() {
	rawEnv := os.Getenv(envKey)
	lang := strings.ToLower(strings.TrimSpace(rawEnv))
	if lang == "" {
		lang = DefaultLang
	}
	// Normalize common variants like "zh-CN" or "en-US" to "zh_cn" / "en_us"
	langCandidate := strings.ReplaceAll(lang, "-", "_")
	normalized := NormalizeLang(langCandidate)

	// If a non-empty, non-default value was provided but NormalizeLang
	// mapped it back to DefaultLang, emit a warning to surface misconfigurations.
	if rawEnv != "" && langCandidate != "" && langCandidate != DefaultLang && normalized == DefaultLang {
		log.Warn().
			Str("env_key", envKey).
			Str("env_value", rawEnv).
			Str("normalized_value", langCandidate).
			Str("effective_lang", normalized).
			Msg("unrecognized language configured in environment, falling back to default")
	}

	lang = normalized

	cwd, _ := os.Getwd()
```
</issue_to_address>

### 评论 4
<location path="agent/go-service/autostockpile/abort_reason.go" line_range="19-23" />
<code_context>
-// LookupAbortReasonZHCN 返回指定原因键对应的中文文案。
-func LookupAbortReasonZHCN(reason AbortReason) (string, error) {
+// LookupAbortReason 返回指定原因键对应的本地化文案。
+func LookupAbortReason(reason AbortReason) (string, error) {
 	if err := ValidateAbortReason(reason); err != nil {
 		return "", err
 	}
-
-	catalog, err := abortReasonCatalogOnce()
-	if err != nil {
-		return "", err
-	}
-
-	message, ok := catalog.ZHCN[reason]
-	if !ok {
-		return "", fmt.Errorf("abort reason %q missing zh_cn message", reason)
-	}
-
-	return message, nil
+	return i18n.T("autostockpile.abort." + string(reason)), nil
 }
</code_context>
<issue_to_address>
**question (bug_risk):** Abort reason 查找不再校验翻译是否存在,并且总是返回 nil 错误。

之前会在启动时对每个 abort reason 做校验,以保证存在非空的 `zh_cn` 条目,并在缺失时通过查找错误暴露问题。现在该函数总是返回 `nil``i18n.T(...)`,而 `i18n.T(...)` 在翻译缺失时会静默回退到 key 本身。这移除了翻译与 `AbortReason` 漂移时的早期失败信号,也让调用方(例如 `lookupAbortReasonText`)无法区分「缺失翻译」与「合法值」。如果仍然希望保留这种行为,建议要么在启动时做一次验证(例如对所有 reason 断言 `i18n.T(...) != key`),要么在 `T` 回退到 key 时,让 `LookupAbortReason` 返回一个错误。
</issue_to_address>

Sourcery 对开源项目免费 —— 如果你觉得这些评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据这些反馈改进之后的评审。
Original comment in English

Hey - I've found 4 issues, and left some high level feedback:

  • The new i18n.ToMatchAPILocale / getLocaleFromState logic ignores RunState.InputLanguage and uses only the global PI language, which changes behavior for essencefilter and may surprise users who previously relied on per-run language selection; consider either preserving the old override semantics or documenting that matchapi locale is now tied to PI_CLIENT_LANGUAGE.
  • The i18n.Init function builds localeDir from os.Getwd() and a relative path, which will fail if the binary is started from a different working directory; consider basing this on the executable path or a configurable root instead of the process CWD.
  • By removing InitAbortReasonCatalog and switching abort reason text lookup to i18n.T, you’ve lost the upfront validation that all AbortReason values have non-empty messages; if that guarantee is still important, consider adding a lightweight startup check that scans known reasons and logs or errors on missing keys.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `i18n.ToMatchAPILocale` / `getLocaleFromState` logic ignores `RunState.InputLanguage` and uses only the global PI language, which changes behavior for essencefilter and may surprise users who previously relied on per-run language selection; consider either preserving the old override semantics or documenting that matchapi locale is now tied to `PI_CLIENT_LANGUAGE`.
- The `i18n.Init` function builds `localeDir` from `os.Getwd()` and a relative path, which will fail if the binary is started from a different working directory; consider basing this on the executable path or a configurable root instead of the process CWD.
- By removing `InitAbortReasonCatalog` and switching abort reason text lookup to `i18n.T`, you’ve lost the upfront validation that all `AbortReason` values have non-empty messages; if that guarantee is still important, consider adding a lightweight startup check that scans known reasons and logs or errors on missing keys.

## Individual Comments

### Comment 1
<location path="agent/go-service/pkg/i18n/i18n.go" line_range="23" />
<code_context>
+
+	DefaultLang    = LangZhCN
+	envKey         = "PI_CLIENT_LANGUAGE"
+	localeRelDir   = "misc/locales/go-service"
+	fileRefPrefix  = "@"
+)
</code_context>
<issue_to_address>
**issue (bug_risk):** Locale directory constant does not match the new assets path and will never find the HTML/JSON files.

The new locale HTML/JSON files live under `assets/misc/locales/go-service/...`, but `localeRelDir` is still `
</issue_to_address>

### Comment 2
<location path="agent/go-service/essencefilter/integration.go" line_range="23-24" />
<code_context>
-		return matchapi.LocaleCN
-	}
-	return loc
+func getLocaleFromState(_ *RunState) string {
+	return i18n.ToMatchAPILocale()
 }

</code_context>
<issue_to_address>
**question (bug_risk):** Essencefilter locale is no longer derived from RunState and now ignores per-run input language settings.

Previously `getLocaleFromState` used `st.InputLanguage` via `matchapi.NormalizeInputLocale`, so essencefilter could use a locale independent of the global UI language. Now it ignores `RunState` and always uses `PI_CLIENT_LANGUAGE` via `i18n.ToMatchAPILocale()`, which can break per-run or per-filter language configuration and lead to incorrect matchapi locales. If the goal is to always use `PI_CLIENT_LANGUAGE`, consider removing the `st *RunState` parameter or explicitly documenting that per-run overrides are no longer supported.
</issue_to_address>

### Comment 3
<location path="agent/go-service/pkg/i18n/i18n.go" line_range="37-44" />
<code_context>
+)
+
+func Init() {
+	lang := strings.ToLower(strings.TrimSpace(os.Getenv(envKey)))
+	if lang == "" {
+		lang = DefaultLang
+	}
+	lang = NormalizeLang(lang)
+
+	cwd, _ := os.Getwd()
</code_context>
<issue_to_address>
**suggestion:** Language normalization does not handle common variants like `zh-CN`, which will silently fall back to the default language.

Environment values like `zh-CN` and `en-US` are common. Since `NormalizeLang` only lowercases and trims, `zh-CN` becomes `zh-cn`, which doesn’t match the allowed constants (`zh_cn`, `en_us`, etc.), so these values are treated as unknown and silently fall back to `DefaultLang`. Please normalize `-` to `_` and/or support `xx-YY` forms explicitly, or at least log when an unrecognized language is provided to make misconfiguration easier to detect.

```suggestion
func Init() {
	rawEnv := os.Getenv(envKey)
	lang := strings.ToLower(strings.TrimSpace(rawEnv))
	if lang == "" {
		lang = DefaultLang
	}
	// Normalize common variants like "zh-CN" or "en-US" to "zh_cn" / "en_us"
	langCandidate := strings.ReplaceAll(lang, "-", "_")
	normalized := NormalizeLang(langCandidate)

	// If a non-empty, non-default value was provided but NormalizeLang
	// mapped it back to DefaultLang, emit a warning to surface misconfigurations.
	if rawEnv != "" && langCandidate != "" && langCandidate != DefaultLang && normalized == DefaultLang {
		log.Warn().
			Str("env_key", envKey).
			Str("env_value", rawEnv).
			Str("normalized_value", langCandidate).
			Str("effective_lang", normalized).
			Msg("unrecognized language configured in environment, falling back to default")
	}

	lang = normalized

	cwd, _ := os.Getwd()
```
</issue_to_address>

### Comment 4
<location path="agent/go-service/autostockpile/abort_reason.go" line_range="19-23" />
<code_context>
-// LookupAbortReasonZHCN 返回指定原因键对应的中文文案。
-func LookupAbortReasonZHCN(reason AbortReason) (string, error) {
+// LookupAbortReason 返回指定原因键对应的本地化文案。
+func LookupAbortReason(reason AbortReason) (string, error) {
 	if err := ValidateAbortReason(reason); err != nil {
 		return "", err
 	}
-
-	catalog, err := abortReasonCatalogOnce()
-	if err != nil {
-		return "", err
-	}
-
-	message, ok := catalog.ZHCN[reason]
-	if !ok {
-		return "", fmt.Errorf("abort reason %q missing zh_cn message", reason)
-	}
-
-	return message, nil
+	return i18n.T("autostockpile.abort." + string(reason)), nil
 }
</code_context>
<issue_to_address>
**question (bug_risk):** Abort reason lookups no longer validate translation presence and always return nil error.

Previously, every abort reason was validated at startup to ensure a non-empty `zh_cn` entry, and lookup errors surfaced when a reason was missing. Now the function always returns `nil` and `i18n.T(...)`, which silently falls back to the key if the translation is absent. This removes the early-fail signal when translations drift from `AbortReason` and prevents callers (e.g. `lookupAbortReasonText`) from distinguishing a missing translation from a valid value. If that behavior is still desired, consider either a startup validation pass (e.g. asserting `i18n.T(...) != key` for all reasons) or returning an error from `LookupAbortReason` when `T` falls back to the key.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +37 to +44
func Init() {
lang := strings.ToLower(strings.TrimSpace(os.Getenv(envKey)))
if lang == "" {
lang = DefaultLang
}
lang = NormalizeLang(lang)

cwd, _ := os.Getwd()
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.

suggestion: 语言标准化没有处理常见变体(例如 zh-CN),会静默回退到默认语言。

环境变量中像 zh-CNen-US 这样的值很常见。由于 NormalizeLang 只做小写和 trim,zh-CN 会变成 zh-cn,这与允许的常量(zh_cnen_us 等)不匹配,因此这些值会被视作未知并静默回退到 DefaultLang。建议把 - 规范化为 _ 并/或显式支持 xx-YY 形式,或者至少在提供了无法识别的语言时输出日志,以便更容易发现配置错误。

Suggested change
func Init() {
lang := strings.ToLower(strings.TrimSpace(os.Getenv(envKey)))
if lang == "" {
lang = DefaultLang
}
lang = NormalizeLang(lang)
cwd, _ := os.Getwd()
func Init() {
rawEnv := os.Getenv(envKey)
lang := strings.ToLower(strings.TrimSpace(rawEnv))
if lang == "" {
lang = DefaultLang
}
// Normalize common variants like "zh-CN" or "en-US" to "zh_cn" / "en_us"
langCandidate := strings.ReplaceAll(lang, "-", "_")
normalized := NormalizeLang(langCandidate)
// If a non-empty, non-default value was provided but NormalizeLang
// mapped it back to DefaultLang, emit a warning to surface misconfigurations.
if rawEnv != "" && langCandidate != "" && langCandidate != DefaultLang && normalized == DefaultLang {
log.Warn().
Str("env_key", envKey).
Str("env_value", rawEnv).
Str("normalized_value", langCandidate).
Str("effective_lang", normalized).
Msg("unrecognized language configured in environment, falling back to default")
}
lang = normalized
cwd, _ := os.Getwd()
Original comment in English

suggestion: Language normalization does not handle common variants like zh-CN, which will silently fall back to the default language.

Environment values like zh-CN and en-US are common. Since NormalizeLang only lowercases and trims, zh-CN becomes zh-cn, which doesn’t match the allowed constants (zh_cn, en_us, etc.), so these values are treated as unknown and silently fall back to DefaultLang. Please normalize - to _ and/or support xx-YY forms explicitly, or at least log when an unrecognized language is provided to make misconfiguration easier to detect.

Suggested change
func Init() {
lang := strings.ToLower(strings.TrimSpace(os.Getenv(envKey)))
if lang == "" {
lang = DefaultLang
}
lang = NormalizeLang(lang)
cwd, _ := os.Getwd()
func Init() {
rawEnv := os.Getenv(envKey)
lang := strings.ToLower(strings.TrimSpace(rawEnv))
if lang == "" {
lang = DefaultLang
}
// Normalize common variants like "zh-CN" or "en-US" to "zh_cn" / "en_us"
langCandidate := strings.ReplaceAll(lang, "-", "_")
normalized := NormalizeLang(langCandidate)
// If a non-empty, non-default value was provided but NormalizeLang
// mapped it back to DefaultLang, emit a warning to surface misconfigurations.
if rawEnv != "" && langCandidate != "" && langCandidate != DefaultLang && normalized == DefaultLang {
log.Warn().
Str("env_key", envKey).
Str("env_value", rawEnv).
Str("normalized_value", langCandidate).
Str("effective_lang", normalized).
Msg("unrecognized language configured in environment, falling back to default")
}
lang = normalized
cwd, _ := os.Getwd()

Comment on lines +19 to +23
func LookupAbortReason(reason AbortReason) (string, error) {
if err := ValidateAbortReason(reason); err != nil {
return "", err
}

catalog, err := abortReasonCatalogOnce()
if err != nil {
return "", err
}

message, ok := catalog.ZHCN[reason]
if !ok {
return "", fmt.Errorf("abort reason %q missing zh_cn message", reason)
}

return message, nil
return i18n.T("autostockpile.abort." + string(reason)), nil
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.

question (bug_risk): Abort reason 查找不再校验翻译是否存在,并且总是返回 nil 错误。

之前会在启动时对每个 abort reason 做校验,以保证存在非空的 zh_cn 条目,并在缺失时通过查找错误暴露问题。现在该函数总是返回 nili18n.T(...),而 i18n.T(...) 在翻译缺失时会静默回退到 key 本身。这移除了翻译与 AbortReason 漂移时的早期失败信号,也让调用方(例如 lookupAbortReasonText)无法区分「缺失翻译」与「合法值」。如果仍然希望保留这种行为,建议要么在启动时做一次验证(例如对所有 reason 断言 i18n.T(...) != key),要么在 T 回退到 key 时,让 LookupAbortReason 返回一个错误。

Original comment in English

question (bug_risk): Abort reason lookups no longer validate translation presence and always return nil error.

Previously, every abort reason was validated at startup to ensure a non-empty zh_cn entry, and lookup errors surfaced when a reason was missing. Now the function always returns nil and i18n.T(...), which silently falls back to the key if the translation is absent. This removes the early-fail signal when translations drift from AbortReason and prevents callers (e.g. lookupAbortReasonText) from distinguishing a missing translation from a valid value. If that behavior is still desired, consider either a startup validation pass (e.g. asserting i18n.T(...) != key for all reasons) or returning an error from LookupAbortReason when T falls back to the key.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

该 PR 为 Go service 引入一套轻量 i18n 机制,将原先硬编码/内嵌(embed)的提示文案与 HTML 消息抽离到 assets/misc/locales/go-service/,并改造多个功能模块在运行时按语言加载对应文本/HTML。

Changes:

  • 新增 agent/go-service/pkg/i18n,基于 PI_CLIENT_LANGUAGE 加载各语言 JSON,并支持 @xxx.html 文件引用渲染。
  • 新增/补齐 go-service 的多语言 locale JSON(zh_cn/zh_tw/en_us/ja_jp/ko_kr)与若干 HTML 模板文件。
  • 改造 resell / visitfriends / batchaddfriends / autostockpile / map-tracker / taskersink / essencefilter 等模块使用 i18n 输出用户可见信息,并移除部分 embed 资源。

Reviewed changes

Copilot reviewed 62 out of 65 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
assets/misc/locales/go-service/zh_tw.json 新增 go-service 繁中翻译 key/value(含部分 HTML 文件引用)
assets/misc/locales/go-service/zh_cn.json 新增 go-service 简中翻译 key/value(含部分 HTML 文件引用)
assets/misc/locales/go-service/ko_kr.json 新增 go-service 韩语翻译 key/value(含部分 HTML 文件引用)
assets/misc/locales/go-service/ja_jp.json 新增 go-service 日语翻译 key/value(含部分 HTML 文件引用)
assets/misc/locales/go-service/en_us.json 新增 go-service 英文翻译 key/value(含部分 HTML 文件引用)
assets/misc/locales/go-service/process-warning-zh_tw.html 新增“黑名单进程”警告 HTML(繁中)
assets/misc/locales/go-service/process-warning-zh_cn.html 新增“黑名单进程”警告 HTML(简中)
assets/misc/locales/go-service/process-warning-ko_kr.html 新增“黑名单进程”警告 HTML(韩语)
assets/misc/locales/go-service/process-warning-ja_jp.html 新增“黑名单进程”警告 HTML(日语)
assets/misc/locales/go-service/process-warning-en_us.html 新增“黑名单进程”警告 HTML(英文)
assets/misc/locales/go-service/navigation-moving-zh_tw.html 新增 map-tracker 导航移动提示 HTML(繁中)
assets/misc/locales/go-service/navigation-moving-zh_cn.html 新增 map-tracker 导航移动提示 HTML(简中)
assets/misc/locales/go-service/navigation-moving-ko_kr.html 新增 map-tracker 导航移动提示 HTML(韩语)
assets/misc/locales/go-service/navigation-moving-ja_jp.html 新增 map-tracker 导航移动提示 HTML(日语)
assets/misc/locales/go-service/navigation-moving-en_us.html 新增 map-tracker 导航移动提示 HTML(英文)
assets/misc/locales/go-service/navigation-finished-zh_tw.html 新增 map-tracker 导航完成提示 HTML(繁中)
assets/misc/locales/go-service/navigation-finished-zh_cn.html 新增 map-tracker 导航完成提示 HTML(简中)
assets/misc/locales/go-service/navigation-finished-ko_kr.html 新增 map-tracker 导航完成提示 HTML(韩语)
assets/misc/locales/go-service/navigation-finished-ja_jp.html 新增 map-tracker 导航完成提示 HTML(日语)
assets/misc/locales/go-service/navigation-finished-en_us.html 新增 map-tracker 导航完成提示 HTML(英文)
assets/misc/locales/go-service/inference-finished-zh_tw.html 新增 map-tracker 推理完成提示 HTML(繁中)
assets/misc/locales/go-service/inference-finished-zh_cn.html 新增 map-tracker 推理完成提示 HTML(简中)
assets/misc/locales/go-service/inference-finished-ko_kr.html 新增 map-tracker 推理完成提示 HTML(韩语)
assets/misc/locales/go-service/inference-finished-ja_jp.html 新增 map-tracker 推理完成提示 HTML(日语)
assets/misc/locales/go-service/inference-finished-en_us.html 新增 map-tracker 推理完成提示 HTML(英文)
assets/misc/locales/go-service/inference-failed-zh_tw.html 新增 map-tracker 推理失败提示 HTML(繁中)
assets/misc/locales/go-service/inference-failed-zh_cn.html 新增 map-tracker 推理失败提示 HTML(简中)
assets/misc/locales/go-service/inference-failed-ko_kr.html 新增 map-tracker 推理失败提示 HTML(韩语)
assets/misc/locales/go-service/inference-failed-ja_jp.html 新增 map-tracker 推理失败提示 HTML(日语)
assets/misc/locales/go-service/inference-failed-en_us.html 新增 map-tracker 推理失败提示 HTML(英文)
assets/misc/locales/go-service/hdr-warning-zh_tw.html 新增 HDR 警告 HTML(繁中)
assets/misc/locales/go-service/hdr-warning-zh_cn.html 新增 HDR 警告 HTML(简中)
assets/misc/locales/go-service/hdr-warning-ko_kr.html 新增 HDR 警告 HTML(韩语)
assets/misc/locales/go-service/hdr-warning-ja_jp.html 新增 HDR 警告 HTML(日语)
assets/misc/locales/go-service/hdr-warning-en_us.html 新增 HDR 警告 HTML(英文)
assets/misc/locales/go-service/emergency-stop-zh_tw.html 新增紧急停止提示 HTML(繁中)
assets/misc/locales/go-service/emergency-stop-zh_cn.html 新增紧急停止提示 HTML(简中)
assets/misc/locales/go-service/emergency-stop-ko_kr.html 新增紧急停止提示 HTML(韩语)
assets/misc/locales/go-service/emergency-stop-ja_jp.html 新增紧急停止提示 HTML(日语)
assets/misc/locales/go-service/emergency-stop-en_us.html 新增紧急停止提示 HTML(英文)
assets/misc/locales/go-service/aspect-ratio-warning-zh_tw.html 新增分辨率比例警告 HTML(繁中)
assets/misc/locales/go-service/aspect-ratio-warning-zh_cn.html 调整分辨率比例警告 HTML(简中,行号/格式微调)
assets/misc/locales/go-service/aspect-ratio-warning-ko_kr.html 新增分辨率比例警告 HTML(韩语)
assets/misc/locales/go-service/aspect-ratio-warning-ja_jp.html 新增分辨率比例警告 HTML(日语)
assets/misc/locales/go-service/aspect-ratio-warning-en_us.html 新增分辨率比例警告 HTML(英文)
agent/go-service/visitfriends/visitfriends.go 将节点提示文案改为 i18n key + 组合输出
agent/go-service/taskersink/processcheck/checker.go 用 i18n.TF 替代 embed HTML 输出黑名单进程警告
agent/go-service/taskersink/hdrcheck/checker.go 用 i18n.TF 替代 embed HTML 输出 HDR 警告
agent/go-service/taskersink/aspectratio/checker.go 用 i18n.TF 替代 embed HTML 输出比例不匹配警告
agent/go-service/resell/decide.go 将购买建议/提示改为 i18n 文案
agent/go-service/pkg/i18n/i18n.go 新增 i18n 初始化、文本与文件引用渲染(T/TF)等实现
agent/go-service/map-tracker/move.go 移除 embed HTML,改为按语言读取并渲染导航 HTML
agent/go-service/map-tracker/infer.go 移除 embed HTML,改为按语言读取并渲染推理结果 HTML
agent/go-service/main.go 启动时初始化 i18n
agent/go-service/essencefilter/ui.go UI 输出文案与拼接分隔符改为 i18n
agent/go-service/essencefilter/options.go 稀有度/列表拼接等文案改为 i18n
agent/go-service/essencefilter/integration.go matchapi locale 选择改为从 i18n 推导
agent/go-service/batchaddfriends/batchaddfriends.go 进度/结果提示文案改为 i18n
agent/go-service/autostockpile/selector.go 多处 focus 文案与模式文本改为 i18n,并切换 abort reason lookup
agent/go-service/autostockpile/register.go 移除 abort reason catalog 初始化流程
agent/go-service/autostockpile/recognition.go 识别完成提示改为 i18n
agent/go-service/autostockpile/quantity.go quantity 决策原因/数量文本改为 i18n
agent/go-service/autostockpile/abort_reason.json 删除原先仅 zh_cn 的 abort reason 文案表
agent/go-service/autostockpile/abort_reason.go abort reason lookup 改为使用 i18n key

Comment on lines +107 to +109
if !ok {
return key
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

i18n.T/TF 在 key 不存在时直接返回 key 字符串(这里的 return key)。这样当非默认语言缺少个别翻译时,最终 UI/日志会直接暴露内部 key(如 maptracker.xxx),而不是自动回退到默认语言。建议在加载时合并 DefaultLang 的 messages(默认语言作为 base,当前语言覆盖),或在查找失败时按 DefaultLang 再查一次。

Copilot uses AI. Check for mistakes.
Comment on lines +197 to +202
data, err := os.ReadFile(path)
if err != nil && lang != DefaultLang {
fallback := resolveFilePath(fileName, DefaultLang)
data, err = os.ReadFile(fallback)
path = fallback
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

readFileContent 在读取当前语言文件失败后会回退到 DefaultLang,并把 cache key 设为 fallback 的实际路径;但下一次同样请求仍会先查当前语言路径缓存(查不到)再触发一次失败 + 回退读取,导致重复 I/O。建议在发生回退时,同时把“原始请求路径”的缓存也填充为 fallback 内容(或用 fileName+lang 作为缓存 key)。

Copilot uses AI. Check for mistakes.
}

return message, nil
return i18n.T("autostockpile.abort." + string(reason)), nil
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

LookupAbortReason 现在直接返回 i18n.T(...),即使对应翻译 key 缺失也会返回 key 本身且不报错;这会让上层继续把 autostockpile.abort.Xxx 这种内部 key 展示给用户,同时这里的 error 返回值也基本失去意义。建议在这里检测 i18n 是否命中(例如返回值等于 key 时认为缺失)并返回明确错误,或至少回退到 DefaultLang 的文案。

Suggested change
return i18n.T("autostockpile.abort." + string(reason)), nil
key := "autostockpile.abort." + string(reason)
translated := i18n.T(key)
if translated == key {
return "", fmt.Errorf("missing i18n translation for key %q", key)
}
return translated, nil

Copilot uses AI. Check for mistakes.
Comment on lines 49 to +51
log.Info().Msgf("[Resell]配额溢出:建议购买%d件,推荐第%d行第%d列(利润:%d)",
overflowAmount, showMaxRecord.Row, showMaxRecord.Col, showMaxRecord.Profit)
message := fmt.Sprintf("⚠️ 配额溢出提醒\n剩余配额明天将超出上限,建议购买%d件商品\n推荐购买: 第%d行第%d列 (最高利润: %d)",
overflowAmount, showMaxRecord.Row, showMaxRecord.Col, showMaxRecord.Profit)
maafocus.NodeActionStarting(ctx, message)
maafocus.NodeActionStarting(ctx, i18n.T("resell.quota_overflow", overflowAmount, showMaxRecord.Row, showMaxRecord.Col, showMaxRecord.Profit))
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

这里继续使用了 Msgf 且把组件前缀(如 [Resell])拼进 Msg 文本里,不符合仓库对 go-service 的 zerolog 约定(应使用链式字段承载上下文,Msg 保持简短)。建议改为 log.Info().Str("component","resell").Int(...).Msg("quota overflow") 这类结构化日志,避免 Msgf/字符串拼接。参考:.github/instructions/go-service.instructions.md 中 LogStyleZerolog 段落。

Copilot uses AI. Check for mistakes.
@MistEO MistEO requested a review from Constrat March 24, 2026 10:04
@MistEO
Copy link
Copy Markdown
Contributor Author

MistEO commented Mar 24, 2026

@sourcery-ai summary

@MistEO MistEO requested a review from Joe-Bao March 24, 2026 11:26
@MistEO
Copy link
Copy Markdown
Contributor Author

MistEO commented Mar 24, 2026

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - 我发现了 3 个问题,并留下了一些整体层面的反馈:

  • EssenceFilter 之前是通过 getLocaleFromStateRunState.InputLanguage 推导出 locale 的,但现在所有用户可见的字符串都走全局的 i18n 语言;请确认取消按运行 / 按任务选择 locale 是有意为之,并且不会导致混合语言场景的回归。
  • 旧的 autostockpile abort_reason.json 加载逻辑会校验每个 AbortReason 是否有非空 message 且没有多余字段;现在查找直接走 i18n.T,建议加一个小的启动检查,遍历 knownAbortReasons 并验证对应的 autostockpile.abort.* key 是否存在,以保留原先的安全网。
  • 新的 pkg/i18n 中的 HTML 模板注册表是手动定义 key 的(例如 essencefilter.no_match_discardmaptracker.navigation_moving);为避免静默降级为返回 key 字符串本身,可能值得增加一个单元测试或初始化时的健全性检查,确认代码中引用的所有模板 key 都存在于 htmlTemplates 中,且底层文件可以被加载。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- EssenceFilter previously derived the locale from `RunState.InputLanguage` via `getLocaleFromState`, but now all user‑visible strings go through the global `i18n` language; please confirm that dropping per-run/Per-task locale selection is intentional and won’t regress mixed-locale scenarios.
- The old `autostockpile` abort_reason.json loader validated that every `AbortReason` had a non-empty message and no extra keys; now that lookup goes straight to `i18n.T`, consider adding a small startup check that iterates `knownAbortReasons` and verifies the corresponding `autostockpile.abort.*` keys exist to keep the same safety net.
- The new HTML template registry in `pkg/i18n` is keyed manually (e.g., `essencefilter.no_match_discard`, `maptracker.navigation_moving`); to avoid silent fallbacks like returning the key string, it may be worth adding a unit or init-time sanity check that all referenced template keys in code are present in `htmlTemplates` and the underlying files can be loaded.

## Individual Comments

### Comment 1
<location path="agent/go-service/pkg/i18n/i18n.go" line_range="26" />
<code_context>
+
+	DefaultLang  = LangZhCN
+	envKey       = "PI_CLIENT_LANGUAGE"
+	localeRelDir = "locales/go-service"
+)
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Locale directory base path likely doesn’t match where assets are actually stored

`localeRelDir` is set to `locales/go-service`, but the locale JSON/HTML files in this repo live under `assets/locales/go-service/...`. That means `Init` will resolve `<cwd>/locales/go-service`, while the actual files are at `<cwd>/assets/locales/go-service`, so `loadMessages` and `readTemplateFile` won’t find them unless something copies/remaps assets at build/deploy time.

Recommend either updating `localeRelDir` to `"assets/locales/go-service"`, or making the base path configurable (env/injected) and pointing it at the real asset directory. Otherwise, message loading will fail and `RenderHTML` will only return template keys instead of localized content.
</issue_to_address>

### Comment 2
<location path="agent/go-service/autostockpile/abort_reason.go" line_range="6-10" />
<code_context>
 	"encoding/json"
 	"fmt"

+	"github.com/MaaXYZ/MaaEnd/agent/go-service/pkg/i18n"
 	"github.com/MaaXYZ/MaaEnd/agent/go-service/pkg/maafocus"
 	maa "github.com/MaaXYZ/maa-framework-go/v4"
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Removing the JSON-backed catalog loses upfront validation of abort reason coverage

The old `abortReasonCatalogOnce` path validated at startup that every `knownAbortReasons` entry had a non-empty message and that the JSON had no unknown keys. The new `i18n.T("autostockpile.abort." + string(reason))` path will just return the key for missing translations, so new/renamed abort reasons can ship without a real user-facing message and we won’t notice until runtime.

To keep the i18n approach but retain that safety net, consider a validation step (e.g., during registration) that:
- Iterates over `knownAbortReasons`, calls `i18n.T(...)`, and warns/errors if the result equals the key.
- Optionally checks for unexpected `autostockpile.abort.*` entries if you track them.

That would restore the robustness of the previous JSON-backed validation while using the new i18n system.
</issue_to_address>

### Comment 3
<location path="agent/go-service/essencefilter/integration.go" line_range="42-47" />
<code_context>
-	if engine == nil {
-		return
-	}
+func reportOCRSkills(ctx *maa.Context, skills []string, levels [3]int, matched bool) {
 	color := "#00bfff"
 	if matched {
 		color = "#064d7c"
 	}
-	LogMXUSimpleHTMLWithColor(ctx, engine.FocusOCRSkills(skills, levels), color)
+	text := i18n.T("essencefilter.focus.ocr_skills",
+		skills[0], levels[0], skills[1], levels[1], skills[2], levels[2])
+	LogMXUSimpleHTMLWithColor(ctx, text, color)
</code_context>
<issue_to_address>
**issue:** reportOCRSkills assumes exactly 3 skills, which can panic if the slice is shorter

This now indexes `skills[0]`, `skills[1]`, and `skills[2]` unconditionally, whereas `engine.FocusOCRSkills` could internally handle unexpected lengths. If `skills` is ever shorter than 3 (e.g., partial OCR results or a failed slot), this will panic on slice bounds.

Please either:
- Guard the access: `if len(skills) < 3 { /* fallback or skip */ }`, or
- Ensure callers always pass a 3-element slice (e.g., pad with empty strings) before calling `reportOCRSkills`.

That preserves the new i18n behavior while avoiding a runtime panic in short-slice cases.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Original comment in English

Hey - I've found 3 issues, and left some high level feedback:

  • EssenceFilter previously derived the locale from RunState.InputLanguage via getLocaleFromState, but now all user‑visible strings go through the global i18n language; please confirm that dropping per-run/Per-task locale selection is intentional and won’t regress mixed-locale scenarios.
  • The old autostockpile abort_reason.json loader validated that every AbortReason had a non-empty message and no extra keys; now that lookup goes straight to i18n.T, consider adding a small startup check that iterates knownAbortReasons and verifies the corresponding autostockpile.abort.* keys exist to keep the same safety net.
  • The new HTML template registry in pkg/i18n is keyed manually (e.g., essencefilter.no_match_discard, maptracker.navigation_moving); to avoid silent fallbacks like returning the key string, it may be worth adding a unit or init-time sanity check that all referenced template keys in code are present in htmlTemplates and the underlying files can be loaded.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- EssenceFilter previously derived the locale from `RunState.InputLanguage` via `getLocaleFromState`, but now all user‑visible strings go through the global `i18n` language; please confirm that dropping per-run/Per-task locale selection is intentional and won’t regress mixed-locale scenarios.
- The old `autostockpile` abort_reason.json loader validated that every `AbortReason` had a non-empty message and no extra keys; now that lookup goes straight to `i18n.T`, consider adding a small startup check that iterates `knownAbortReasons` and verifies the corresponding `autostockpile.abort.*` keys exist to keep the same safety net.
- The new HTML template registry in `pkg/i18n` is keyed manually (e.g., `essencefilter.no_match_discard`, `maptracker.navigation_moving`); to avoid silent fallbacks like returning the key string, it may be worth adding a unit or init-time sanity check that all referenced template keys in code are present in `htmlTemplates` and the underlying files can be loaded.

## Individual Comments

### Comment 1
<location path="agent/go-service/pkg/i18n/i18n.go" line_range="26" />
<code_context>
+
+	DefaultLang  = LangZhCN
+	envKey       = "PI_CLIENT_LANGUAGE"
+	localeRelDir = "locales/go-service"
+)
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Locale directory base path likely doesn’t match where assets are actually stored

`localeRelDir` is set to `locales/go-service`, but the locale JSON/HTML files in this repo live under `assets/locales/go-service/...`. That means `Init` will resolve `<cwd>/locales/go-service`, while the actual files are at `<cwd>/assets/locales/go-service`, so `loadMessages` and `readTemplateFile` won’t find them unless something copies/remaps assets at build/deploy time.

Recommend either updating `localeRelDir` to `"assets/locales/go-service"`, or making the base path configurable (env/injected) and pointing it at the real asset directory. Otherwise, message loading will fail and `RenderHTML` will only return template keys instead of localized content.
</issue_to_address>

### Comment 2
<location path="agent/go-service/autostockpile/abort_reason.go" line_range="6-10" />
<code_context>
 	"encoding/json"
 	"fmt"

+	"github.com/MaaXYZ/MaaEnd/agent/go-service/pkg/i18n"
 	"github.com/MaaXYZ/MaaEnd/agent/go-service/pkg/maafocus"
 	maa "github.com/MaaXYZ/maa-framework-go/v4"
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Removing the JSON-backed catalog loses upfront validation of abort reason coverage

The old `abortReasonCatalogOnce` path validated at startup that every `knownAbortReasons` entry had a non-empty message and that the JSON had no unknown keys. The new `i18n.T("autostockpile.abort." + string(reason))` path will just return the key for missing translations, so new/renamed abort reasons can ship without a real user-facing message and we won’t notice until runtime.

To keep the i18n approach but retain that safety net, consider a validation step (e.g., during registration) that:
- Iterates over `knownAbortReasons`, calls `i18n.T(...)`, and warns/errors if the result equals the key.
- Optionally checks for unexpected `autostockpile.abort.*` entries if you track them.

That would restore the robustness of the previous JSON-backed validation while using the new i18n system.
</issue_to_address>

### Comment 3
<location path="agent/go-service/essencefilter/integration.go" line_range="42-47" />
<code_context>
-	if engine == nil {
-		return
-	}
+func reportOCRSkills(ctx *maa.Context, skills []string, levels [3]int, matched bool) {
 	color := "#00bfff"
 	if matched {
 		color = "#064d7c"
 	}
-	LogMXUSimpleHTMLWithColor(ctx, engine.FocusOCRSkills(skills, levels), color)
+	text := i18n.T("essencefilter.focus.ocr_skills",
+		skills[0], levels[0], skills[1], levels[1], skills[2], levels[2])
+	LogMXUSimpleHTMLWithColor(ctx, text, color)
</code_context>
<issue_to_address>
**issue:** reportOCRSkills assumes exactly 3 skills, which can panic if the slice is shorter

This now indexes `skills[0]`, `skills[1]`, and `skills[2]` unconditionally, whereas `engine.FocusOCRSkills` could internally handle unexpected lengths. If `skills` is ever shorter than 3 (e.g., partial OCR results or a failed slot), this will panic on slice bounds.

Please either:
- Guard the access: `if len(skills) < 3 { /* fallback or skip */ }`, or
- Ensure callers always pass a 3-element slice (e.g., pad with empty strings) before calling `reportOCRSkills`.

That preserves the new i18n behavior while avoiding a runtime panic in short-slice cases.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +6 to 10
"github.com/MaaXYZ/MaaEnd/agent/go-service/pkg/i18n"
)

// ValidateAbortReason 校验给定原因键是否合法。
func ValidateAbortReason(reason AbortReason) error {
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.

suggestion (bug_risk): 移除基于 JSON 的目录会失去对中止原因覆盖率的前置校验

旧的 abortReasonCatalogOnce 流程会在启动时校验每个 knownAbortReasons 条目都有非空的 message,并且 JSON 中没有未知字段。新的 i18n.T("autostockpile.abort." + string(reason)) 路径在缺少翻译时只会返回 key 本身,因此新增或重命名的中止原因可能在没有真正面向用户的信息的情况下上线,而且直到运行期我们才会发现。

为了继续使用 i18n 的方式同时保留这层安全网,可以考虑在注册阶段增加一个校验步骤,例如:

  • 遍历 knownAbortReasons,调用 i18n.T(...),如果结果与 key 相同则发出警告/报错;
  • 如果有追踪相关信息,可选地检查是否存在意外的 autostockpile.abort.* 条目。

这样可以在使用新 i18n 系统的同时,恢复之前基于 JSON 校验所提供的健壮性。

Original comment in English

suggestion (bug_risk): Removing the JSON-backed catalog loses upfront validation of abort reason coverage

The old abortReasonCatalogOnce path validated at startup that every knownAbortReasons entry had a non-empty message and that the JSON had no unknown keys. The new i18n.T("autostockpile.abort." + string(reason)) path will just return the key for missing translations, so new/renamed abort reasons can ship without a real user-facing message and we won’t notice until runtime.

To keep the i18n approach but retain that safety net, consider a validation step (e.g., during registration) that:

  • Iterates over knownAbortReasons, calls i18n.T(...), and warns/errors if the result equals the key.
  • Optionally checks for unexpected autostockpile.abort.* entries if you track them.

That would restore the robustness of the previous JSON-backed validation while using the new i18n system.

Comment on lines +42 to +47
func reportOCRSkills(ctx *maa.Context, skills []string, levels [3]int, matched bool) {
color := "#00bfff"
if matched {
color = "#064d7c"
}
LogMXUSimpleHTMLWithColor(ctx, engine.FocusOCRSkills(skills, levels), color)
text := i18n.T("essencefilter.focus.ocr_skills",
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.

issue: reportOCRSkills 假定一定有 3 个技能,如果切片更短会导致 panic

现在无条件访问 skills[0]skills[1]skills[2],而 engine.FocusOCRSkills 之前可能在内部处理了长度异常的情况。如果 skills 长度小于 3(例如 OCR 结果不完整或某个槽位识别失败),就会在切片越界时产生 panic。

请考虑:

  • 添加访问保护:if len(skills) < 3 { /* fallback or skip */ };或者
  • 确保调用方在调用 reportOCRSkills 之前总是传入长度为 3 的切片(例如用空字符串填充)。

这样可以保留新的 i18n 行为,同时避免在切片过短时出现运行期 panic。

Original comment in English

issue: reportOCRSkills assumes exactly 3 skills, which can panic if the slice is shorter

This now indexes skills[0], skills[1], and skills[2] unconditionally, whereas engine.FocusOCRSkills could internally handle unexpected lengths. If skills is ever shorter than 3 (e.g., partial OCR results or a failed slot), this will panic on slice bounds.

Please either:

  • Guard the access: if len(skills) < 3 { /* fallback or skip */ }, or
  • Ensure callers always pass a 3-element slice (e.g., pad with empty strings) before calling reportOCRSkills.

That preserves the new i18n behavior while avoiding a runtime panic in short-slice cases.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 68 out of 75 changed files in this pull request and generated 10 comments.

Comment on lines +1 to +5
{
"resell.stock_empty": "⚠️ 庫存已售罄,無可購買商品",
"resell.quota_overflow": "⚠️ 配額溢出提醒\n剩餘配額明天將超出上限,建議購買%d件商品\n推薦購買: 第%d行第%d列 (最高利潤: %d)",
"resell.auto_buy_disabled": "💡 已禁用自動購買/出售\n推薦購買: 第%d行第%d列 (利潤: %d)",
"resell.below_min_profit": "💡 沒有達到最低利潤的商品,建議把配額留至明天\n推薦購買: 第%d行第%d列 (利潤: %d)",
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

ko_kr.json 文件开头多条文案仍为中文(看起来更像从 zh_tw 复制),会导致韩语界面显示错误语言。建议补齐韩语翻译;如果短期无法翻译,至少避免提交明显错误语言的内容(例如先用英文/占位,并在后续补翻译)。

Copilot uses AI. Check for mistakes.
@MistEO MistEO merged commit c062f3e into v2 Mar 24, 2026
11 checks passed
@MistEO MistEO deleted the refactor/go-i18n branch March 24, 2026 11:53
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.

6 participants