Files
PKHeX/PKHeX.WinForms/Util/PropertyGridLocalization.cs
Alex 4965d96ddc Localize additional settings PropertyGrid editors (#4833)
* Localize additional settings PropertyGrid editors

* Handle [Flags] enum with multiple flagged values

* Regenerate translation file

Can be done by pressing CTRL-ALT-D on the Main form when running a debug build of the program.

Also includes new translations for Champions SP stat export for battle templates from an unrelated commit.
2026-07-04 17:06:38 -05:00

347 lines
14 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows.Forms;
namespace PKHeX.WinForms;
internal static class PropertyGridLocalization
{
// Track which instances already have a provider registered to avoid stacking providers
// (TypeDescriptor.AddProviderTransparent registrations live for the process lifetime).
private static readonly ConditionalWeakTable<object, LocalizationState> Registered = [];
public static void Apply(PropertyGrid grid, object selectedObject, string lang)
{
var localizer = WinFormsTranslator.GetDictionary(lang);
if (!Registered.TryGetValue(selectedObject, out var state))
{
state = new LocalizationState(localizer);
TypeDescriptor.AddProviderTransparent(new LocalizedTypeDescriptionProvider(selectedObject.GetType(), state), selectedObject);
Registered.Add(selectedObject, state);
}
else
{
state.Localizer = localizer;
}
grid.SelectedObject = selectedObject;
}
}
internal sealed class LocalizationState(IReadOnlyDictionary<string, string> localizer)
{
public IReadOnlyDictionary<string, string> Localizer { get; set; } = localizer;
}
internal sealed class LocalizedTypeDescriptionProvider(System.Type type, LocalizationState state)
: TypeDescriptionProvider(TypeDescriptor.GetProvider(type))
{
public override ICustomTypeDescriptor GetTypeDescriptor(System.Type objectType, object? instance)
{
var descriptor = base.GetTypeDescriptor(objectType, instance)!;
return new LocalizedTypeDescriptor(descriptor, state);
}
}
internal sealed class LocalizedTypeDescriptor(ICustomTypeDescriptor parent, LocalizationState state) : CustomTypeDescriptor(parent)
{
public override PropertyDescriptorCollection GetProperties() => Localize(base.GetProperties());
public override PropertyDescriptorCollection GetProperties(Attribute[]? attributes) => Localize(base.GetProperties(attributes));
private PropertyDescriptorCollection Localize(PropertyDescriptorCollection properties)
{
var localized = properties.Cast<PropertyDescriptor>()
.Select(z => new LocalizedPropertyDescriptor(z, state))
.ToArray<PropertyDescriptor>();
return new PropertyDescriptorCollection(localized, true);
}
}
internal sealed class LocalizedPropertyDescriptor(PropertyDescriptor parent, LocalizationState state)
: PropertyDescriptor(parent)
{
private TypeConverter? converter;
public override string DisplayName => TranslatePropertyName(Name, parent.DisplayName);
public override string Category => Translate(GetKey("PropertyGrid.Category", parent.Category), parent.Category);
public override TypeConverter Converter => converter ??= new LocalizedTypeConverter(parent.Converter, PropertyType, state);
public override bool CanResetValue(object component) => parent.CanResetValue(component);
public override System.Type ComponentType => parent.ComponentType;
public override object? GetValue(object? component) => parent.GetValue(component);
public override bool IsReadOnly => parent.IsReadOnly;
public override System.Type PropertyType => parent.PropertyType;
public override void ResetValue(object component) => parent.ResetValue(component);
public override void SetValue(object? component, object? value) => parent.SetValue(component, value);
public override bool ShouldSerializeValue(object component) => parent.ShouldSerializeValue(component);
private string TranslatePropertyName(string name, string fallback)
=> state.Localizer.GetValueOrDefault(GetKey("PropertyGrid", name), fallback);
private static string GetKey(string parent, string name) => WinFormsTranslator.GetKey(parent, name);
private string Translate(string key, string fallback) => state.Localizer.GetValueOrDefault(key, fallback);
}
internal sealed class LocalizedTypeConverter(TypeConverter parent, System.Type valueType, LocalizationState state) : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, System.Type sourceType) => parent.CanConvertFrom(context, sourceType);
public override bool CanConvertTo(ITypeDescriptorContext? context, System.Type? destinationType) => parent.CanConvertTo(context, destinationType);
public override bool GetCreateInstanceSupported(ITypeDescriptorContext? context) => parent.GetCreateInstanceSupported(context);
public override object? CreateInstance(ITypeDescriptorContext? context, System.Collections.IDictionary propertyValues) => parent.CreateInstance(context, propertyValues);
public override bool GetPropertiesSupported(ITypeDescriptorContext? context) => parent.GetPropertiesSupported(context);
public override PropertyDescriptorCollection? GetProperties(ITypeDescriptorContext? context, object value, Attribute[]? attributes)
{
var properties = parent.GetProperties(context, value, attributes);
if (properties is null)
return null;
var localized = properties.Cast<PropertyDescriptor>()
.Select(z => new LocalizedPropertyDescriptor(z, state))
.ToArray<PropertyDescriptor>();
return new PropertyDescriptorCollection(localized, true);
}
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => parent.GetStandardValuesExclusive(context);
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => parent.GetStandardValuesSupported(context);
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context) => parent.GetStandardValues(context);
public override bool IsValid(ITypeDescriptorContext? context, object? value)
{
if (value is string text && TryGetOriginalValue(text, out _))
return true;
return parent.IsValid(context, value);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is string text && TryGetOriginalValue(text, out var original))
return original;
return parent.ConvertFrom(context, culture, value);
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, System.Type destinationType)
{
if (destinationType == typeof(string) && value is not null && ShouldLocalizeString(value.GetType()))
return GetLocalizedText(value) ?? parent.ConvertTo(context, culture, value, destinationType);
return parent.ConvertTo(context, culture, value, destinationType);
}
private bool ShouldLocalizeString(System.Type type)
{
var effective = Nullable.GetUnderlyingType(type) ?? type;
if (effective == typeof(string) || effective == typeof(bool) || effective.IsEnum || typeof(ICollection).IsAssignableFrom(effective))
return true;
if (effective.IsPrimitive || effective.Namespace?.StartsWith("System.", StringComparison.Ordinal) == true)
return false;
if (valueType.Namespace?.StartsWith("PKHeX.", StringComparison.Ordinal) == true)
return true;
return false;
}
private string? GetLocalizedText(object value) => GetLocalizedText(value, 0);
private string? GetLocalizedText(object value, int depth) => value switch
{
string s => Translate(GetKey("PropertyGrid.Value", s), s),
bool b => Translate(GetKey("PropertyGrid.Value", b.ToString()), b.ToString()),
Enum e => GetLocalizedEnum(e),
ICollection => Translate(GetKey("PropertyGrid.Value", "Collection"), "(Collection)"),
_ => GetLocalizedObjectText(value, depth),
};
private string GetLocalizedEnum(Enum e)
{
var name = e.GetType().Name;
var current = e.ToString();
if (!current.Contains(','))
return Translate(GetKey(name, e.ToString()), current);
// Multiple values! Need to split and re-merge.
var split = current.Split(", ");
for (var i = 0; i < split.Length; i++)
{
var value = split[i];
var key = GetKey(name, value);
value = Translate(key, value);
split[i] = value;
}
return string.Join(", ", split);
}
private bool TryGetOriginalValue(string text, out object? value)
{
var type = Nullable.GetUnderlyingType(valueType) ?? valueType;
if (type == typeof(bool))
{
if (TryGetSingleTranslatedMatch(text, [true, false], out value))
return true;
if (MatchesRaw(text, true)) { value = true; return true; }
if (MatchesRaw(text, false)) { value = false; return true; }
}
else if (type.IsEnum)
{
var values = Enum.GetValues(type).Cast<object>().ToArray();
if (TryGetSingleTranslatedMatch(text, values, out value))
return true;
foreach (var candidate in values)
{
if (MatchesRaw(text, candidate)) { value = candidate; return true; }
}
if (type.IsDefined(typeof(FlagsAttribute), false) && TryGetFlagsEnumValue(type, text, values, out value))
return true;
}
value = null;
return false;
}
private bool TryGetFlagsEnumValue(Type type, string text, ReadOnlySpan<object> values, out object? value)
{
value = null;
var split = text.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
if (split.Length <= 1)
return false;
var names = new string[split.Length];
for (var i = 0; i < split.Length; i++)
{
if (!TryGetSingleEnumName(split[i], values, out var name))
return false;
names[i] = name;
}
try
{
value = Enum.Parse(type, string.Join(", ", names), true);
return true;
}
catch (ArgumentException)
{
return false;
}
}
private bool TryGetSingleEnumName(string text, ReadOnlySpan<object> values, out string name)
{
name = string.Empty;
object? match = null;
foreach (var candidate in values)
{
if (!MatchesTranslated(text, candidate) && !MatchesRaw(text, candidate))
continue;
if (match is not null)
return false;
match = candidate;
}
if (match is null)
return false;
name = match.ToString() ?? string.Empty;
return name.Length != 0;
}
private bool TryGetSingleTranslatedMatch(string text, ReadOnlySpan<object> values, out object? value)
{
value = null;
foreach (var candidate in values)
{
if (!MatchesTranslated(text, candidate))
continue;
if (value is not null)
{
value = null;
return false; // ambiguous translation; let the parent converter decide.
}
value = candidate;
}
return value is not null;
}
private bool MatchesTranslated(string text, object value)
{
var translated = GetLocalizedText(value);
return !string.IsNullOrEmpty(translated) && string.Equals(text, translated, StringComparison.CurrentCultureIgnoreCase);
}
private static bool MatchesRaw(string text, object value)
=> string.Equals(text, value.ToString(), StringComparison.OrdinalIgnoreCase);
private string? GetLocalizedObjectText(object value, int depth)
{
var type = value.GetType();
if (!ShouldLocalizeString(type) || type.IsPrimitive)
return null;
var typeName = type.Name;
var prefix = Translate($"PropertyGrid.Type.{typeName}", typeName);
if (depth >= 2)
return prefix;
PropertyDescriptorCollection? descriptors;
if (parent.GetPropertiesSupported())
{
try { descriptors = parent.GetProperties(value); }
catch { return prefix; }
}
else if (type.GetMethod("<Clone>$") is not null)
{
// record type: synthesize a localized summary instead of the compiler-generated ToString.
try { descriptors = TypeDescriptor.GetProperties(value); }
catch { return prefix; }
}
else if (type.GetMethod(nameof(ToString), System.Type.EmptyTypes)?.DeclaringType is { } dt && dt != typeof(object) && dt != typeof(ValueType))
{
return null; // has a meaningful ToString override; respect it.
}
else
{
return prefix; // plain object: show the (translated) type name instead of the namespace-qualified default.
}
var properties = descriptors?.Cast<PropertyDescriptor>() ?? [];
var parts = new List<string>();
foreach (var property in properties)
{
if (!property.IsBrowsable)
continue;
object? propertyValue;
try { propertyValue = property.GetValue(value); }
catch { continue; }
if (propertyValue is null)
continue;
var name = TranslatePropertyName(property.Name, property.DisplayName);
string text;
try { text = (GetLocalizedText(propertyValue, depth + 1) ?? propertyValue).ToString() ?? string.Empty; }
catch { continue; }
parts.Add($"{name} = {text}");
if (parts.Count >= 4)
break;
}
return parts.Count == 0 ? prefix : $"{prefix} {{ {string.Join(", ", parts)} }}";
}
private static string GetKey(string parent, string name) => WinFormsTranslator.GetKey(parent, name);
private string TranslatePropertyName(string name, string fallback)
=> state.Localizer.GetValueOrDefault(GetKey("PropertyGrid", name), fallback);
private string Translate(string key, string fallback) => state.Localizer.GetValueOrDefault(key, fallback);
}