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
11 changes: 11 additions & 0 deletions DVModManager/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ public override void OnFrameworkInitializationCompleted()
ConfigureServices(services);
Services = services.BuildServiceProvider();

// Initialize localization service with default language (English).
// If settings are loaded later, the language will be updated via MainWindowViewModel.
// For now, we just ensure the service is ready before any UI is created.
var localizationService = Services.GetRequiredService<ILocalizationService>();
// Expose as an Application-level resource so XAML can bind to it via {StaticResource Loc}
Resources["Loc"] = localizationService;
// Language will be applied after settings load in MainWindowViewModel.InitializeAsync()

// Catch unhandled exceptions on any thread and surface them in the status bar / console
var logDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
Expand Down Expand Up @@ -106,6 +114,9 @@ private static void ConfigureServices(IServiceCollection services)
// HTTP
services.AddHttpClient();

// Localization (must be registered early, before UI/ViewModels)
services.AddSingleton<ILocalizationService, LocalizationService>();

// Core infrastructure
services.AddSingleton<ISettingsService, SettingsService>();
services.AddSingleton<IDialogService, DialogService>();
Expand Down
73 changes: 73 additions & 0 deletions DVModManager/Converters/LocalizeExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System.ComponentModel;
using Avalonia.Data;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.MarkupExtensions;
using DVModManager.Services;
using Microsoft.Extensions.DependencyInjection;

namespace DVModManager.Converters;

/// <summary>
/// Markup extension for localizing text.
/// Usage in XAML: Text="{conv:Localize toolbar.save_profile}"
/// </summary>
public class LocalizeExtension : MarkupExtension
{
public string Key { get; set; } = "";

public LocalizeExtension() { }

public LocalizeExtension(string key)
{
Key = key;
}

public override object ProvideValue(IServiceProvider serviceProvider)
{
if (string.IsNullOrWhiteSpace(Key))
return string.Empty;

var source = App.Services.GetRequiredService<ILocalizationService>();

// Wrap each key in a dedicated observable so PropertyChanged("Value") fires
// on language change — Avalonia's ReflectionBindingExtension reliably handles
// named-property change notifications but NOT "Item[]" (WPF/Silverlight convention).
var localizedValue = new LocalizedValue(source, Key);

var binding = new ReflectionBindingExtension(nameof(LocalizedValue.Value))
{
Source = localizedValue,
Mode = BindingMode.OneWay,
FallbackValue = Key,
};

return binding.ProvideValue(serviceProvider);
}
}

/// <summary>
/// Single-key observable wrapper over <see cref="ILocalizationService"/>.
/// Raises PropertyChanged("Value") whenever the active language changes,
/// ensuring bound UI elements update immediately without relying on "Item[]".
/// </summary>
internal sealed class LocalizedValue : INotifyPropertyChanged
{
private readonly ILocalizationService _service;
private readonly string _key;

public string Value => _service[_key];

public event PropertyChangedEventHandler? PropertyChanged;

public LocalizedValue(ILocalizationService service, string key)
{
_service = service;
_key = key;
service.LanguageChanged += OnLanguageChanged;
}

private void OnLanguageChanged(object? sender, EventArgs e)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
}
}
40 changes: 40 additions & 0 deletions DVModManager/Converters/StringKeyConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Avalonia.Data.Converters;
using DVModManager.Services;

namespace DVModManager.Converters;

/// <summary>
/// Value converter that translates a localization key (from ViewModel) to a localized string.
/// Binding: Text="{Binding Some ObservableProperty, Converter={StaticResource LocalizeConverter}}"
/// where the property returns a localization key like "status.activated".
/// </summary>
public class StringKeyConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo culture)
{
if (value is not string key || string.IsNullOrEmpty(key))
return "";

try
{
var localizationService = App.Services.GetService(typeof(ILocalizationService)) as ILocalizationService;
if (localizationService == null)
return key; // Fallback

// If parameter contains format args, apply them
if (parameter is object[] args)
return localizationService.GetString(key, args);

return localizationService.GetString(key);
}
catch
{
return key;
}
}

public object? ConvertBack(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
4 changes: 4 additions & 0 deletions DVModManager/DVModManager.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,8 @@
<AvaloniaResource Include="Assets\**" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="Resources\strings.csv" />
</ItemGroup>

</Project>
3 changes: 3 additions & 0 deletions DVModManager/Models/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ public class AppSettings
/// <summary>"Dark" or "Light"</summary>
public string ThemeVariant { get; set; } = "Dark";

/// <summary>Language code: "en", "de", "fr", etc.</summary>
public string Language { get; set; } = "en";

public bool AutoCheckUpdatesOnStartup { get; set; } = true;

public bool BackupBeforeChanges { get; set; } = true;
Expand Down
Loading
Loading