Skip to content

I18n v2#2709

Merged
huiyadanli merged 16 commits intomainfrom
i18n-v2
Feb 15, 2026
Merged

I18n v2#2709
huiyadanli merged 16 commits intomainfrom
i18n-v2

Conversation

@huiyadanli
Copy link
Member

@huiyadanli huiyadanli commented Jan 29, 2026

A dynamic functionality to extract key text from UI and logs, seamlessly integrated without hardcoding.

  • If matching text is found in the corresponding language file, it will be replaced for display.
  • Otherwise, the missing text will be generated into a missing.{lang}.json file.

close #94 close #275 close #2608

translation file: https://github.com/babalae/bettergi-i18n


一种动态从UI、日志中动态获取关键文本的功能,可以无缝集成且无需硬编码

  • 如果从对应语言文件中找到匹配文本,则替换展示
  • 无则生成至 missing.{lang}.json 文件

翻译文件: https://github.com/babalae/bettergi-i18n

TODO

  • 语言切换的影响面评估
  • 仓库自动翻译的可行性
  • 对接后端自动化收集missing数据并翻译

Summary by CodeRabbit

  • 新功能

    • 添加完整英文本地化资源,支持英文界面。
    • 引入运行时翻译服务与自动翻译拦截器,窗口级别可自动翻译并在切换语言时刷新。
    • 新增 XAML 翻译转换器,绑定/模板中可直接使用翻译。
    • 设置页新增界面语言选择卡,选择后即时生效。
  • 其他

    • 收集缺失翻译并异步上报/持久化的机制已加入。
    • 构建配置确保本地化资源随构建输出。

- 新增 ITranslationService 接口及 JsonTranslationService 实现,提供基于 JSON 的翻译服务
- 添加 TrConverter 转换器,支持通过绑定动态翻译界面文本
- 引入 AutoTranslateInterceptor 行为,自动扫描并翻译界面中的静态文本
- 集成 TranslatingSerilogLoggerProvider,实现日志输出的实时翻译
- 在 App.xaml 中注册全局样式,为 Window、UserControl 和 Page 启用自动翻译
- 移除 HomePage 中冗余的 EnableAutoTranslate 属性设置,改为继承属性
- 通过类构造函数注册全局 Loaded 事件处理器,替代在每个元素上单独添加
- 引入请求队列机制,批量处理待应用翻译的元素,避免重复调度
- 扩展属性类型检查,支持 object 类型以处理更多动态内容场景
移除在 App.xaml 中为 Window、UserControl 和 Page 全局设置的 AutoTranslateInterceptor,
改为仅在 PickerWindow 中显式启用。这解决了全局样式可能导致的意外行为或冲突。
为 MapLabelSearchWindow、ArtifactOcrDialog、PromptDialog 等 14 个窗口添加了 AutoTranslateInterceptor.EnableAutoTranslate 属性,以启用自动翻译拦截功能。
- 在通用设置页面新增UI语言选择控件,支持动态切换界面语言
- 修改游戏语言标签为“原神游戏语言”以明确区分
- 改进JsonTranslationService,支持UI语言切换时的实时翻译更新
- 优化AutoTranslateInterceptor,缓存原始文本值并在语言切换时恢复
- 添加属性变更监听机制,确保UI元素在语言切换后正确刷新
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 29, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

新增运行时翻译子系统:JSON 驱动的 ITranslationService 实现、缺失翻译采集与上报、用于日志模板翻译的 ILoggerProvider、WPF 自动翻译拦截器与绑定转换器;在 App 中注册并替换日志提供器;大量 XAML 启用自动翻译;添加 English i18n 数据文件。

Changes

Cohort / File(s) Summary
应用与 DI 注册
BetterGenshinImpact/App.xaml, BetterGenshinImpact/App.xaml.cs, BetterGenshinImpact/BetterGenshinImpact.csproj
在 App.xaml 中注册 TrConverter 资源;在 App.xaml.cs 中注册 ITranslationService(JsonTranslationService)与 IMissingTranslationReporter(SupabaseMissingTranslationReporter),并以 TranslatingSerilogLoggerProvider 替换默认 ILoggerProvider;csproj 将 User\I18n\en.json 设置为 PreserveNewest 输出。
翻译接口与实现
BetterGenshinImpact/Service/Interface/ITranslationService.cs, BetterGenshinImpact/Service/JsonTranslationService.cs
新增接口/类型:ITranslationServiceMissingTextSourceTranslationSourceInfo;新增 JsonTranslationService:按文化加载/缓存 JSON 翻译、检测 CJK 与中文例外、记录并原子写入缺失项、响应文化切换并公开 Translate/GetCurrentCulture/Dispose。
缺失翻译采集与上报
BetterGenshinImpact/Service/Interface/IMissingTranslationReporter.cs, BetterGenshinImpact/Service/MissingTranslationCollectionSettings.cs, BetterGenshinImpact/Service/SupabaseMissingTranslationReporter.cs
新增 IMissingTranslationReporter 接口、配置类 MissingTranslationCollectionSettingsSupabaseMissingTranslationReporter:内存批量缓冲、后台周期性上报到 Supabase(upsert + 合并源信息)、可取消的优雅关闭。
日志模板翻译提供器
BetterGenshinImpact/Helpers/TranslatingSerilogLoggerProvider.cs
新增 TranslatingSerilogLoggerProvider:将 Microsoft.Logging 框架的日志委托给 Serilog,同时在写入前通过 ITranslationService 翻译消息模板与格式化流程。
WPF 自动翻译行为与转换器
BetterGenshinImpact/View/Behavior/AutoTranslateInterceptor.cs, BetterGenshinImpact/View/Converters/TrConverter.cs
新增 AutoTranslateInterceptor(attached property、UI 树遍历、跳过绑定项、跟踪与恢复原值、文化变更监听、ToolTip/ContextMenu 处理等)与绑定用 TrConverter(在绑定时调用翻译服务)。
启用自动翻译的 XAML 窗口/页面
BetterGenshinImpact/View/.../*.xaml
例如: BetterGenshinImpact/View/MainWindow.xaml, .../Pages/CommonSettingsPage.xaml, .../Pages/View/PathingConfigView.xaml, View/Windows/.../*.xaml
在许多窗口/页面添加 xmlns:behavior 并设置 behavior:AutoTranslateInterceptor.EnableAutoTranslate="True";CommonSettingsPage 新增界面语言选择控件。
英文本地化数据
BetterGenshinImpact/User/I18n/en.json
新增完整英文翻译映射数据文件(大量键值对),作为运行时翻译资源。
杂项小改动
BetterGenshinImpact/Core/Config/OtherConfig.cs, BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs
删除未使用的 using、移除 BOM/编码空行等格式修正,无行为变更。

Sequence Diagram(s)

sequenceDiagram
    participant App as App (启动)
    participant DI as DI 容器
    participant JsonTS as JsonTranslationService
    participant Reporter as SupabaseMissingTranslationReporter
    participant LoggerProv as TranslatingSerilogLoggerProvider
    participant Serilog as Serilog
    participant UI as WPF 窗口/控件
    participant ATI as AutoTranslateInterceptor

    App->>DI: 注册 ITranslationService, IMissingTranslationReporter, ILoggerProvider
    DI->>JsonTS: 构造/注入 JsonTranslationService
    DI->>Reporter: 构造/注入 SupabaseMissingTranslationReporter
    DI->>LoggerProv: 构造 TranslatingSerilogLoggerProvider(translationService)
    UI->>ATI: Loaded (EnableAutoTranslate=True)
    ATI->>JsonTS: 请求 Translate(text, sourceInfo) / GetCurrentCulture()
    JsonTS->>JsonTS: 从 en.json 或文化文件加载映射;若缺失则记录 MissingItem
    JsonTS->>Reporter: TryEnqueue(language, key, sourceInfo) (可选)
    JsonTS-->>ATI: 返回翻译或原文
    ATI->>UI: 应用翻译文本到控件
    LoggerProv->>JsonTS: 在记录日志前 Translate(模板)
    LoggerProv->>Serilog: 转发已翻译模板与值
    Reporter->>Reporter: 批量聚合并周期性上报到 Supabase(HTTP upsert)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🐰 我在代码洞里嗅到新词,
JSON 小径搭起英语桥,
窗口悄悄披上双语衣裳,
日志也学会低声翻译,
小兔子在终端前快乐蹦跳。

🚥 Pre-merge checks | ✅ 5 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed 标题「I18n v2」简明扼要地总结了此PR的核心改动:实现了国际化第二版本,包含翻译服务、缺失翻译检测和收集等功能。
Linked Issues check ✅ Passed 该PR成功满足了关联议题#94的需求,通过实现完整的i18n系统(ITranslationService、JsonTranslationService、AutoTranslateInterceptor、TrConverter)为非中文使用者提供了英文翻译支持。
Out of Scope Changes check ✅ Passed 所有改动均在PR目标范围内:包括i18n基础设施(翻译服务接口实现)、UI集成(自动翻译拦截器、数据绑定)、缺失翻译报告(Supabase集成)及英文本地化数据。
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch i18n-v2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@BetterGenshinImpact/Service/JsonTranslationService.cs`:
- Around line 74-93: GetCurrentCulture can return InvariantCulture (empty Name)
and the current code still calls EnsureMapLoaded and records missing keys
(creating missing..json); change the early-return logic in the translation
method (the block using GetCurrentCulture, IsChineseCulture, EnsureMapLoaded,
_map and _missingKeys) to first check for an invariant/empty culture name (e.g.
culture == CultureInfo.InvariantCulture or string.IsNullOrEmpty(culture.Name))
and immediately return the original text without calling EnsureMapLoaded or
touching _missingKeys/_dirtyMissing; keep the existing Chinese-culture
short-circuit behavior (IsChineseCulture) as-is.
🧹 Nitpick comments (4)
BetterGenshinImpact/View/Pages/CommonSettingsPage.xaml (1)

55-59: 命令名称可能不匹配其用途

此处 UI 语言选择使用了 GameLangSelectionChangedCommand,但该命令名暗示是用于"游戏语言"切换。如果两者确实共用同一处理逻辑,建议将命令重命名为更通用的名称(如 LanguageSelectionChangedCommand),或为 UI 语言添加专门的命令。

BetterGenshinImpact/View/Behavior/AutoTranslateInterceptor.cs (2)

208-222: 空的 catch 块会掩盖潜在问题

Dispose 方法中的空 catch 块会静默吞掉所有异常,可能导致调试困难。建议至少记录日志或使用条件编译在调试模式下输出异常信息。

♻️ 建议的改进
 public void Dispose()
 {
     foreach (var unsub in _unsubscribe)
     {
         try
         {
             unsub();
         }
         catch
         {
+            // 可选:在调试模式下记录异常
+#if DEBUG
+            System.Diagnostics.Debug.WriteLine($"Failed to unsubscribe: {ex}");
+#endif
         }
     }

     _unsubscribe.Clear();
+    _originalValues.Clear();
+    _trackedContextMenus.Clear();
+    _trackedToolTips.Clear();
 }

156-166: 建议在 Dispose 中清理所有集合

_originalValues_trackedContextMenus_trackedToolTips 集合持有对 UI 元素的引用。虽然在 Scope 被释放后这些引用最终会被 GC 回收,但在 Dispose 中显式清理有助于更快释放内存。

BetterGenshinImpact/View/Converters/TrConverter.cs (1)

11-20: 考虑缓存 ITranslationService 实例以提升性能。

当前实现每次 Convert 调用都会通过 App.GetService<ITranslationService>() 获取服务实例。如果在高频绑定场景下使用,可能会有轻微的性能开销。可以考虑使用延迟缓存模式。

♻️ 可选优化:延迟缓存服务实例
 [ValueConversion(typeof(string), typeof(string))]
 public sealed class TrConverter : IValueConverter
 {
+    private ITranslationService? _translator;
+    
     public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
     {
         if (value is not string s || string.IsNullOrEmpty(s))
         {
             return value ?? string.Empty;
         }
 
-        var translator = App.GetService<ITranslationService>();
-        return translator?.Translate(s, MissingTextSource.UiDynamicBinding) ?? s;
+        _translator ??= App.GetService<ITranslationService>();
+        return _translator?.Translate(s, MissingTextSource.UiDynamicBinding) ?? s;
     }

在 AutoTranslateInterceptor 中新增 ExcludeAutoTranslate 附加属性,允许对特定依赖对象禁用自动翻译功能。当遍历元素进行翻译时,会检查此属性并跳过已标记排除的元素。
扩展翻译服务以收集缺失文本的详细上下文,包括视图路径、元素类型、属性名等。
重构 `ITranslationService` 接口,引入 `TranslationSourceInfo` 类封装上下文信息。
修改 `AutoTranslateInterceptor` 自动收集 UI 元素信息,`JsonTranslationService` 合并多来源上下文。
添加 IsInGridViewRowPresenter 检查,避免在 GridViewRowPresenter 控件内进行自动翻译,防止潜在的界面显示问题。
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@BetterGenshinImpact/View/Behavior/AutoTranslateInterceptor.cs`:
- Around line 156-222: The Scope currently becomes a "zombie" after OnUnloaded
calls Dispose because Dispose clears subscriptions but doesn't reset state or
remove the Scope association from the element; update Dispose (and/or
OnUnloaded) in the Scope class so that it (1) sets _applied = false, (2) clears
_originalValues (and any other tracked collections like
_trackedContextMenus/_trackedToolTips/_pendingApply), (3) unregisters messenger
handlers via WeakReferenceMessenger.Default.UnregisterAll(this) (already done)
and (4) removes the Scope instance from the attached property on the
FrameworkElement (the ScopeProperty association) so the element can create a
fresh Scope when reloaded; ensure these steps happen safely (guard against
multiple Dispose calls) in the Scope.Dispose method and/or call a dedicated
Cleanup method from OnUnloaded.
🧹 Nitpick comments (9)
BetterGenshinImpact/View/Converters/TrConverter.cs (1)

12-22: 建议缓存 ITranslationService 引用,避免每次转换都进行 DI 查找

Convert 在每次绑定值更新时都会调用 App.GetService<ITranslationService>()。在包含大量绑定的界面中,这会产生不必要的开销。由于翻译服务是单例,可以在首次使用时缓存引用。

♻️ 建议修改
 public sealed class TrConverter : IValueConverter
 {
+    private ITranslationService? _translator;
+
     public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
     {
         if (value is not string s || string.IsNullOrEmpty(s))
         {
             return value ?? string.Empty;
         }
 
-        var translator = App.GetService<ITranslationService>();
+        var translator = _translator ??= App.GetService<ITranslationService>();
         var source = parameter is MissingTextSource sourceParam ? sourceParam : MissingTextSource.UiDynamicBinding;
         return translator?.Translate(s, TranslationSourceInfo.From(source)) ?? s;
     }
BetterGenshinImpact/Helpers/TranslatingSerilogLoggerProvider.cs (1)

45-48: IsEnabled 未检查 Serilog 的最低日志级别,可能导致不必要的性能开销

当前实现对除 LogLevel.None 之外的所有级别返回 true。框架代码(如 LoggerMessage.Define 生成的高性能日志路径)依赖 IsEnabled 来决定是否构造日志状态对象。如果 Serilog 配置为 Information 级别,IsEnabled(LogLevel.Trace) 仍返回 true,会导致框架不必要地构造状态并进入 Log 方法(尽管最终不会写入)。

♻️ 建议修改
         public bool IsEnabled(LogLevel logLevel)
         {
-            return logLevel != LogLevel.None;
+            if (logLevel == LogLevel.None) return false;
+            var serilogLevel = ConvertLevel(logLevel);
+            return serilogLevel.HasValue && _logger.IsEnabled(serilogLevel.Value);
         }
BetterGenshinImpact/View/Behavior/AutoTranslateInterceptor.cs (4)

19-31: 全局 RegisterClassHandler 会为应用中每个元素触发回调,需关注性能影响

静态构造函数为所有 FrameworkElementFrameworkContentElementLoaded 事件注册了全局处理器。这意味着即使未启用翻译的区域,每个元素加载时也会触发 OnAnyElementLoadedFindNearestScope(向上遍历树)。在复杂 UI 中,这可能造成可观的性能开销。

如果性能测试显示有问题,可以考虑仅在 OnEnableAutoTranslateChanged 中为特定根元素注册事件,而非全局拦截。


291-355: ApplyIsInGridViewRowPresenter 对每个节点都向上遍历,建议优化

Line 317 的 IsInGridViewRowPresenter(current) 对 BFS 中的每个节点都调用 GetParentObject 向上遍历至根节点。对于大型树,这使整体复杂度接近 O(n²)。

一个更高效的方案是在 BFS 遍历时维护一个标记:当遇到 GridViewRowPresenter 时,直接跳过其子节点入队,而不是对每个节点都做向上遍历检查。

♻️ 建议修改思路
                     var current = queue.Dequeue();
                     if (!visited.Add(current))
                     {
                         continue;
                     }
-                    
-                    if (IsInGridViewRowPresenter(current))
+
+                    // Skip children of GridViewRowPresenter
+                    if (current is GridViewRowPresenter)
                     {
                         continue;
                     }

这样当 BFS 遍历到 GridViewRowPresenter 时直接跳过,不再入队其子节点,从而避免对每个节点的向上遍历。


561-592: TranslateIfNotBound:当值不含汉字时仍调用 Translate,产生不必要的开销

Line 581-584:当 ContainsHan(currentValue)false_originalValues 中无记录时,original 被设为 currentValue 但未提前返回,而是继续调用 translator.Translate(original, ...)。虽然 JsonTranslationService.Translate 会通过 ContainsCjk 检查快速短路返回,但可以在此处直接 return 避免构造 TranslationSourceInfo 和方法调用开销。

♻️ 建议修改
                 if (!_originalValues.TryGetValue(key, out var original))
                 {
                     if (ContainsHan(currentValue))
                     {
                         _originalValues[key] = currentValue;
                         original = currentValue;
                     }
                     else
                     {
-                        original = currentValue;
+                        return;
                     }
                 }

521-546: ShouldTranslatePropertyName"MemberPath" 检查被 "Path" 覆盖,属冗余条件

Line 529:EndsWith("MemberPath") 的结果已被 Line 528 的 EndsWith("Path") 完全覆盖("MemberPath" 以 "Path" 结尾)。虽然不影响正确性,但可以移除冗余检查。

BetterGenshinImpact/Service/JsonTranslationService.cs (2)

39-55: GetCurrentCulture() 每次调用都创建新的 CultureInfo 对象,建议缓存

GetCurrentCulture() 在每次 Translate 调用时执行,而 new CultureInfo(name) 有一定开销(涉及系统区域性数据查找)。在高频翻译场景(如 UI 树遍历或日志翻译)中,这会产生大量临时对象和不必要的计算。

建议在 OnOtherConfigPropertyChanged 中缓存解析后的 CultureInfoGetCurrentCulture() 直接返回缓存值。

♻️ 建议修改
+    private volatile CultureInfo _cachedCulture = CultureInfo.InvariantCulture;
+
     public JsonTranslationService(IConfigService configService)
     {
         _configService = configService;
         _otherConfig = _configService.Get().OtherConfig;
+        _cachedCulture = ParseCulture(_otherConfig.UiCultureInfoName);
         _otherConfig.PropertyChanged += OnOtherConfigPropertyChanged;
         _flushTimer = new Timer(_ => FlushMissingIfDirty(), null, TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(3));
     }
 
     public CultureInfo GetCurrentCulture()
     {
-        var name = _configService.Get().OtherConfig.UiCultureInfoName;
+        return _cachedCulture;
+    }
+
+    private static CultureInfo ParseCulture(string? name)
+    {
         if (string.IsNullOrWhiteSpace(name))
         {
             return CultureInfo.InvariantCulture;
         }
 
         try
         {
             return new CultureInfo(name);
         }
         catch
         {
             return CultureInfo.InvariantCulture;
         }
     }

并在 OnOtherConfigPropertyChanged 中更新缓存:

     private void OnOtherConfigPropertyChanged(object? sender, PropertyChangedEventArgs e)
     {
         if (e.PropertyName != nameof(OtherConfig.UiCultureInfoName))
         {
             return;
         }
 
         var cultureName = _otherConfig.UiCultureInfoName ?? string.Empty;
+        _cachedCulture = ParseCulture(cultureName);

96-117: 双重检查锁模式中 _loadedCultureName_map 缺少 volatile 修饰

EnsureMapLoaded 使用了双重检查锁模式(line 98 外部读,line 105 锁内读),但 _loadedCultureName(line 27)和 _map(line 28)都未标记为 volatile。虽然 .NET CLR 的内存模型通常比 C# 规范更严格,在 x86/x64 上一般不会出问题,但按规范外部读取可能看到旧值。

_map 上使用 volatile 或在读取前使用 Volatile.Read 可以消除此隐患。

♻️ 建议修改
-    private string _loadedCultureName = string.Empty;
-    private IReadOnlyDictionary<string, string> _map = new Dictionary<string, string>(StringComparer.Ordinal);
+    private volatile string _loadedCultureName = string.Empty;
+    private volatile IReadOnlyDictionary<string, string> _map = new Dictionary<string, string>(StringComparer.Ordinal);
BetterGenshinImpact/Service/Interface/ITranslationService.cs (1)

13-31: 可选优化:考虑使用 init 访问器提高不可变性

TranslationSourceInfo 的所有属性都是 public set,但从实际使用来看(NormalizeSourceInfoMergeSourceInfoBuildSourceInfo 等),实例总是通过对象初始化器创建,之后不再修改。使用 init 替代 set 可以防止意外修改,同时仍支持对象初始化器和 JSON 反序列化。Newtonsoft.Json 13.0.3 完整支持 init 属性——init 访问器在 IL 元数据中作为正常的 set_ 方法发出,Json.NET 通过反射设置属性值,因此兼容。

♻️ 建议修改
 public sealed class TranslationSourceInfo
 {
-    public MissingTextSource Source { get; set; } = MissingTextSource.Unknown;
-    public string? ViewXamlPath { get; set; }
-    public string? ViewType { get; set; }
-    public string? ElementType { get; set; }
-    public string? ElementName { get; set; }
-    public string? PropertyName { get; set; }
-    public string? BindingPath { get; set; }
-    public string? Notes { get; set; }
+    public MissingTextSource Source { get; init; } = MissingTextSource.Unknown;
+    public string? ViewXamlPath { get; init; }
+    public string? ViewType { get; init; }
+    public string? ElementType { get; init; }
+    public string? ElementName { get; init; }
+    public string? PropertyName { get; init; }
+    public string? BindingPath { get; init; }
+    public string? Notes { get; init; }

Comment on lines +156 to +222
private sealed class Scope : IDisposable
{
private readonly FrameworkElement _root;
private readonly List<Action> _unsubscribe = new();
private bool _applied;
private readonly HashSet<ContextMenu> _trackedContextMenus = new();
private readonly HashSet<ToolTip> _trackedToolTips = new();
private readonly HashSet<DependencyObject> _pendingApply = new();
private bool _applyScheduled;
private readonly Dictionary<(DependencyObject Obj, DependencyProperty Property), string> _originalValues = new();
private bool _refreshScheduled;

public Scope(FrameworkElement root)
{
_root = root;
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<object>>(this, (_, msg) =>
{
if (msg.PropertyName == nameof(OtherConfig.UiCultureInfoName))
{
ScheduleRefresh();
}
});
_unsubscribe.Add(() => WeakReferenceMessenger.Default.UnregisterAll(this));
}

public void OnLoaded(object sender, RoutedEventArgs e)
{
if (_applied)
{
return;
}

_applied = true;
Apply(_root);
}

public void ApplyNow()
{
if (_applied)
{
return;
}

_applied = true;
Apply(_root);
}

public void OnUnloaded(object sender, RoutedEventArgs e)
{
Dispose();
}

public void Dispose()
{
foreach (var unsub in _unsubscribe)
{
try
{
unsub();
}
catch
{
}
}

_unsubscribe.Clear();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Scope 生命周期问题:OnUnloaded 调用 Dispose 后,元素重新加载时 Scope 处于"僵尸"状态

OnUnloaded 调用 Dispose() 清除了 messenger 订阅和事件处理器,但没有重置 _applied 标志,也没有从元素上移除 ScopeProperty。当元素在 TabControl 等场景中被卸载后重新加载时:

  1. OnLoaded_applied == true 立即返回,不会重新翻译
  2. Messenger 订阅已被移除,文化切换不再生效
  3. _originalValues 中持有的 DependencyObject 引用阻止了 GC

建议在 Dispose 时同时重置 _applied = false,或在 OnUnloaded 中完整清理并从元素上移除 Scope。

🐛 建议修改方案一:在 Dispose 中重置状态以支持重新加载
         public void Dispose()
         {
             foreach (var unsub in _unsubscribe)
             {
                 try
                 {
                     unsub();
                 }
                 catch
                 {
                 }
             }
 
             _unsubscribe.Clear();
+            _applied = false;
+            _originalValues.Clear();
+            _pendingApply.Clear();
+            _trackedContextMenus.Clear();
+            _trackedToolTips.Clear();
         }
🤖 Prompt for AI Agents
In `@BetterGenshinImpact/View/Behavior/AutoTranslateInterceptor.cs` around lines
156 - 222, The Scope currently becomes a "zombie" after OnUnloaded calls Dispose
because Dispose clears subscriptions but doesn't reset state or remove the Scope
association from the element; update Dispose (and/or OnUnloaded) in the Scope
class so that it (1) sets _applied = false, (2) clears _originalValues (and any
other tracked collections like
_trackedContextMenus/_trackedToolTips/_pendingApply), (3) unregisters messenger
handlers via WeakReferenceMessenger.Default.UnregisterAll(this) (already done)
and (4) removes the Scope instance from the attached property on the
FrameworkElement (the ScopeProperty association) so the element can create a
fresh Scope when reloaded; ensure these steps happen safely (guard against
multiple Dispose calls) in the Scope.Dispose method and/or call a dedicated
Cleanup method from OnUnloaded.

在自动翻译拦截器中添加了 IsInComboBoxContext 方法,用于检测依赖对象是否处于 ComboBox 或其相关弹出菜单的上下文中。当检测到对象位于组合框上下文时,跳过自动翻译逻辑,避免对下拉选项等界面元素进行不必要的翻译操作,从而解决潜在的界面干扰问题。
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@BetterGenshinImpact/View/Behavior/AutoTranslateInterceptor.cs`:
- Line 166: _current implementation uses a strong-reference Dictionary named
_originalValues keyed by (DependencyObject, DependencyProperty), which prevents
UI elements from being GC'd and can leak memory; replace it with a
ConditionalWeakTable<DependencyObject, Dictionary<DependencyProperty, string>>
(or equivalent weak-key map) so keys (DependencyObject) are held weakly, and
adapt all accesses/updates/removals (places referencing _originalValues, e.g.,
the add/get/remove logic around the earlier usages and the block at lines
referenced 605-617) to first GetOrCreateValue for the DependencyObject and then
index the inner Dictionary by DependencyProperty, ensuring to remove inner
entries when empty; alternatively, if ConditionalWeakTable isn't desirable, add
periodic cleanup that walks entries and removes those with disconnected or
unloaded DependencyObjects.
- Around line 57-65: OnAnyElementLoaded is invoking FindNearestScope for every
loaded FrameworkElement because the static class handler (registered with
handledEventsToo: true) runs globally; add an early-exit in FindNearestScope to
avoid full tree traversal by checking the inherited EnableAutoTranslateProperty
on the starting DependencyObject (use obj.GetValue(EnableAutoTranslateProperty)
or equivalent) and return null immediately when the inherited value indicates
auto-translate is disabled or unset, so only elements under a scope with
EnableAutoTranslateProperty=true are walked further.
- Around line 225-252: ScheduleRefresh currently reads/writes _applied and
_refreshScheduled off the UI thread, causing a race; fix it by moving all checks
and mutations of those flags into the UI-dispatched action and use atomic ops
for the scheduling flag: always call _root.Dispatcher.BeginInvoke(...) from
ScheduleRefresh (so threading is normalized), then inside the dispatched lambda
check the current value of _applied and use Interlocked.Exchange(ref
_refreshScheduled, 1) (or Interlocked.CompareExchange) to detect/claim
scheduling and set it back to 0 when done, then call RestoreOriginalValues,
RefreshBoundValues(_root) and Apply(_root); this keeps
_applied/_refreshScheduled accesses on the UI thread and avoids races between
WeakReferenceMessenger callbacks and dispatcher execution.
🧹 Nitpick comments (3)
BetterGenshinImpact/View/Behavior/AutoTranslateInterceptor.cs (3)

770-784: IsInGridViewRowPresenterApply 循环内对每个节点向上遍历,导致 O(n × depth) 复杂度

Apply 方法的 BFS 遍历中(第 323 行),对每个子节点都调用 IsInGridViewRowPresenter,后者通过 GetParentObject 向上遍历至根。对于深层树,这意味着总操作量为 O(n × depth)。IsInComboBoxContext(第 328 行)也有同样的问题。

建议在 BFS 遍历过程中维护一个 context 标记集合,遇到 GridViewRowPresenterComboBox 时直接跳过其子树,而不是对每个节点反复向上查找。


572-603: 非 CJK 文本仍会调用 translator.Translate,产生不必要的开销

ContainsHan(currentValue) 返回 false_originalValues 中无记录时(第 587-595 行),original 被赋值为 currentValue 但未存入字典,随后仍会调用 translator.Translate。虽然翻译服务内部会短路返回,但 BuildSourceInfo(第 598 行)涉及向上遍历树查找 view 信息,开销不小。

建议在 ContainsHan 为 false 且无已存原始值时直接 return:

♻️ 建议修改
                 var key = (obj, property);
                 if (!_originalValues.TryGetValue(key, out var original))
                 {
                     if (ContainsHan(currentValue))
                     {
                         _originalValues[key] = currentValue;
                         original = currentValue;
                     }
                     else
                     {
-                        original = currentValue;
+                        return;
                     }
                 }

539-540: "MemberPath" 检查被 "Path" 检查完全覆盖

name.EndsWith("MemberPath") 永远是 name.EndsWith("Path") 的子集,第 540 行的判断永远不会单独命中。可以移除以减少认知负担。

Comment on lines +57 to +65
private static void OnAnyElementLoaded(object sender, RoutedEventArgs e)
{
if (sender is not DependencyObject obj)
{
return;
}

FindNearestScope(obj)?.RequestApply(obj);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

全局 class handler 对性能的影响需关注

静态构造函数为所有 FrameworkElementFrameworkContentElement 注册了 Loaded 事件处理器(handledEventsToo: true),每次元素加载都会调用 FindNearestScope 向上遍历树。对于未启用翻译的窗口中的大量元素,这些遍历全部走到根节点后返回 null,是无效开销。

建议在 FindNearestScope 中增加早期退出条件,例如检查 EnableAutoTranslateProperty 继承值:

♻️ 建议优化
         private static void OnAnyElementLoaded(object sender, RoutedEventArgs e)
         {
             if (sender is not DependencyObject obj)
             {
                 return;
             }

+            // Fast path: skip elements not under an auto-translate root
+            if (!GetEnableAutoTranslate(obj))
+            {
+                return;
+            }
+
             FindNearestScope(obj)?.RequestApply(obj);
         }

由于 EnableAutoTranslateProperty 使用了 FrameworkPropertyMetadataOptions.Inherits,子元素会继承该值,因此可以直接检查来快速排除无关元素。

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static void OnAnyElementLoaded(object sender, RoutedEventArgs e)
{
if (sender is not DependencyObject obj)
{
return;
}
FindNearestScope(obj)?.RequestApply(obj);
}
private static void OnAnyElementLoaded(object sender, RoutedEventArgs e)
{
if (sender is not DependencyObject obj)
{
return;
}
// Fast path: skip elements not under an auto-translate root
if (!GetEnableAutoTranslate(obj))
{
return;
}
FindNearestScope(obj)?.RequestApply(obj);
}
🤖 Prompt for AI Agents
In `@BetterGenshinImpact/View/Behavior/AutoTranslateInterceptor.cs` around lines
57 - 65, OnAnyElementLoaded is invoking FindNearestScope for every loaded
FrameworkElement because the static class handler (registered with
handledEventsToo: true) runs globally; add an early-exit in FindNearestScope to
avoid full tree traversal by checking the inherited EnableAutoTranslateProperty
on the starting DependencyObject (use obj.GetValue(EnableAutoTranslateProperty)
or equivalent) and return null immediately when the inherited value indicates
auto-translate is disabled or unset, so only elements under a scope with
EnableAutoTranslateProperty=true are walked further.

private readonly HashSet<ToolTip> _trackedToolTips = new();
private readonly HashSet<DependencyObject> _pendingApply = new();
private bool _applyScheduled;
private readonly Dictionary<(DependencyObject Obj, DependencyProperty Property), string> _originalValues = new();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

_originalValues 字典对所有已翻译元素持有强引用,可能导致内存泄漏

_originalValues 的 key 是 (DependencyObject, DependencyProperty) 元组,持有对 UI 元素的强引用。只要 Scope 存活,即使对应元素已从可视化树移除,也无法被 GC 回收。对于长期运行的窗口或频繁添加/移除子元素的场景(如列表虚拟化),这会造成持续的内存增长。

考虑使用 ConditionalWeakTable<DependencyObject, Dictionary<DependencyProperty, string>> 或定期清理已失效的条目。

Also applies to: 605-617

🤖 Prompt for AI Agents
In `@BetterGenshinImpact/View/Behavior/AutoTranslateInterceptor.cs` at line 166,
_current implementation uses a strong-reference Dictionary named _originalValues
keyed by (DependencyObject, DependencyProperty), which prevents UI elements from
being GC'd and can leak memory; replace it with a
ConditionalWeakTable<DependencyObject, Dictionary<DependencyProperty, string>>
(or equivalent weak-key map) so keys (DependencyObject) are held weakly, and
adapt all accesses/updates/removals (places referencing _originalValues, e.g.,
the add/get/remove logic around the earlier usages and the block at lines
referenced 605-617) to first GetOrCreateValue for the DependencyObject and then
index the inner Dictionary by DependencyProperty, ensuring to remove inner
entries when empty; alternatively, if ConditionalWeakTable isn't desirable, add
periodic cleanup that walks entries and removes those with disconnected or
unloaded DependencyObjects.

Comment on lines +225 to +252
private void ScheduleRefresh()
{
if (!_applied)
{
return;
}

if (_refreshScheduled)
{
return;
}

_refreshScheduled = true;
_root.Dispatcher.BeginInvoke(
() =>
{
_refreshScheduled = false;
if (!_applied)
{
return;
}

RestoreOriginalValues();
RefreshBoundValues(_root);
Apply(_root);
},
DispatcherPriority.Loaded);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

ScheduleRefresh 存在跨线程竞态条件

WeakReferenceMessenger 的回调可能在非 UI 线程触发,而 ScheduleRefresh 在第 227、232、237 行直接读写 _applied_refreshScheduled,没有任何同步保护。虽然实际翻译工作 dispatch 到了 UI 线程,但在高频文化切换场景下可能导致重复 dispatch 或遗漏已 Dispose 的状态。

建议使用 volatileInterlocked 保护这两个标志位,或者将整个 ScheduleRefresh 通过 Dispatcher.BeginInvoke 回到 UI 线程后再做检查。

♻️ 建议修改
-            private bool _refreshScheduled;
+            private volatile bool _refreshScheduled;

             private void ScheduleRefresh()
             {
-                if (!_applied)
+                // Dispatch to UI thread to avoid cross-thread access to _applied
+                _root.Dispatcher.BeginInvoke(() =>
                 {
-                    return;
-                }
-
-                if (_refreshScheduled)
-                {
-                    return;
-                }
-
-                _refreshScheduled = true;
-                _root.Dispatcher.BeginInvoke(
-                    () =>
+                    if (!_applied || _refreshScheduled)
                     {
-                        _refreshScheduled = false;
-                        if (!_applied)
-                        {
-                            return;
-                        }
-
-                        RestoreOriginalValues();
-                        RefreshBoundValues(_root);
-                        Apply(_root);
-                    },
-                    DispatcherPriority.Loaded);
+                        return;
+                    }
+
+                    _refreshScheduled = true;
+                    _root.Dispatcher.BeginInvoke(
+                        () =>
+                        {
+                            _refreshScheduled = false;
+                            if (!_applied) return;
+                            RestoreOriginalValues();
+                            RefreshBoundValues(_root);
+                            Apply(_root);
+                        },
+                        DispatcherPriority.Loaded);
+                });
             }
🤖 Prompt for AI Agents
In `@BetterGenshinImpact/View/Behavior/AutoTranslateInterceptor.cs` around lines
225 - 252, ScheduleRefresh currently reads/writes _applied and _refreshScheduled
off the UI thread, causing a race; fix it by moving all checks and mutations of
those flags into the UI-dispatched action and use atomic ops for the scheduling
flag: always call _root.Dispatcher.BeginInvoke(...) from ScheduleRefresh (so
threading is normalized), then inside the dispatched lambda check the current
value of _applied and use Interlocked.Exchange(ref _refreshScheduled, 1) (or
Interlocked.CompareExchange) to detect/claim scheduling and set it back to 0
when done, then call RestoreOriginalValues, RefreshBoundValues(_root) and
Apply(_root); this keeps _applied/_refreshScheduled accesses on the UI thread
and avoids races between WeakReferenceMessenger callbacks and dispatcher
execution.

- 新增 IMissingTranslationReporter 接口及 SupabaseMissingTranslationReporter 实现
- 在 JsonTranslationService 中集成缺失翻译上报逻辑
- 添加缺失翻译收集的配置设置(MissingTranslationCollectionSettings)
- 优化缺失翻译文件的序列化格式,将 Source 字段改为紧凑的数字表示
- 移除 ScriptRepoUpdater 中未使用的 using 语句
- 在 App.xaml.cs 中注册 SupabaseMissingTranslationReporter 服务
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@BetterGenshinImpact/Service/JsonTranslationService.cs`:
- Around line 296-398: 这些方法在 JsonTranslationService 和
SupabaseMissingTranslationReporter 中逐行重复:NormalizeSourceInfo,
NormalizeSourceInfoForMissing, StripSource, MergeSourceInfo, IsSameSourceInfo,
SourceToCompactString;请把它们提取到一个公共静态工具类(例如
TranslationSourceInfoHelper),在该类中保留原签名和行为(包括返回 TranslationSourceInfo、参数可空处理与
StringComparison.Ordinal 比较、Source 转 compact 字符串使用
CultureInfo.InvariantCulture),然后将 JsonTranslationService 和
SupabaseMissingTranslationReporter 中对应的方法删除并改为调用
TranslationSourceInfoHelper.NormalizeSourceInfo/NormalizeSourceInfoForMissing/StripSource/MergeSourceInfo/IsSameSourceInfo/SourceToCompactString,以消除重复并保持原有逻辑不变。

In `@BetterGenshinImpact/Service/MissingTranslationCollectionSettings.cs`:
- Around line 5-21: The MissingTranslationCollectionSettings class currently
hardcodes Enabled = true and secrets (SupabaseApiKey/SupabaseUrl) — change it to
read SupabaseUrl, SupabaseApiKey, Table, BatchSize and FlushInterval from
configuration or environment variables and set Enabled default to false (or
derive Enabled from a user-facing opt-in setting) so users can opt out at
runtime; update the IsValid property to validate the config/env values instead
of literals and ensure no secret values are committed in source (rotate to use
secure config provider or environment-only values and provide sensible
defaults/fallbacks where appropriate).

In `@BetterGenshinImpact/Service/SupabaseMissingTranslationReporter.cs`:
- Around line 58-99: WorkerAsync currently cancels immediately on Dispose and
loses aggregated items; add a final drain-and-flush before exit by moving the
pending handling into a finally block (or adding a finally after the try/catch)
that: 1) drains remaining events from _channel.Reader (use TryRead in a loop,
using MakeKey and MissingTranslationUpsertRow.Merge to aggregate into the
existing pending dictionary), 2) checks
MissingTranslationCollectionSettings.IsValid, and 3) calls FlushAsync(pending,
CancellationToken.None or a short timeout token) to persist any remaining
entries (use the existing FlushAsync and pending dictionary), then clear
pending; ensure exceptions are still handled but that this final flush runs even
after OperationCanceledException so data isn't lost.
🧹 Nitpick comments (5)
BetterGenshinImpact/BetterGenshinImpact.csproj (1)

198-200: User\I18n\en.json 的拷贝策略与 User\** 通配规则存在冲突。

第 183-185 行已将 User\** 设为 CopyToOutputDirectory=Always,此处又将 User\I18n\en.json 单独设为 PreserveNewest。MSBuild 通常以更具体的规则优先,因此 en.json 最终是 PreserveNewest。如果这是有意为之(避免每次构建覆盖用户可能编辑的翻译文件),建议添加注释说明原因,否则容易在后续维护中被误解。

BetterGenshinImpact/Service/JsonTranslationService.cs (3)

105-126: EnsureMapLoaded_loadedCultureName 的跨线程可见性。

_loadedCultureName 在第 107 行被无锁读取(快速路径),但在 lock 内部赋值(第 122 行)。虽然 .NET x86/x64 内存模型下引用赋值通常可见,但严格来说缺少 volatile 语义可能导致编译器/JIT 优化问题。建议将 _loadedCultureName 声明为 volatile,或使用 Volatile.Read/Volatile.Write

建议修改
-    private string _loadedCultureName = string.Empty;
+    private volatile string _loadedCultureName = string.Empty;

434-445: CJK 检测范围可能不足。

ContainsCjk 仅覆盖了 CJK 统一汉字基本区 (U+4E00–U+9FFF) 和扩展 A 区 (U+3400–U+4DBF),未涵盖 CJK 兼容汉字 (U+F900–U+FAFF) 及常用标点符号区。对于主要处理简繁体中文的场景基本足够,但如果有日文汉字或兼容字符的翻译需求,可能遗漏。标记为可选改进。


401-416: WriteAtomicallyFile.Move 时存在轻微竞态。

第 408 行的 File.Exists 检查和第 414 行的 File.Move 之间,如果另一个进程/线程创建了同名文件,File.Move 会抛异常。可以直接使用 File.Move(tmp, filePath, overwrite: true)(.NET 6+)消除此竞态,同时简化代码逻辑。

简化方案(.NET 6+)
-        if (File.Exists(filePath))
-        {
-            File.Replace(tmp, filePath, null);
-        }
-        else
-        {
-            File.Move(tmp, filePath);
-        }
+        File.Move(tmp, filePath, overwrite: true);
BetterGenshinImpact/Service/SupabaseMissingTranslationReporter.cs (1)

186-198: _worker.Wait(TimeSpan) 会同步阻塞调用线程。

如果 Dispose 在 UI 线程上被调用(例如应用关闭期间),_worker.Wait() 会阻塞 UI 线程最多 1 秒。由于这是应用退出路径,影响有限,但值得留意。

Comment on lines +296 to +398

private static TranslationSourceInfo NormalizeSourceInfo(TranslationSourceInfo? sourceInfo)
{
if (sourceInfo == null)
{
return TranslationSourceInfo.From(MissingTextSource.Unknown);
}

return new TranslationSourceInfo
{
Source = sourceInfo.Source,
ViewXamlPath = sourceInfo.ViewXamlPath,
ViewType = sourceInfo.ViewType,
ElementType = sourceInfo.ElementType,
ElementName = sourceInfo.ElementName,
PropertyName = sourceInfo.PropertyName,
BindingPath = sourceInfo.BindingPath,
Notes = sourceInfo.Notes
};
}

private static TranslationSourceInfo NormalizeSourceInfoForMissing(TranslationSourceInfo? sourceInfo)
{
return StripSource(NormalizeSourceInfo(sourceInfo));
}

private static TranslationSourceInfo StripSource(TranslationSourceInfo sourceInfo)
{
return new TranslationSourceInfo
{
Source = MissingTextSource.Unknown,
ViewXamlPath = sourceInfo.ViewXamlPath,
ViewType = sourceInfo.ViewType,
ElementType = sourceInfo.ElementType,
ElementName = sourceInfo.ElementName,
PropertyName = sourceInfo.PropertyName,
BindingPath = sourceInfo.BindingPath,
Notes = sourceInfo.Notes
};
}

private static TranslationSourceInfo MergeSourceInfo(TranslationSourceInfo? existing, TranslationSourceInfo? incoming)
{
if (incoming == null)
{
return NormalizeSourceInfo(existing);
}

if (existing == null)
{
return NormalizeSourceInfo(incoming);
}

if (existing.Source == MissingTextSource.Unknown && incoming.Source != MissingTextSource.Unknown)
{
return incoming;
}

if (existing.Source != MissingTextSource.Unknown && incoming.Source == MissingTextSource.Unknown)
{
return existing;
}

if (existing.Source != incoming.Source)
{
return existing;
}

var merged = new TranslationSourceInfo
{
Source = existing.Source,
ViewXamlPath = string.IsNullOrWhiteSpace(existing.ViewXamlPath) ? incoming.ViewXamlPath : existing.ViewXamlPath,
ViewType = string.IsNullOrWhiteSpace(existing.ViewType) ? incoming.ViewType : existing.ViewType,
ElementType = string.IsNullOrWhiteSpace(existing.ElementType) ? incoming.ElementType : existing.ElementType,
ElementName = string.IsNullOrWhiteSpace(existing.ElementName) ? incoming.ElementName : existing.ElementName,
PropertyName = string.IsNullOrWhiteSpace(existing.PropertyName) ? incoming.PropertyName : existing.PropertyName,
BindingPath = string.IsNullOrWhiteSpace(existing.BindingPath) ? incoming.BindingPath : existing.BindingPath,
Notes = string.IsNullOrWhiteSpace(existing.Notes) ? incoming.Notes : existing.Notes
};

if (IsSameSourceInfo(merged, existing))
{
return existing;
}

return merged;
}

private static bool IsSameSourceInfo(TranslationSourceInfo left, TranslationSourceInfo right)
{
return left.Source == right.Source
&& string.Equals(left.ViewXamlPath, right.ViewXamlPath, StringComparison.Ordinal)
&& string.Equals(left.ViewType, right.ViewType, StringComparison.Ordinal)
&& string.Equals(left.ElementType, right.ElementType, StringComparison.Ordinal)
&& string.Equals(left.ElementName, right.ElementName, StringComparison.Ordinal)
&& string.Equals(left.PropertyName, right.PropertyName, StringComparison.Ordinal)
&& string.Equals(left.BindingPath, right.BindingPath, StringComparison.Ordinal)
&& string.Equals(left.Notes, right.Notes, StringComparison.Ordinal);
}

private static string SourceToCompactString(MissingTextSource source)
{
return ((int)source).ToString(CultureInfo.InvariantCulture);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

NormalizeSourceInfoStripSourceMergeSourceInfoIsSameSourceInfoSourceToCompactStringSupabaseMissingTranslationReporter 完全重复。

这五个静态方法在 JsonTranslationServiceSupabaseMissingTranslationReporter 中逐行一致。建议提取到一个共享的静态帮助类(如 TranslationSourceInfoHelper),避免维护两份相同逻辑时产生不一致。

#!/bin/bash
# Verify the duplication between the two files
echo "=== JsonTranslationService NormalizeSourceInfo ==="
rg -n "private static TranslationSourceInfo (NormalizeSourceInfo|NormalizeSourceInfoForMissing|StripSource|MergeSourceInfo)" --type=cs

echo "=== IsSameSourceInfo ==="
rg -n "private static bool IsSameSourceInfo" --type=cs

echo "=== SourceToCompactString ==="
rg -n "private static string SourceToCompactString" --type=cs
🤖 Prompt for AI Agents
In `@BetterGenshinImpact/Service/JsonTranslationService.cs` around lines 296 -
398, 这些方法在 JsonTranslationService 和 SupabaseMissingTranslationReporter
中逐行重复:NormalizeSourceInfo, NormalizeSourceInfoForMissing, StripSource,
MergeSourceInfo, IsSameSourceInfo, SourceToCompactString;请把它们提取到一个公共静态工具类(例如
TranslationSourceInfoHelper),在该类中保留原签名和行为(包括返回 TranslationSourceInfo、参数可空处理与
StringComparison.Ordinal 比较、Source 转 compact 字符串使用
CultureInfo.InvariantCulture),然后将 JsonTranslationService 和
SupabaseMissingTranslationReporter 中对应的方法删除并改为调用
TranslationSourceInfoHelper.NormalizeSourceInfo/NormalizeSourceInfoForMissing/StripSource/MergeSourceInfo/IsSameSourceInfo/SourceToCompactString,以消除重复并保持原有逻辑不变。

Comment on lines +5 to +21
public static class MissingTranslationCollectionSettings
{
public static readonly bool Enabled = true;
public static readonly string SupabaseUrl = "https://obwddvnwzaolbdawduxg.supabase.co";
public static readonly string SupabaseApiKey = "sb_publishable_PyvQSxxCi02aawC-G6vtgg_wzOctlgm";
public static readonly string Table = "translation_missing";
public static readonly int BatchSize = 200;
public static readonly TimeSpan FlushInterval = TimeSpan.FromSeconds(5);

public static bool IsValid =>
Enabled
&& !string.IsNullOrWhiteSpace(SupabaseUrl)
&& !string.IsNullOrWhiteSpace(SupabaseApiKey)
&& !string.IsNullOrWhiteSpace(Table)
&& BatchSize > 0
&& FlushInterval > TimeSpan.Zero;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

API 密钥硬编码在源码中,且缺少用户 opt-out 机制。

  1. 隐私/合规风险Enabled = true 硬编码且无运行时开关,意味着所有用户都会自动向 Supabase 上报缺失翻译数据(包含 UI 文本和来源信息)。用户无法知情或选择退出,这在部分地区(如 GDPR/CCPA 覆盖范围)可能存在合规风险。建议将 Enabled 与用户可见的设置项关联,或至少默认关闭。

  2. 密钥管理:尽管前缀 sb_publishable_ 暗示这是公开密钥,但将其硬编码在源码中仍不符合最佳实践。后续如需轮换密钥,需要发布新版本才能生效。建议从配置文件或环境变量中读取。

🤖 Prompt for AI Agents
In `@BetterGenshinImpact/Service/MissingTranslationCollectionSettings.cs` around
lines 5 - 21, The MissingTranslationCollectionSettings class currently hardcodes
Enabled = true and secrets (SupabaseApiKey/SupabaseUrl) — change it to read
SupabaseUrl, SupabaseApiKey, Table, BatchSize and FlushInterval from
configuration or environment variables and set Enabled default to false (or
derive Enabled from a user-facing opt-in setting) so users can opt out at
runtime; update the IsValid property to validate the config/env values instead
of literals and ensure no secret values are committed in source (rotate to use
secure config provider or environment-only values and provide sensible
defaults/fallbacks where appropriate).

Comment on lines +58 to +99
private async Task WorkerAsync(CancellationToken token)
{
var pending = new Dictionary<string, MissingTranslationUpsertRow>(StringComparer.Ordinal);

using var timer = new PeriodicTimer(MissingTranslationCollectionSettings.FlushInterval);

try
{
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
{
while (_channel.Reader.TryRead(out var ev))
{
var k = MakeKey(ev.Language, ev.Key);
if (!pending.TryGetValue(k, out var existing))
{
pending[k] = new MissingTranslationUpsertRow(ev.Language, ev.Key, ev.Source, ev.SourceInfo);
continue;
}

pending[k] = existing.Merge(ev.Source, ev.SourceInfo);
}

if (!MissingTranslationCollectionSettings.IsValid)
{
pending.Clear();
continue;
}

if (pending.Count > 0)
{
await FlushAsync(pending, token).ConfigureAwait(false);
}
}
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Worker 关闭时未排空待处理数据。

Dispose() 调用 _cts.Cancel() 后,PeriodicTimer.WaitForNextTickAsync 立即抛出 OperationCanceledException,导致 pending 字典中已聚合的数据以及 channel 中尚未读取的事件全部丢失。对于缺失翻译上报场景,偶尔丢失可接受,但如果希望尽量完整上报,可以在 catch 块后添加一次最终排空和刷新。

建议:在 worker 退出前做最终 flush
         catch (OperationCanceledException)
         {
         }
         catch (Exception ex)
         {
             Debug.WriteLine(ex);
         }
+        finally
+        {
+            // 最终排空 channel 中剩余事件并尝试上报
+            while (_channel.Reader.TryRead(out var ev))
+            {
+                var k = MakeKey(ev.Language, ev.Key);
+                if (!pending.TryGetValue(k, out var existing))
+                {
+                    pending[k] = new MissingTranslationUpsertRow(ev.Language, ev.Key, ev.Source, ev.SourceInfo);
+                    continue;
+                }
+                pending[k] = existing.Merge(ev.Source, ev.SourceInfo);
+            }
+            if (pending.Count > 0 && MissingTranslationCollectionSettings.IsValid)
+            {
+                try { await FlushAsync(pending, CancellationToken.None).ConfigureAwait(false); }
+                catch { /* best-effort */ }
+            }
+        }
🤖 Prompt for AI Agents
In `@BetterGenshinImpact/Service/SupabaseMissingTranslationReporter.cs` around
lines 58 - 99, WorkerAsync currently cancels immediately on Dispose and loses
aggregated items; add a final drain-and-flush before exit by moving the pending
handling into a finally block (or adding a finally after the try/catch) that: 1)
drains remaining events from _channel.Reader (use TryRead in a loop, using
MakeKey and MissingTranslationUpsertRow.Merge to aggregate into the existing
pending dictionary), 2) checks MissingTranslationCollectionSettings.IsValid, and
3) calls FlushAsync(pending, CancellationToken.None or a short timeout token) to
persist any remaining entries (use the existing FlushAsync and pending
dictionary), then clear pending; ensure exceptions are still handled but that
this final flush runs even after OperationCanceledException so data isn't lost.

- 移除未使用的法语翻译支持以简化语言选项
- 修复 Supabase 报告序列化时移除冗余字段
- 添加已缺失翻译键的缓存以避免重复报告
- 重构自动翻译拦截器,将原始值存储移至依赖属性
- 修复原始值恢复逻辑,确保正确遍历所有子元素
- 在 ITranslationService 接口中添加 Reload 方法
- 在 JsonTranslationService 中实现 Reload 方法,支持重新加载语言文件并发送变更通知
- 在通用设置页面添加“更新”按钮,点击后从远程仓库下载最新语言文件
- 实现 OnUpdateUiLanguageAsync 命令,支持从 GitHub 和镜像源下载语言文件
- 下载后自动替换本地文件并重新加载翻译服务
# Conflicts:
#	BetterGenshinImpact/App.xaml
#	BetterGenshinImpact/BetterGenshinImpact.csproj
@huiyadanli huiyadanli merged commit 9f67726 into main Feb 15, 2026
13 of 14 checks passed
kaedelcb pushed a commit to kaedelcb/better-genshin-impact that referenced this pull request Feb 23, 2026
* feat(i18n): 添加界面与日志的国际化支持

- 新增 ITranslationService 接口及 JsonTranslationService 实现,提供基于 JSON 的翻译服务
- 添加 TrConverter 转换器,支持通过绑定动态翻译界面文本
- 引入 AutoTranslateInterceptor 行为,自动扫描并翻译界面中的静态文本
- 集成 TranslatingSerilogLoggerProvider,实现日志输出的实时翻译
- 在 App.xaml 中注册全局样式,为 Window、UserControl 和 Page 启用自动翻译

* refactor(AutoTranslateInterceptor): 优化自动翻译拦截器的加载与应用机制

- 移除 HomePage 中冗余的 EnableAutoTranslate 属性设置,改为继承属性
- 通过类构造函数注册全局 Loaded 事件处理器,替代在每个元素上单独添加
- 引入请求队列机制,批量处理待应用翻译的元素,避免重复调度
- 扩展属性类型检查,支持 object 类型以处理更多动态内容场景

* fix: 移除全局自动翻译拦截器以避免冲突

移除在 App.xaml 中为 Window、UserControl 和 Page 全局设置的 AutoTranslateInterceptor,
改为仅在 PickerWindow 中显式启用。这解决了全局样式可能导致的意外行为或冲突。

* feat(ui): 为多个窗口启用自动翻译拦截器

为 MapLabelSearchWindow、ArtifactOcrDialog、PromptDialog 等 14 个窗口添加了 AutoTranslateInterceptor.EnableAutoTranslate 属性,以启用自动翻译拦截功能。

* feat(i18n): 添加国际化目录支持并优化异常处理

* feat(ui): 添加软件UI语言设置并改进翻译服务

- 在通用设置页面新增UI语言选择控件,支持动态切换界面语言
- 修改游戏语言标签为“原神游戏语言”以明确区分
- 改进JsonTranslationService,支持UI语言切换时的实时翻译更新
- 优化AutoTranslateInterceptor,缓存原始文本值并在语言切换时恢复
- 添加属性变更监听机制,确保UI元素在语言切换后正确刷新

* feat(自动翻译): 添加排除自动翻译的依赖属性

在 AutoTranslateInterceptor 中新增 ExcludeAutoTranslate 附加属性,允许对特定依赖对象禁用自动翻译功能。当遍历元素进行翻译时,会检查此属性并跳过已标记排除的元素。

* feat(translation): 为缺失文本翻译添加详细上下文信息

扩展翻译服务以收集缺失文本的详细上下文,包括视图路径、元素类型、属性名等。
重构 `ITranslationService` 接口,引入 `TranslationSourceInfo` 类封装上下文信息。
修改 `AutoTranslateInterceptor` 自动收集 UI 元素信息,`JsonTranslationService` 合并多来源上下文。

* Revert "feat(自动翻译): 添加排除自动翻译的依赖属性"

This reverts commit a1c2334.

* fix: 跳过 GridViewRowPresenter 中的文本翻译

添加 IsInGridViewRowPresenter 检查,避免在 GridViewRowPresenter 控件内进行自动翻译,防止潜在的界面显示问题。

* fix: 修复自动翻译拦截器在组合框上下文中的误触发

在自动翻译拦截器中添加了 IsInComboBoxContext 方法,用于检测依赖对象是否处于 ComboBox 或其相关弹出菜单的上下文中。当检测到对象位于组合框上下文时,跳过自动翻译逻辑,避免对下拉选项等界面元素进行不必要的翻译操作,从而解决潜在的界面干扰问题。

* feat(translation): 添加缺失翻译上报至 Supabase 的功能

- 新增 IMissingTranslationReporter 接口及 SupabaseMissingTranslationReporter 实现
- 在 JsonTranslationService 中集成缺失翻译上报逻辑
- 添加缺失翻译收集的配置设置(MissingTranslationCollectionSettings)
- 优化缺失翻译文件的序列化格式,将 Source 字段改为紧凑的数字表示
- 移除 ScriptRepoUpdater 中未使用的 using 语句
- 在 App.xaml.cs 中注册 SupabaseMissingTranslationReporter 服务

* fix: 修复自动翻译功能中原始值恢复和重复报告问题

- 移除未使用的法语翻译支持以简化语言选项
- 修复 Supabase 报告序列化时移除冗余字段
- 添加已缺失翻译键的缓存以避免重复报告
- 重构自动翻译拦截器,将原始值存储移至依赖属性
- 修复原始值恢复逻辑,确保正确遍历所有子元素

* feat(ui): 添加更新UI语言文件功能

- 在 ITranslationService 接口中添加 Reload 方法
- 在 JsonTranslationService 中实现 Reload 方法,支持重新加载语言文件并发送变更通知
- 在通用设置页面添加“更新”按钮,点击后从远程仓库下载最新语言文件
- 实现 OnUpdateUiLanguageAsync 命令,支持从 GitHub 和镜像源下载语言文件
- 下载后自动替换本地文件并重新加载翻译服务
DR-lin-eng added a commit to DR-lin-eng/better-genshin-impact that referenced this pull request Feb 27, 2026
* feat: 优化进出千星速度 (babalae#2682)

* fix adventurers guild path (babalae#2690)

* 七圣召唤不再等待吞星之鲸加血 (babalae#2679)

* feat: 添加bgi版本获取 (babalae#2692)

* 修复切换队伍拾取初始队伍为空的异常识别 (babalae#2688)

* fix: 修复自动剧情有时无法完成对话的问题 (babalae#2684)

* 地图遮罩功能(把米游社大地图搬到原神内) (babalae#2655)

* Update version to 0.55.2-alpha.1

* 修复部分组件未随主题变更颜色 babalae#2644 (babalae#2699)

* feat: 为多个窗口启用自动翻译拦截器

为 MapLabelSearchWindow、ArtifactOcrDialog、PromptDialog 等十三个 WPF 窗口添加了 AutoTranslateInterceptor 行为,以启用自动翻译功能。这确保了应用程序界面的文本内容在需要时可以被正确拦截并处理,提升国际化支持。

* Revert "feat: 为多个窗口启用自动翻译拦截器"

This reverts commit fc942bd.

* fix: 调整截图预览区域尺寸以适应不同分辨率

调整 MaskWindow 中截图预览区域的列宽比例,将固定宽高改为最大宽高并启用拉伸对齐,提升在不同屏幕分辨率下的显示适应性。

* fix: 调整日志级别及UI元素位置比例

将米游社地图接口调用失败日志级别从Warning降为Debug,避免频繁警告干扰
调整日志文本框和状态列表的顶部位置及宽度比例,优化界面布局

* fix: 修复地图标签搜索窗口DPI缩放和关闭逻辑

修复地图标签搜索窗口在高DPI缩放下的定位问题,调整计算方式以适配系统缩放比例。
同时确保地图点选择器关闭时同步隐藏搜索窗口,避免窗口残留。

* Update version to 0.55.2-alpha.2

* Revise asset instructions and GitHub Actions setup

Updated README to reflect changes in asset handling and GitHub Actions.

* 优化换队拾取和切换队伍 (babalae#2729)

* 优化更新界面UI与交互

* Update version to 0.56.0

* 遮罩内 Popup 保持置顶

* fix: update BetterGI.Assets.Other package version to 1.0.12

* Update version to 0.56.1

* feat: 简化自动战斗日志输出 (babalae#2732)

* Revert "遮罩内 Popup 保持置顶"

This reverts commit ff3e89a.

* fix: 调整事件触发顺序并修复地图点选择器状态

修复 PointsCanvas 中 ViewportChanged 事件与 Refresh 方法的调用顺序,确保界面正确更新。
在 MaskWindow 中增加关闭地图点选择器的逻辑,避免状态不一致。

* 重置选项移动

* Update version to 0.56.2

* feat: 调整时间使用更短的延时 (babalae#2738)

* fix: 修复 JS 脚本 multi-checkbox 配置项删除后仍能读取到旧值的问题 (babalae#2725)

* fix: 修复 RunMulti 中 Mat 所有权语义不明确导致的潜在 Dispose 问题 (babalae#2730)

* feat: 支持仓库使用公共资源 (babalae#2716)

* feat: 状态机 (babalae#2712)

* UI变化了后的10s内则所有触发器执行一遍,修复遮罩控件部分场景下残留的问题

* 优化换队拾取 (babalae#2748)

* 优化换队拾取

需要换队拾取拾取时才执行换队拾取操作

* Add files via upload

* 地图遮罩开关实时生效

* fix: 每日委托领奖使用点击屏幕 (babalae#2761)

* fix: 键鼠回调改为异步事件分发 (babalae#2741)

* fix: 键鼠回调改为异步事件分发

* 添加队列限制与并行锁

* Update BetterGenshinImpact/Core/Script/Dependence/KeyMouseHook.cs

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* 只允许外部dispose

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix: 阻止默认的委托和派遣橙色点击 (babalae#2762)

* Fix rare welkin moon stuck issue (babalae#2765)

* Update version to 0.56.3-alpha.1

* fix: 日志遮罩扩大行数显示限制 (babalae#2773)

* 触发器多次触发改为30s

* hide map mask on run task babalae/bettergi-scripts-list#2870

* feat: 修改仓库页面构建地址 (babalae#2778)

* feat: 优化地图遮罩UI,添加快捷键 (babalae#2775)

* 修改二次拾取,使万叶能再次拾取。 (babalae#2760)

* feat: 显示后台角色技能cd (babalae#2754)

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: 辉鸭蛋 <huiyadanli@gmail.com>

* Update version to 0.56.3-alpha.2

* feat:AutoDomain支持领奖时切换20/40原粹树脂,仅JS可用 (babalae#2783)

* feat:AutoDomain支持领奖时切换20/40原粹树脂,仅JS可用

* fix bug

* chore

* fix:捕获重试耗尽异常,GameTaskManager资源重载

* Update README.md

* 新增:自动地脉花独立任务 (babalae#2583)

* 新增自动地脉花任务代码

* 新增自动地脉花任务资源

* 优化自动地脉花界面文案

* 删除地脉花旧脚本与无用资源

* 完善自动地脉花说明与任务日志

* 完善一条龙地脉花配置与教程入口

* feat: Enhance AutoLeyLineOutcrop functionality and add new pathing data

- Refactored combat text detection logic to improve accuracy.
- Added new regions and positions for "挪德卡莱" in LeyLineOutcropData.json.
- Introduced new pathing files for "挪德卡莱4-望崖营垒" and "挪德卡莱5-厄布拉神柱".
- Updated config.json with new strategies and coordinates for enhanced navigation.
- Improved error handling during party switch operations.
- Removed outdated README.md file from pathing assets.

* 修复一些日志打印

* 移动所有assets文件

* feat: clear drawn content in VisionContext before activating mask window

---------

Co-authored-by: 辉鸭蛋 <huiyadanli@gmail.com>

* Update version to 0.56.3-alpha.3

* 默认蒙德

* feat: 补充挪德卡莱区域的地脉花独立任务路径数据 (babalae#2790)

* I18n v2 (babalae#2709)

* feat(i18n): 添加界面与日志的国际化支持

- 新增 ITranslationService 接口及 JsonTranslationService 实现,提供基于 JSON 的翻译服务
- 添加 TrConverter 转换器,支持通过绑定动态翻译界面文本
- 引入 AutoTranslateInterceptor 行为,自动扫描并翻译界面中的静态文本
- 集成 TranslatingSerilogLoggerProvider,实现日志输出的实时翻译
- 在 App.xaml 中注册全局样式,为 Window、UserControl 和 Page 启用自动翻译

* refactor(AutoTranslateInterceptor): 优化自动翻译拦截器的加载与应用机制

- 移除 HomePage 中冗余的 EnableAutoTranslate 属性设置,改为继承属性
- 通过类构造函数注册全局 Loaded 事件处理器,替代在每个元素上单独添加
- 引入请求队列机制,批量处理待应用翻译的元素,避免重复调度
- 扩展属性类型检查,支持 object 类型以处理更多动态内容场景

* fix: 移除全局自动翻译拦截器以避免冲突

移除在 App.xaml 中为 Window、UserControl 和 Page 全局设置的 AutoTranslateInterceptor,
改为仅在 PickerWindow 中显式启用。这解决了全局样式可能导致的意外行为或冲突。

* feat(ui): 为多个窗口启用自动翻译拦截器

为 MapLabelSearchWindow、ArtifactOcrDialog、PromptDialog 等 14 个窗口添加了 AutoTranslateInterceptor.EnableAutoTranslate 属性,以启用自动翻译拦截功能。

* feat(i18n): 添加国际化目录支持并优化异常处理

* feat(ui): 添加软件UI语言设置并改进翻译服务

- 在通用设置页面新增UI语言选择控件,支持动态切换界面语言
- 修改游戏语言标签为“原神游戏语言”以明确区分
- 改进JsonTranslationService,支持UI语言切换时的实时翻译更新
- 优化AutoTranslateInterceptor,缓存原始文本值并在语言切换时恢复
- 添加属性变更监听机制,确保UI元素在语言切换后正确刷新

* feat(自动翻译): 添加排除自动翻译的依赖属性

在 AutoTranslateInterceptor 中新增 ExcludeAutoTranslate 附加属性,允许对特定依赖对象禁用自动翻译功能。当遍历元素进行翻译时,会检查此属性并跳过已标记排除的元素。

* feat(translation): 为缺失文本翻译添加详细上下文信息

扩展翻译服务以收集缺失文本的详细上下文,包括视图路径、元素类型、属性名等。
重构 `ITranslationService` 接口,引入 `TranslationSourceInfo` 类封装上下文信息。
修改 `AutoTranslateInterceptor` 自动收集 UI 元素信息,`JsonTranslationService` 合并多来源上下文。

* Revert "feat(自动翻译): 添加排除自动翻译的依赖属性"

This reverts commit a1c2334.

* fix: 跳过 GridViewRowPresenter 中的文本翻译

添加 IsInGridViewRowPresenter 检查,避免在 GridViewRowPresenter 控件内进行自动翻译,防止潜在的界面显示问题。

* fix: 修复自动翻译拦截器在组合框上下文中的误触发

在自动翻译拦截器中添加了 IsInComboBoxContext 方法,用于检测依赖对象是否处于 ComboBox 或其相关弹出菜单的上下文中。当检测到对象位于组合框上下文时,跳过自动翻译逻辑,避免对下拉选项等界面元素进行不必要的翻译操作,从而解决潜在的界面干扰问题。

* feat(translation): 添加缺失翻译上报至 Supabase 的功能

- 新增 IMissingTranslationReporter 接口及 SupabaseMissingTranslationReporter 实现
- 在 JsonTranslationService 中集成缺失翻译上报逻辑
- 添加缺失翻译收集的配置设置(MissingTranslationCollectionSettings)
- 优化缺失翻译文件的序列化格式,将 Source 字段改为紧凑的数字表示
- 移除 ScriptRepoUpdater 中未使用的 using 语句
- 在 App.xaml.cs 中注册 SupabaseMissingTranslationReporter 服务

* fix: 修复自动翻译功能中原始值恢复和重复报告问题

- 移除未使用的法语翻译支持以简化语言选项
- 修复 Supabase 报告序列化时移除冗余字段
- 添加已缺失翻译键的缓存以避免重复报告
- 重构自动翻译拦截器,将原始值存储移至依赖属性
- 修复原始值恢复逻辑,确保正确遍历所有子元素

* feat(ui): 添加更新UI语言文件功能

- 在 ITranslationService 接口中添加 Reload 方法
- 在 JsonTranslationService 中实现 Reload 方法,支持重新加载语言文件并发送变更通知
- 在通用设置页面添加“更新”按钮,点击后从远程仓库下载最新语言文件
- 实现 OnUpdateUiLanguageAsync 命令,支持从 GitHub 和镜像源下载语言文件
- 下载后自动替换本地文件并重新加载翻译服务

* Update version to 0.56.3-alpha.5

* 背包计数任务更快定位到武器经验材料 (babalae#2793)

* 背包计数任务更快定位到武器经验材料

* 防止任务取消时鼠标按钮可能卡在按下状态

* 删除无用日志

* fix: 修复命令行参数检测和日志翻译的条件逻辑

- 命令行参数检测改为不区分大小写,确保参数匹配的可靠性
- 调试模式下禁用日志翻译,避免开发过程中的翻译干扰
- 仅非中文界面启用翻译日志提供程序,中文界面使用标准日志

* Update version to 0.56.3-alpha.6

* fix: 地脉花任务捕获任务取消异常并调整日志级别

将地脉花自动任务中的特定异常捕获扩展为包含 TaskCanceledException,防止任务意外终止。同时将退出奖励界面的失败日志从 Warning 降级为 Debug,避免在正常任务取消时产生干扰性日志。

* fix(自动地脉花): 更新图标并改进异常处理日志

- 将自动地脉花功能图标从原图标更改为新图标
- 将自动地脉花任务失败时的日志级别从 Error 调整为 Debug,并添加简化的 Error 日志
- 在自动战斗任务结束时忽略 NormalEndException 和 TaskCanceledException 异常

* 开启地脉花之前的OCR加入延迟

* feat(script): 向脚本引擎添加新的Bv视觉类

添加BvPage、BvLocator和BvImage类型到脚本引擎的宿主对象中,扩展脚本的视觉识别能力。

* 尝试修复可能存在的角色识别错误问题

* 修复后台点击时 “选择对话选项前的延迟” 不生效的问题

* chore: add AGENTS.md to .gitignore (babalae#2798)

* 更正自动拾取掉落物为扫描掉落物光柱 (babalae#2795)

* 更正自动拾取掉落物为扫描掉落物光柱

原表述易引起误会,以为捡东西就要开启,改为更明确的扫描掉落物光柱以说明其功能
同时,该方法仅作为无万叶和琴时的备选方案,效果原差于直接使用聚集材料动作,将原”与万叶配合更佳“更正为”仅在无万叶时备用“

* Update en.json

更正en.json

* 补充地脉花教程url

* Update version to 0.56.3-alpha.7

* Update version to 0.56.3-alpha.8

* Update version to 0.56.4-alpha.1

* Update version to 0.56.4-alpha.2

* Update version to 0.56.4-alpha.3

* Update version to 0.56.4-alpha.4

* Update version to 0.57.0

* 尝试修复一些ROI越界 (babalae#2808)

* fix: 修复多处 OpenCV ROI 越界导致的断言失败

在低分辨率(如 1280x720)下,多处 Rect 坐标计算未做边界保护,
直接传入 SubMat / new Mat(mat, rect) 时触发 OpenCV ROI 断言崩溃。

修复位置:
- Behaviours.cs: fishBoxRect 计算结果钳位到图像边界,修复钓鱼任务越界
- GridScreen.cs: PostProcess 中幻影格子(插值生成)越界时直接丢弃
- ImageRegion.cs: DeriveCrop 两个重载统一加入坐标钳位与有效性校验
- GetGridIconsTask.cs: CropResizeArtifactSetFilterGridIcon X/Y 坐标加非负保护
- GeniusInvokationControl.cs: 角色区域扩展和 HP 区域 Y 偏移各加边界保护

* chore: 为 AutoFishingTask 鱼饵图标裁剪补充说明注释

* refactor: 提取 Rect 钳位逻辑为共享扩展方法 ClampTo

将 6 处重复的 ROI 钳位代码统一为 CommonExtension.ClampTo 扩展方法,
采用交集语义(坐标钳位时宽高同步缩减,不会扩大矩形)。
删除 AutoLeyLineOutcropTask 中的私有 ClampRect 方法。

* 文本识别的模糊匹配功能 (babalae#2799)

* chore: add AGENTS.md to .gitignore

* feat(config): 新增 AllowDuplicateChar OCR配置项

* refactor(ocr): Rec 暴露protected成员、提取RunInference、支持AllowDuplicateChar

* feat(ocr): 打通 AllowDuplicateChar 参数链 PaddleOcrService → Rec

* feat(ocr): OcrUtils 新增 CreateLabelDict/CreateWeights 工具方法

* feat(helpers): 新增 LruCache 缓存工具类

* feat(ocr): 新增 RecMatch DP模糊匹配识别器

* test(helpers): 新增 LruCache 单元测试

* test(ocr): 新增 RecMatch.GetTarget / CreateLabelDict 单元测试

* fix(ocr): 修复 RecMatch 中权重矩阵乘法的使用方式

* refactor(ocr): 合并 RecMatch 到 Rec,提取可测试静态方法,补充单元测试

将 RecMatch 子类合并到 Rec 中,消除继承关系和重复的批处理逻辑(提取 RunBatch<T>)。
将 GetTarget 核心逻辑和 GetMaxScoreDP 提取为 OcrUtils 静态方法以便独立测试。
重命名测试文件并新增 16 个单元测试覆盖 MapStringToLabelIndices、GetMaxScoreDP、CreateWeights。

* feat(ocr): 将 Rec.RunMatch 暴露给 JS 引擎和内部 C# 代码

新增 IOcrMatchService 接口,提供基于 DP 模糊匹配的 OcrMatch/OcrMatchDirect 方法,
返回 0~1 置信度分数。PaddleOcrService 实现该接口,OcrFactory.PaddleMatch 保证
非 null 返回(引擎不支持时自动回退到普通 OCR + 编辑距离字符串比较)。
BvPage 新增 OcrMatch/WaitForOcrMatch 供 JS 脚本使用,阈值可通过配置调整。

* feat(ui): 为 OCR 配置添加允许重复字符和模糊匹配阈值的设置项

在通用设置页 OCR 配置区域新增两个控件:
- 允许连续重复字符(AllowDuplicateChar)开关
- OCR模糊匹配阈值(OcrMatchDefaultThreshold)输入框

* fix: 修复 PR babalae#2799 代码审查中发现的多项问题

- 修复 Rec.cs 空文本时 score/sb.Length 除零产生 NaN
- 修复 BvPage.cs rect==default 时同一对象被双重 Dispose
- 移除 Rec.cs Finalizer 避免 GC 线程加锁死锁
- 移除 CacheHelper WeakKey 无效功能,简化为直接 Dictionary 查找
- 添加 weights 数组长度与模型输出维度校验
- 修复 CreateLabelDict 空格标签索引冲突
- 修复 GetMaxScoreDP availableCount=0 除零
- 修复 OcrMatchFallbackService Contains 大小写敏感
- 修复 BvPage.cs DefaultRetryInterval=0 除零
- 添加 OcrMatchDefaultThreshold [0,1] 范围约束
- 提取 PaddleOcrService BGRA→BGR 转换辅助方法
- 使用 Interlocked.CompareExchange 修复 OcrFactory Fallback 线程安全
- 增大 LruCacheTests BuilderTest TTL 裕量避免 CI 不稳定
- 更新 .gitignore 注释

* fix: 修复 OcrMatch 归一化分母导致多区域匹配分数过低的 bug,改进 UI

- 修复 GetMaxScoreFlat 中 availableCount 使用非空图像数作为分母,
  导致多文字区域场景下匹配分数被过度稀释的问题,改为使用 target.Length
- AllowDuplicateChar 设置项添加"需重新加载OCR引擎"的提示
- OCR模糊匹配阈值控件从 TextBox 改为 Slider + 数值显示
- 移除 Det 类中有问题的 finalizer(含锁的析构函数可能导致死锁)
- 补充多区域场景的单元测试

* feat(ocr): 添加队伍切换时使用OcrMatch模糊匹配的选项和相关配置

* fix(ui): 更新匹配成功阈值默认值为 0.8

* fix(ocr): 修复队伍切换逻辑中的空值处理和优化代码结构

* refactor: 简化 LruCache,移除弱引用支持和 Builder 模式

- 移除有 TOCTOU bug 的 WeakReference 支持(且无实际使用方)
- CacheItem 类改为 ValueTuple 减少堆分配
- 无过期时不再赋值 DateTime.MaxValue,过期检查短路跳过
- 移除仅剩两参数的 LruCacheBuilder,直接使用构造函数

* fix(ocr): 修复 CreateWeights 中空格字符权重写入错误索引的 bug

复用 CreateLabelDict 构建索引映射,确保空格映射到 labels.Count+1,
与 CreateLabelDict 保持一致。添加对应测试用例。

* fix(ocr): 修复 GCHandle.Alloc 失败时 finally 中 Free 掩盖原始异常的问题

* fix(ocr): 添加队伍选择按钮存在性检查,避免 PartySetupFailedException

* fix(ocr): 调整 OcrMatchDefaultThreshold 的 TickFrequency 为 0.01

* fix(ocr): 修复区域裁剪逻辑,确保裁剪尺寸不为负值

* fix(ocr): 优化字符置信度提取逻辑,直接按目标字符索引查找置信度

* fix(ocr): 修正变量命名以保持一致性,调整方法名大小写

* fix(ocr): 修改 CreateWeights 方法以使用标签字典和标签计数,优化权重创建逻辑

* fix(ocr): 更新 OCR 置信度阈值设置,确保阈值范围为 0.01 到 0.99,并优化相关逻辑

* feat: 根据文件夹名字和内容重合度区分仓库;启动时自动更新仓库和订阅 (babalae#2767)

* feat: 实现启动时自动更新已订阅脚本及多仓库分离存储

- ScriptConfig: 新增 AutoUpdateSubscribedScripts 配置项
- ScriptRepoUpdater: 动态 CenterRepoPath, 按仓库URL分离存储
  - 内容重合度检测(Jaccard系数)判断仓库异同
  - URL→文件夹名持久化映射(repo_folder_mapping.json)
  - repo_updated.json 存放于各自仓库文件夹内
  - AutoUpdateSubscribedScripts 启动时自动更新订阅脚本
  - 静默同步仓库、渠道URL解析、检出更新脚本
- RepoWebBridge: 使用动态路径, 辅助方法改为 internal
- MainWindowViewModel: 启动时调用自动更新

* feat: 基于内容重合度的导入zip仓库

* perf: 合并默认仓库url映射

* perf: 清理兼容字段

* perf: 添加线程锁以避免并发调用

* fix: 缓存FolderMapping、修复重合度异常返回值、目录扫描异常隔离、移除未使用变量

* perf: 优化更新流程

* perf: 内存缓存添加锁

* fix: 修复更新状态逻辑,确保克隆失败时不标记为已更新

* perf: 文件夹映射先写磁盘再写缓存

* refactor: 简化生成唯一文件夹名称的方法,移除不必要的参数

* fix: ResetRepo加写锁并清理URL映射条目

* perf: 优化重合度算法

* docs: 更新注释

* fix: 仅重置实际更新成功的脚本的 hasUpdate 标记

* feat: 手动一键更新按钮

* feat: 订阅路径迁移至独立文件存储并简化更新逻辑

- 订阅数据从 config.json 迁移到 User/subscriptions/{repo}.json 独立文件
- 添加 ReaderWriterLockSlim 保护订阅文件并发读写
- 使用 System.Text.Json + ConfigService.JsonOptions 序列化
- 新增 RepoWebBridge.GetSubscribedScriptPaths() 桥接方法
- 启动时自动从旧 config.json 迁移订阅数据到独立文件
- 合并手动/自动更新为 UpdateAllSubscribedScriptsCore 共用核心
- 移除 hasUpdate 检查,直接全量更新所有订阅脚本
- 移除冗余 logPrefix 参数

* refactor: 简化启动时自动更新调用

- 移除 Task.Run + try-catch 包装,异常处理已内置于方法中
- 直接使用 fire-and-forget 异步调用

* fix: 订阅目录命名改为 PascalCase (Subscriptions)

* refactor: 移除死代码和冗余中间层方法

* fix: ReadSubscriptionFile 异常时记录日志避免订阅数据静默丢失

* fix: 进度条改为Indeterminate模式、异常日志补全、订阅去重、迁移批量写入、锁注释

* refactor: 提取 ReadFolderMappingFromDisk 消除映射方法嵌套 try

* fix: 补全静态方法异常日志、WriteSubscriptionFile异常保护、ManualUpdate注释

* fix: ManualUpdateSubscribedScripts 加 try-catch 兜底并提示用户重置仓库

* fix: Dialog打开时检测后台自动更新状态,自动禁用按钮并显示进度提示

- ScriptRepoUpdater 新增 IsAutoUpdating 标志和 AutoUpdateStateChanged 事件
- ScriptRepoWindow 订阅事件,自动更新期间显示进度条、禁用所有操作按钮并 Toast 提示
- 更新仓库/重置仓库按钮也加上 IsEnabled 绑定 IsUpdating

* fix: 将自动更新调用包裹在 Task.Run 中避免 UI 线程阻塞

AutoUpdateSubscribedScripts 的 await 后续会被 WPF SynchronizationContext
调度回 UI 线程,导致大量 Git checkout 和文件 IO 操作阻塞界面。
用 Task.Run 确保整个流程在线程池执行。

* fix: 进度条分离 IsProgressIndeterminate 属性,按操作类型正确切换确定/不确定模式

* refactor: 消除 pathing 展开重复逻辑、用布尔字段替换字符串比较追踪状态来源、补全锁注释、统一日志方式

* fix: ExpandTopLevelPaths 泛化展开所有 PathMapper 顶层 key 防止误删用户目录,迁移移入锁内

* fix: 命令行启动配置组/一条龙前先等待自动更新订阅脚本完成

* feat: 添加命令行启动时是否先自动更新选项

* fix: 修复按钮位置

* 去除所有 Cv2.ImRead ,解决中文路径问题

* 修复 babalae#2804,树脂不足时候未终止自动秘境

* Update version to 0.57.1-alpha.1

* fix: 修复迁移订阅触发时机 (babalae#2811)

* 自动拾取排除:「月谕圣牌」收藏

* revert revert babalae#2809 with babalae#2763 , 减轻死锁问题的表现

* 彻底删除 实验功能 遮罩以原神子窗体方式启动 UseSubform

* fix:自动拾取黑名单配置文本框复制剪切时UI短暂无响应 (babalae#2818)

* Revert "feat: 自动拾取文本识别帧间连续性检查 (babalae#2676)" (babalae#2817)

* 支持任意文件名的启动 (babalae#2819)

* feat: 为快速尘歌壶任务增加离开功能,支持进出双向操作 (babalae#2822)

* Update version to 0.57.1-alpha.2

* 开放自动地脉花 JS调用  (babalae#2789)

Co-authored-by: 辉鸭蛋 <huiyadanli@gmail.com>

* feat: JS级联选择 和 秘境选择 滚轮事件禁止穿透至其他窗口 (babalae#2828)

* 新增小地图遮罩,在小地图上展示资源点位 (babalae#2830)

* Update version to 0.57.1-alpha.3

* refactor: 简化进程去重逻辑,直接使用 GroupBy 和 Select

使用 GroupBy 和 Select 直接获取唯一的进程对象,避免创建中间字典和手动释放重复的 Process 对象,使代码更简洁。

* fix: 修复小地图遮罩在主界面外的显示逻辑

* 修复七圣召唤月之五伊涅芙会导致手牌不可调和的死循环 (babalae#2848)

* feat: 自动地脉花独立战斗配置与OCR遮罩 (babalae#2829)

* 七圣召唤配置文件中可以设置骰子数量增减 (babalae#2832)

* auto tcg: update character card config to v6.4 (babalae#2849)

* fix:适配秘境限时全开的UI改动 (babalae#2854)

---------

Co-authored-by: 躁动的氨气 <131591012+zaodonganqi@users.noreply.github.com>
Co-authored-by: 起个名字好难的喵 <25520958+MisakaAldrich@users.noreply.github.com>
Co-authored-by: sdk2 <469656392@qq.com>
Co-authored-by: kaedelcb <57870068+kaedelcb@users.noreply.github.com>
Co-authored-by: Anyrainel <vanyrainel@gmail.com>
Co-authored-by: 辉鸭蛋 <huiyadanli@gmail.com>
Co-authored-by: huiyadanli <15783049+huiyadanli@users.noreply.github.com>
Co-authored-by: ema <mccoy39082@163.com>
Co-authored-by: bling-yshs <41812674+bling-yshs@users.noreply.github.com>
Co-authored-by: Ayu0K <152470574+Ayu0K@users.noreply.github.com>
Co-authored-by: ShadowLemoon <119576779+ShadowLemoon@users.noreply.github.com>
Co-authored-by: this-Fish <york5252@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Jamis <JamisHoo@users.noreply.github.com>
Co-authored-by: DarkFlameMaster <1004452714@qq.com>
Co-authored-by: ddaodan <40017293+ddaodan@users.noreply.github.com>
Co-authored-by: Takaranoao <takaranoao@gmail.com>
Co-authored-by: mno <718135749@qq.com>
Co-authored-by: xytoki <7547189+xytoki@users.noreply.github.com>
Co-authored-by: 云端客 <107686912+Kirito520Asuna@users.noreply.github.com>
Co-authored-by: Guest Liang <47164120+Guest-Liang@users.noreply.github.com>
Co-authored-by: HSHMENG <141244101+hshmeng@users.noreply.github.com>
Co-authored-by: 郝凯阳 <10895490+haokaiyang@users.noreply.github.com>
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.

[question] Logs in english [suggestion] 这个节目能用中文以外的语言进行本地化吗? EN 翻译/translation

1 participant