Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/AvaloniaXKCD.Browser/Exports/LocalizationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ namespace AvaloniaXKCD.Browser;
/// <summary>
/// Browser implementation of localization service with JavaScript interop
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public partial class BrowserLocalizationService : LocalizationService
{
[JSImport("getLocale", "interop")]
Expand Down
1 change: 0 additions & 1 deletion src/AvaloniaXKCD.Desktop/Exports/LocalizationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ namespace AvaloniaXKCD.Desktop;
/// <summary>
/// Desktop implementation of localization service
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class DesktopLocalizationService : LocalizationService
{
public override CultureInfo GetCulture()
Expand Down
1 change: 0 additions & 1 deletion src/AvaloniaXKCD.Tests/Exports/TestLocalizationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ namespace AvaloniaXKCD.Tests.Exports;
/// Test implementation of LocalizationService for unit tests.
/// This is in the Tests.Exports namespace to be properly registered.
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public sealed class TestLocalizationService : LocalizationService
{
public override CultureInfo GetCulture()
Expand Down
1 change: 0 additions & 1 deletion src/AvaloniaXKCD/Exports/LocalizationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ namespace AvaloniaXKCD.Exports;
/// <summary>
/// Abstract base service for handling application localization using .resx files
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public abstract class LocalizationService : ILocalizationService
{
private readonly ResourceManager _resourceManager;
Expand Down
54 changes: 36 additions & 18 deletions src/AvaloniaXKCD/MarkupExtensions/LocalizeExtension.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings;
using AvaloniaXKCD.Exports;

namespace AvaloniaXKCD.MarkupExtensions;
Expand All @@ -14,45 +17,60 @@ public class LocalizeExtension : MarkupExtension
{
public string Key { get; set; } = string.Empty;

[UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode",
Justification = "The Binding source is a local private class (LocalizedStringProvider) where the property 'Value' is preserved via code structure or annotation.")]
public override object ProvideValue(IServiceProvider serviceProvider)
{
var localizationService = ExportContainer.Get<ILocalizationService>();
if (localizationService == null)
{
return new Binding { Source = Key };
}
return Key;

var provider = new LocalizedStringProvider(localizationService, Key);

// Create an IPropertyInfo for the 'Value' CLR property
var propInfo = new ClrPropertyInfo(
name: nameof(LocalizedStringProvider.Value),
getter: obj => ((LocalizedStringProvider)obj!).Value,
setter: null,
propertyType: typeof(string)
);

// Build a compiled binding path that uses an INPC-aware accessor factory
var path = new CompiledBindingPathBuilder()
.Property(
propInfo,
// Use Avalonia's helper to create an INPC-capable property accessor
PropertyInfoAccessorFactory.CreateInpcPropertyAccessor
)
.Build();

var binding = new Binding
// Create and return a CompiledBindingExtension using that compiled path
var ext = new CompiledBindingExtension(path)
{
Source = new LocalizedStringProvider(localizationService, Key),
Path = nameof(LocalizedStringProvider.Value),
Source = provider,
Mode = BindingMode.OneWay
};

return binding;
return ext.ProvideValue(serviceProvider);
}

private class LocalizedStringProvider : INotifyPropertyChanged
private sealed class LocalizedStringProvider : ObservableObject, IDisposable
{
private readonly ILocalizationService _localizationService;
private readonly ILocalizationService _service;
private readonly string _key;

public LocalizedStringProvider(ILocalizationService localizationService, string key)
public LocalizedStringProvider(ILocalizationService service, string key)
{
_localizationService = localizationService;
_service = service;
_key = key;
_localizationService.CultureChanged += OnCultureChanged;
_service.CultureChanged += OnCultureChanged;
}

public string Value => _localizationService.GetString(_key);
public string Value => _service.GetString(_key);

public event PropertyChangedEventHandler? PropertyChanged;
private void OnCultureChanged(object? sender, CultureInfo e) => OnPropertyChanged(nameof(Value));

private void OnCultureChanged(object? sender, System.Globalization.CultureInfo e)
public void Dispose()
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
_service.CultureChanged -= OnCultureChanged;
}
}
}