mirror of
https://github.com/babalae/better-genshin-impact.git
synced 2026-05-21 09:45:48 +08:00
* 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 a1c2334951.
* 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 和镜像源下载语言文件
- 下载后自动替换本地文件并重新加载翻译服务
887 lines
30 KiB
C#
887 lines
30 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Controls.Primitives;
|
|
using System.Windows.Data;
|
|
using System.Windows.Documents;
|
|
using System.Windows.Media;
|
|
using System.Windows.Threading;
|
|
using BetterGenshinImpact.Core.Config;
|
|
using BetterGenshinImpact.Service.Interface;
|
|
using CommunityToolkit.Mvvm.Messaging;
|
|
using CommunityToolkit.Mvvm.Messaging.Messages;
|
|
|
|
namespace BetterGenshinImpact.View.Behavior
|
|
{
|
|
public static class AutoTranslateInterceptor
|
|
{
|
|
static AutoTranslateInterceptor()
|
|
{
|
|
EventManager.RegisterClassHandler(
|
|
typeof(FrameworkElement),
|
|
FrameworkElement.LoadedEvent,
|
|
new RoutedEventHandler(OnAnyElementLoaded),
|
|
true);
|
|
EventManager.RegisterClassHandler(
|
|
typeof(FrameworkContentElement),
|
|
FrameworkContentElement.LoadedEvent,
|
|
new RoutedEventHandler(OnAnyElementLoaded),
|
|
true);
|
|
}
|
|
|
|
public static readonly DependencyProperty EnableAutoTranslateProperty =
|
|
DependencyProperty.RegisterAttached(
|
|
"EnableAutoTranslate",
|
|
typeof(bool),
|
|
typeof(AutoTranslateInterceptor),
|
|
new FrameworkPropertyMetadata(
|
|
false,
|
|
FrameworkPropertyMetadataOptions.Inherits,
|
|
OnEnableAutoTranslateChanged));
|
|
|
|
public static void SetEnableAutoTranslate(DependencyObject element, bool value)
|
|
=> element.SetValue(EnableAutoTranslateProperty, value);
|
|
|
|
public static bool GetEnableAutoTranslate(DependencyObject element)
|
|
=> (bool)element.GetValue(EnableAutoTranslateProperty);
|
|
|
|
private static readonly DependencyProperty ScopeProperty =
|
|
DependencyProperty.RegisterAttached(
|
|
"Scope",
|
|
typeof(Scope),
|
|
typeof(AutoTranslateInterceptor),
|
|
new PropertyMetadata(null));
|
|
|
|
private static readonly DependencyProperty OriginalValuesProperty =
|
|
DependencyProperty.RegisterAttached(
|
|
"OriginalValues",
|
|
typeof(Dictionary<DependencyProperty, string>),
|
|
typeof(AutoTranslateInterceptor),
|
|
new PropertyMetadata(null));
|
|
|
|
private static Dictionary<DependencyProperty, string>? GetOriginalValuesMap(DependencyObject obj)
|
|
=> (Dictionary<DependencyProperty, string>?)obj.GetValue(OriginalValuesProperty);
|
|
|
|
private static Dictionary<DependencyProperty, string> GetOrCreateOriginalValuesMap(DependencyObject obj)
|
|
{
|
|
var map = GetOriginalValuesMap(obj);
|
|
if (map != null)
|
|
{
|
|
return map;
|
|
}
|
|
|
|
map = new Dictionary<DependencyProperty, string>();
|
|
obj.SetValue(OriginalValuesProperty, map);
|
|
return map;
|
|
}
|
|
|
|
private static void OnAnyElementLoaded(object sender, RoutedEventArgs e)
|
|
{
|
|
if (sender is not DependencyObject obj)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FindNearestScope(obj)?.RequestApply(obj);
|
|
}
|
|
|
|
private static Scope? FindNearestScope(DependencyObject obj)
|
|
{
|
|
DependencyObject? current = obj;
|
|
while (current != null)
|
|
{
|
|
if (current is FrameworkElement fe && fe.GetValue(ScopeProperty) is Scope scope)
|
|
{
|
|
return scope;
|
|
}
|
|
|
|
current = GetParentObject(current);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static DependencyObject? GetParentObject(DependencyObject obj)
|
|
{
|
|
if (obj is FrameworkElement fe)
|
|
{
|
|
if (fe.Parent != null)
|
|
{
|
|
return fe.Parent;
|
|
}
|
|
|
|
if (fe.TemplatedParent is DependencyObject templatedParent)
|
|
{
|
|
return templatedParent;
|
|
}
|
|
}
|
|
|
|
if (obj is FrameworkContentElement fce)
|
|
{
|
|
if (fce.Parent != null)
|
|
{
|
|
return fce.Parent;
|
|
}
|
|
|
|
if (fce.TemplatedParent is DependencyObject templatedParent)
|
|
{
|
|
return templatedParent;
|
|
}
|
|
}
|
|
|
|
if (obj is Visual || obj is System.Windows.Media.Media3D.Visual3D)
|
|
{
|
|
return VisualTreeHelper.GetParent(obj);
|
|
}
|
|
|
|
return LogicalTreeHelper.GetParent(obj);
|
|
}
|
|
|
|
private static void OnEnableAutoTranslateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if (d is not FrameworkElement fe)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (e.NewValue is true)
|
|
{
|
|
if (fe.GetValue(ScopeProperty) is Scope oldScope)
|
|
{
|
|
fe.Loaded -= oldScope.OnLoaded;
|
|
fe.Unloaded -= oldScope.OnUnloaded;
|
|
oldScope.Dispose();
|
|
fe.ClearValue(ScopeProperty);
|
|
}
|
|
|
|
var scope = new Scope(fe);
|
|
fe.SetValue(ScopeProperty, scope);
|
|
fe.Loaded += scope.OnLoaded;
|
|
fe.Unloaded += scope.OnUnloaded;
|
|
if (fe.IsLoaded)
|
|
{
|
|
scope.ApplyNow();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (fe.GetValue(ScopeProperty) is Scope scope)
|
|
{
|
|
fe.Loaded -= scope.OnLoaded;
|
|
fe.Unloaded -= scope.OnUnloaded;
|
|
scope.Dispose();
|
|
fe.ClearValue(ScopeProperty);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 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();
|
|
}
|
|
|
|
private void ScheduleRefresh()
|
|
{
|
|
if (!_applied)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_refreshScheduled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_refreshScheduled = true;
|
|
_root.Dispatcher.BeginInvoke(
|
|
() =>
|
|
{
|
|
_refreshScheduled = false;
|
|
if (!_applied)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RestoreOriginalValues(_root);
|
|
RefreshBoundValues(_root);
|
|
Apply(_root);
|
|
},
|
|
DispatcherPriority.Loaded);
|
|
}
|
|
|
|
public void RequestApply(DependencyObject obj)
|
|
{
|
|
if (!_applied)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (IsInComboBoxContext(obj))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!_pendingApply.Add(obj))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_applyScheduled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_applyScheduled = true;
|
|
_root.Dispatcher.BeginInvoke(
|
|
() =>
|
|
{
|
|
_applyScheduled = false;
|
|
if (!_applied)
|
|
{
|
|
_pendingApply.Clear();
|
|
return;
|
|
}
|
|
|
|
var items = _pendingApply.ToArray();
|
|
_pendingApply.Clear();
|
|
foreach (var item in items)
|
|
{
|
|
Apply(item);
|
|
}
|
|
},
|
|
DispatcherPriority.Loaded);
|
|
}
|
|
|
|
private void Apply(DependencyObject root)
|
|
{
|
|
var translator = App.GetService<ITranslationService>();
|
|
if (translator == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var culture = translator.GetCurrentCulture();
|
|
if (culture.Name.StartsWith("zh", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var queue = new Queue<DependencyObject>();
|
|
var visited = new HashSet<DependencyObject>();
|
|
queue.Enqueue(root);
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
var current = queue.Dequeue();
|
|
if (!visited.Add(current))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (IsInGridViewRowPresenter(current))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (IsInComboBoxContext(current))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
TranslateKnown(current, translator);
|
|
|
|
if (current is FrameworkElement feCurrent)
|
|
{
|
|
TrackContextMenu(feCurrent.ContextMenu, queue);
|
|
TrackToolTip(feCurrent.ToolTip, queue);
|
|
}
|
|
|
|
if (current is Visual || current is System.Windows.Media.Media3D.Visual3D)
|
|
{
|
|
var count = VisualTreeHelper.GetChildrenCount(current);
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
queue.Enqueue(VisualTreeHelper.GetChild(current, i));
|
|
}
|
|
}
|
|
|
|
if (current is FrameworkElement || current is FrameworkContentElement)
|
|
{
|
|
foreach (var child in LogicalTreeHelper.GetChildren(current).OfType<DependencyObject>())
|
|
{
|
|
queue.Enqueue(child);
|
|
}
|
|
}
|
|
|
|
if (current is FrameworkElement fe)
|
|
{
|
|
foreach (var inline in EnumerateInlineObjects(fe))
|
|
{
|
|
queue.Enqueue(inline);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void TrackContextMenu(ContextMenu? contextMenu, Queue<DependencyObject> queue)
|
|
{
|
|
if (contextMenu == null || !_trackedContextMenus.Add(contextMenu))
|
|
{
|
|
return;
|
|
}
|
|
|
|
queue.Enqueue(contextMenu);
|
|
|
|
RoutedEventHandler? openedHandler = null;
|
|
openedHandler = (_, _) =>
|
|
{
|
|
_root.Dispatcher.BeginInvoke(
|
|
() => Apply(contextMenu),
|
|
DispatcherPriority.Loaded);
|
|
};
|
|
contextMenu.Opened += openedHandler;
|
|
_unsubscribe.Add(() => contextMenu.Opened -= openedHandler);
|
|
}
|
|
|
|
private void TrackToolTip(object? toolTip, Queue<DependencyObject> queue)
|
|
{
|
|
if (toolTip is not ToolTip tt || !_trackedToolTips.Add(tt))
|
|
{
|
|
return;
|
|
}
|
|
|
|
queue.Enqueue(tt);
|
|
|
|
RoutedEventHandler? openedHandler = null;
|
|
openedHandler = (_, _) =>
|
|
{
|
|
_root.Dispatcher.BeginInvoke(
|
|
() => Apply(tt),
|
|
DispatcherPriority.Loaded);
|
|
};
|
|
tt.Opened += openedHandler;
|
|
_unsubscribe.Add(() => tt.Opened -= openedHandler);
|
|
}
|
|
|
|
private static IEnumerable<DependencyObject> EnumerateInlineObjects(FrameworkElement fe)
|
|
{
|
|
if (fe is not TextBlock tb)
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
foreach (var inline in EnumerateInlineObjects(tb.Inlines))
|
|
{
|
|
yield return inline;
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<DependencyObject> EnumerateInlineObjects(InlineCollection inlines)
|
|
{
|
|
foreach (var inline in inlines)
|
|
{
|
|
yield return inline;
|
|
|
|
if (inline is Span span)
|
|
{
|
|
foreach (var nested in EnumerateInlineObjects(span.Inlines))
|
|
{
|
|
yield return nested;
|
|
}
|
|
}
|
|
|
|
if (inline is InlineUIContainer { Child: DependencyObject child })
|
|
{
|
|
yield return child;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void TranslateKnown(DependencyObject obj, ITranslationService translator)
|
|
{
|
|
switch (obj)
|
|
{
|
|
case TextBlock tb:
|
|
TranslateIfNotBound(tb, TextBlock.TextProperty, tb.Text, s => tb.Text = s, translator);
|
|
TranslateToolTip(tb, translator);
|
|
break;
|
|
case Run run:
|
|
TranslateIfNotBound(run, Run.TextProperty, run.Text, s => run.Text = s, translator);
|
|
break;
|
|
case HeaderedContentControl hcc:
|
|
if (hcc.Header is string header)
|
|
{
|
|
TranslateIfNotBound(hcc, HeaderedContentControl.HeaderProperty, header, s => hcc.Header = s, translator);
|
|
}
|
|
TranslateToolTip(hcc, translator);
|
|
break;
|
|
case ContentControl cc:
|
|
if (cc.Content is string content)
|
|
{
|
|
TranslateIfNotBound(cc, ContentControl.ContentProperty, content, s => cc.Content = s, translator);
|
|
}
|
|
TranslateToolTip(cc, translator);
|
|
break;
|
|
case FrameworkElement fe:
|
|
TranslateToolTip(fe, translator);
|
|
break;
|
|
}
|
|
|
|
TranslateStringLocalValues(obj, translator);
|
|
}
|
|
|
|
private void TranslateToolTip(FrameworkElement fe, ITranslationService translator)
|
|
{
|
|
if (fe.ToolTip is string tip)
|
|
{
|
|
TranslateIfNotBound(fe, FrameworkElement.ToolTipProperty, tip, s => fe.ToolTip = s, translator);
|
|
}
|
|
}
|
|
|
|
private void TranslateStringLocalValues(DependencyObject obj, ITranslationService translator)
|
|
{
|
|
var enumerator = obj.GetLocalValueEnumerator();
|
|
while (enumerator.MoveNext())
|
|
{
|
|
var entry = enumerator.Current;
|
|
var property = entry.Property;
|
|
if (property.PropertyType != typeof(string) && property.PropertyType != typeof(object))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (BindingOperations.IsDataBound(obj, property))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (entry.Value is not string value || string.IsNullOrWhiteSpace(value))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!ShouldTranslatePropertyName(property.Name))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var map = GetOriginalValuesMap(obj);
|
|
if (map == null || !map.TryGetValue(property, out var original))
|
|
{
|
|
if (ContainsHan(value))
|
|
{
|
|
map = GetOrCreateOriginalValuesMap(obj);
|
|
map[property] = value;
|
|
original = value;
|
|
}
|
|
else
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var translated = translator.Translate(original, BuildSourceInfo(obj, property, MissingTextSource.UiStaticLiteral));
|
|
if (!ReferenceEquals(value, translated) && !string.Equals(value, translated, StringComparison.Ordinal))
|
|
{
|
|
obj.SetValue(property, translated);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool ShouldTranslatePropertyName(string name)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (name.EndsWith("Path", StringComparison.Ordinal)
|
|
|| name.EndsWith("MemberPath", StringComparison.Ordinal)
|
|
|| string.Equals(name, "Uid", StringComparison.Ordinal)
|
|
|| string.Equals(name, "Name", StringComparison.Ordinal))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return name.Contains("Text", StringComparison.Ordinal)
|
|
|| name.Contains("Content", StringComparison.Ordinal)
|
|
|| name.Contains("Header", StringComparison.Ordinal)
|
|
|| name.Contains("ToolTip", StringComparison.Ordinal)
|
|
|| name.Contains("Title", StringComparison.Ordinal)
|
|
|| name.Contains("Subtitle", StringComparison.Ordinal)
|
|
|| name.Contains("Description", StringComparison.Ordinal)
|
|
|| name.Contains("Placeholder", StringComparison.Ordinal)
|
|
|| name.Contains("Label", StringComparison.Ordinal)
|
|
|| name.Contains("Caption", StringComparison.Ordinal);
|
|
}
|
|
|
|
private static bool ContainsHan(string text)
|
|
{
|
|
foreach (var ch in text)
|
|
{
|
|
if (ch is >= '\u4E00' and <= '\u9FFF')
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void TranslateIfNotBound(
|
|
DependencyObject obj,
|
|
DependencyProperty property,
|
|
string currentValue,
|
|
Action<string> setter,
|
|
ITranslationService translator)
|
|
{
|
|
if (BindingOperations.IsDataBound(obj, property))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var map = GetOriginalValuesMap(obj);
|
|
if (map == null || !map.TryGetValue(property, out var original))
|
|
{
|
|
if (ContainsHan(currentValue))
|
|
{
|
|
map = GetOrCreateOriginalValuesMap(obj);
|
|
map[property] = currentValue;
|
|
original = currentValue;
|
|
}
|
|
else
|
|
{
|
|
original = currentValue;
|
|
}
|
|
}
|
|
|
|
var translated = translator.Translate(original, BuildSourceInfo(obj, property, MissingTextSource.UiStaticLiteral));
|
|
if (!ReferenceEquals(currentValue, translated) && !string.Equals(currentValue, translated, StringComparison.Ordinal))
|
|
{
|
|
setter(translated);
|
|
}
|
|
}
|
|
|
|
private void RestoreOriginalValues(DependencyObject root)
|
|
{
|
|
var queue = new Queue<DependencyObject>();
|
|
var visited = new HashSet<DependencyObject>();
|
|
queue.Enqueue(root);
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
var current = queue.Dequeue();
|
|
if (!visited.Add(current))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var map = GetOriginalValuesMap(current);
|
|
if (map != null)
|
|
{
|
|
foreach (var pair in map)
|
|
{
|
|
if (BindingOperations.IsDataBound(current, pair.Key))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
current.SetValue(pair.Key, pair.Value);
|
|
}
|
|
}
|
|
|
|
if (current is FrameworkElement feCurrent)
|
|
{
|
|
if (feCurrent.ContextMenu != null)
|
|
{
|
|
queue.Enqueue(feCurrent.ContextMenu);
|
|
}
|
|
|
|
if (feCurrent.ToolTip is DependencyObject tt)
|
|
{
|
|
queue.Enqueue(tt);
|
|
}
|
|
}
|
|
|
|
if (current is Visual || current is System.Windows.Media.Media3D.Visual3D)
|
|
{
|
|
var count = VisualTreeHelper.GetChildrenCount(current);
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
queue.Enqueue(VisualTreeHelper.GetChild(current, i));
|
|
}
|
|
}
|
|
|
|
if (current is FrameworkElement || current is FrameworkContentElement)
|
|
{
|
|
foreach (var child in LogicalTreeHelper.GetChildren(current).OfType<DependencyObject>())
|
|
{
|
|
queue.Enqueue(child);
|
|
}
|
|
}
|
|
|
|
if (current is TextBlock tb)
|
|
{
|
|
foreach (var inline in EnumerateInlineObjects(tb.Inlines))
|
|
{
|
|
queue.Enqueue(inline);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void RefreshBoundValues(DependencyObject root)
|
|
{
|
|
var queue = new Queue<DependencyObject>();
|
|
var visited = new HashSet<DependencyObject>();
|
|
queue.Enqueue(root);
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
var current = queue.Dequeue();
|
|
if (!visited.Add(current))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
RefreshBindings(current);
|
|
|
|
if (current is FrameworkElement feCurrent)
|
|
{
|
|
if (feCurrent.ContextMenu != null)
|
|
{
|
|
queue.Enqueue(feCurrent.ContextMenu);
|
|
}
|
|
|
|
if (feCurrent.ToolTip is DependencyObject tt)
|
|
{
|
|
queue.Enqueue(tt);
|
|
}
|
|
}
|
|
|
|
if (current is Visual || current is System.Windows.Media.Media3D.Visual3D)
|
|
{
|
|
var count = VisualTreeHelper.GetChildrenCount(current);
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
queue.Enqueue(VisualTreeHelper.GetChild(current, i));
|
|
}
|
|
}
|
|
|
|
if (current is FrameworkElement || current is FrameworkContentElement)
|
|
{
|
|
foreach (var child in LogicalTreeHelper.GetChildren(current).OfType<DependencyObject>())
|
|
{
|
|
queue.Enqueue(child);
|
|
}
|
|
}
|
|
|
|
if (current is TextBlock tb)
|
|
{
|
|
foreach (var inline in EnumerateInlineObjects(tb.Inlines))
|
|
{
|
|
queue.Enqueue(inline);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void RefreshBindings(DependencyObject obj)
|
|
{
|
|
var enumerator = obj.GetLocalValueEnumerator();
|
|
while (enumerator.MoveNext())
|
|
{
|
|
var entry = enumerator.Current;
|
|
var property = entry.Property;
|
|
if (!BindingOperations.IsDataBound(obj, property))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
BindingOperations.GetBindingExpressionBase(obj, property)?.UpdateTarget();
|
|
}
|
|
}
|
|
|
|
private static TranslationSourceInfo BuildSourceInfo(DependencyObject element, DependencyProperty property, MissingTextSource source)
|
|
{
|
|
var viewElement = FindViewElement(element);
|
|
var viewType = viewElement?.GetType();
|
|
var xamlPath = GetViewXamlPath(viewType);
|
|
|
|
return new TranslationSourceInfo
|
|
{
|
|
Source = source,
|
|
ViewXamlPath = xamlPath,
|
|
ViewType = viewType?.FullName,
|
|
ElementType = element.GetType().FullName,
|
|
ElementName = GetElementName(element),
|
|
PropertyName = property.Name
|
|
};
|
|
}
|
|
|
|
private static DependencyObject? FindViewElement(DependencyObject? element)
|
|
{
|
|
var current = element;
|
|
while (current != null)
|
|
{
|
|
if (current is Window || current is Page || current is UserControl)
|
|
{
|
|
return current;
|
|
}
|
|
|
|
var parent = LogicalTreeHelper.GetParent(current);
|
|
if (parent == null && current is FrameworkElement fe)
|
|
{
|
|
parent = fe.Parent ?? fe.TemplatedParent as DependencyObject;
|
|
}
|
|
|
|
if (parent == null && current is FrameworkContentElement fce)
|
|
{
|
|
parent = fce.Parent;
|
|
}
|
|
|
|
if (parent == null)
|
|
{
|
|
parent = VisualTreeHelper.GetParent(current);
|
|
}
|
|
|
|
current = parent;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static string? GetViewXamlPath(Type? viewType)
|
|
{
|
|
if (viewType == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var ns = viewType.Namespace ?? string.Empty;
|
|
const string viewMarker = ".View.";
|
|
var index = ns.IndexOf(viewMarker, StringComparison.Ordinal);
|
|
if (index < 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var relativeNamespace = ns[(index + viewMarker.Length)..];
|
|
var folder = string.IsNullOrWhiteSpace(relativeNamespace) ? "View" : $"View/{relativeNamespace.Replace('.', '/')}";
|
|
return $"{folder}/{viewType.Name}.xaml";
|
|
}
|
|
|
|
private static string? GetElementName(DependencyObject element)
|
|
{
|
|
return element switch
|
|
{
|
|
FrameworkElement fe when !string.IsNullOrWhiteSpace(fe.Name) => fe.Name,
|
|
FrameworkContentElement fce when !string.IsNullOrWhiteSpace(fce.Name) => fce.Name,
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
private static bool IsInGridViewRowPresenter(DependencyObject obj)
|
|
{
|
|
DependencyObject? current = obj;
|
|
while (current != null)
|
|
{
|
|
if (current is GridViewRowPresenter)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
current = GetParentObject(current);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool IsInComboBoxContext(DependencyObject obj)
|
|
{
|
|
DependencyObject? current = obj;
|
|
while (current != null)
|
|
{
|
|
if (current is ComboBox or ComboBoxItem)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (current is Popup { PlacementTarget: ComboBox })
|
|
{
|
|
return true;
|
|
}
|
|
|
|
current = GetParentObject(current);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|