diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index adea1b90fc..13c88d7ed1 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -174,6 +174,9 @@
+
+
+
diff --git a/samples/ControlCatalog/Pages/PlatformSettingsPage.xaml b/samples/ControlCatalog/Pages/PlatformSettingsPage.xaml
new file mode 100644
index 0000000000..2a6e375413
--- /dev/null
+++ b/samples/ControlCatalog/Pages/PlatformSettingsPage.xaml
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/PlatformSettingsPage.xaml.cs b/samples/ControlCatalog/Pages/PlatformSettingsPage.xaml.cs
new file mode 100644
index 0000000000..b235c9a3ac
--- /dev/null
+++ b/samples/ControlCatalog/Pages/PlatformSettingsPage.xaml.cs
@@ -0,0 +1,15 @@
+using Avalonia.Controls;
+using ControlCatalog.ViewModels;
+
+namespace ControlCatalog.Pages
+{
+ public partial class PlatformSettingsPage : UserControl
+ {
+ public PlatformSettingsPage()
+ {
+ InitializeComponent();
+ DataContext = new PlatformSettingsViewModel();
+ }
+ }
+}
+
diff --git a/samples/ControlCatalog/ViewModels/PlatformSettingsViewModel.cs b/samples/ControlCatalog/ViewModels/PlatformSettingsViewModel.cs
new file mode 100644
index 0000000000..a9ff1214cf
--- /dev/null
+++ b/samples/ControlCatalog/ViewModels/PlatformSettingsViewModel.cs
@@ -0,0 +1,77 @@
+using System;
+using Avalonia;
+using Avalonia.Input;
+using Avalonia.Media;
+using Avalonia.Platform;
+using MiniMvvm;
+
+namespace ControlCatalog.ViewModels;
+
+public class PlatformSettingsViewModel : ViewModelBase
+{
+ private readonly IPlatformSettings? _platformSettings;
+ private PlatformColorValues? _colorValues;
+ private string? _preferredLanguage;
+
+ public PlatformSettingsViewModel()
+ {
+ _platformSettings = AvaloniaLocator.Current.GetService();
+
+ if (_platformSettings != null)
+ {
+ _colorValues = _platformSettings.GetColorValues();
+ _preferredLanguage = _platformSettings.PreferredApplicationLanguage;
+
+ _platformSettings.ColorValuesChanged += OnColorValuesChanged;
+ _platformSettings.PreferredApplicationLanguageChanged += OnPreferredLanguageChanged;
+ }
+ }
+
+ private void OnColorValuesChanged(object? sender, PlatformColorValues e)
+ {
+ _colorValues = e;
+ RaisePropertyChanged(nameof(ThemeVariant));
+ RaisePropertyChanged(nameof(ContrastPreference));
+ RaisePropertyChanged(nameof(AccentColor1));
+ RaisePropertyChanged(nameof(AccentColor2));
+ RaisePropertyChanged(nameof(AccentColor3));
+ }
+
+ private void OnPreferredLanguageChanged(object? sender, EventArgs e)
+ {
+ if (_platformSettings != null)
+ {
+ _preferredLanguage = _platformSettings.PreferredApplicationLanguage;
+ RaisePropertyChanged(nameof(PreferredLanguage));
+ }
+ }
+
+ public bool IsAvailable => _platformSettings != null;
+
+ public string PreferredLanguage => _preferredLanguage ?? "Not available";
+
+ public string ThemeVariant => _colorValues?.ThemeVariant.ToString() ?? "Not available";
+
+ public string ContrastPreference => _colorValues?.ContrastPreference.ToString() ?? "Not available";
+
+ public Color AccentColor1 => _colorValues?.AccentColor1 ?? Colors.Gray;
+
+ public Color AccentColor2 => _colorValues?.AccentColor2 ?? Colors.Gray;
+
+ public Color AccentColor3 => _colorValues?.AccentColor3 ?? Colors.Gray;
+
+ public string HoldWaitDuration => _platformSettings?.HoldWaitDuration.ToString() ?? "Not available";
+
+ public string TapSizeTouch => _platformSettings?.GetTapSize(PointerType.Touch).ToString() ?? "Not available";
+
+ public string TapSizeMouse => _platformSettings?.GetTapSize(PointerType.Mouse).ToString() ?? "Not available";
+
+ public string DoubleTapSizeTouch => _platformSettings?.GetDoubleTapSize(PointerType.Touch).ToString() ?? "Not available";
+
+ public string DoubleTapSizeMouse => _platformSettings?.GetDoubleTapSize(PointerType.Mouse).ToString() ?? "Not available";
+
+ public string DoubleTapTimeTouch => _platformSettings?.GetDoubleTapTime(PointerType.Touch).ToString() ?? "Not available";
+
+ public string DoubleTapTimeMouse => _platformSettings?.GetDoubleTapTime(PointerType.Mouse).ToString() ?? "Not available";
+}
+
diff --git a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs
index 5f17627ace..afc2673861 100644
--- a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs
+++ b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs
@@ -1,4 +1,5 @@
using System;
+using System.Globalization;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Media;
@@ -39,6 +40,9 @@ namespace Avalonia.Platform
public PlatformHotkeyConfiguration HotkeyConfiguration =>
AvaloniaLocator.Current.GetRequiredService();
+ public virtual string PreferredApplicationLanguage =>
+ CultureInfo.CurrentUICulture.Name;
+
public virtual PlatformColorValues GetColorValues()
{
return new PlatformColorValues
@@ -48,11 +52,18 @@ namespace Avalonia.Platform
}
public virtual event EventHandler? ColorValuesChanged;
+ public virtual event EventHandler? PreferredApplicationLanguageChanged;
protected void OnColorValuesChanged(PlatformColorValues colorValues)
{
Dispatcher.UIThread.Send(
_ => ColorValuesChanged?.Invoke(this, colorValues));
}
+
+ protected void OnPreferredApplicationLanguageChanged()
+ {
+ Dispatcher.UIThread.Send(
+ _ => PreferredApplicationLanguageChanged?.Invoke(this, EventArgs.Empty));
+ }
}
}
diff --git a/src/Avalonia.Base/Platform/IPlatformSettings.cs b/src/Avalonia.Base/Platform/IPlatformSettings.cs
index 46980c6d51..61a8f1870a 100644
--- a/src/Avalonia.Base/Platform/IPlatformSettings.cs
+++ b/src/Avalonia.Base/Platform/IPlatformSettings.cs
@@ -37,12 +37,17 @@ namespace Avalonia.Platform
/// Holding duration between pointer press and when event is fired.
///
TimeSpan HoldWaitDuration { get; }
-
+
///
/// Get a configuration for platform-specific hotkeys in an Avalonia application.
///
PlatformHotkeyConfiguration HotkeyConfiguration { get; }
-
+
+ ///
+ /// Gets the preferred application language as specified in the operating system settings.
+ ///
+ string PreferredApplicationLanguage { get; }
+
///
/// Gets current system color values including dark mode and accent colors.
///
@@ -52,5 +57,10 @@ namespace Avalonia.Platform
/// Raises when current system color values are changed. Including changing of a dark mode and accent colors.
///
event EventHandler? ColorValuesChanged;
+
+ ///
+ /// Raises when the preferred application language is changed in the operating system settings.
+ ///
+ event EventHandler? PreferredApplicationLanguageChanged;
}
}
diff --git a/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs b/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs
index 2a3cef4334..ea6b064bff 100644
--- a/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs
+++ b/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics.CodeAnalysis;
using Avalonia.Browser.Interop;
using Avalonia.Platform;
@@ -9,20 +10,41 @@ internal class BrowserPlatformSettings : DefaultPlatformSettings
private bool _isDarkMode;
private bool _isHighContrast;
private bool _isInitialized;
+ private string? _lastLanguage;
public override event EventHandler? ColorValuesChanged
{
add
{
- EnsureBackend();
+ EnsureSettings();
base.ColorValuesChanged += value;
}
remove => base.ColorValuesChanged -= value;
}
+ public override event EventHandler? PreferredApplicationLanguageChanged
+ {
+ add
+ {
+ EnsureSettings();
+ base.PreferredApplicationLanguageChanged += value;
+ }
+ remove => base.PreferredApplicationLanguageChanged -= value;
+ }
+
+ public override string PreferredApplicationLanguage
+ {
+ get
+ {
+ EnsureSettings();
+
+ return _lastLanguage ?? base.PreferredApplicationLanguage;
+ }
+ }
+
public override PlatformColorValues GetColorValues()
{
- EnsureBackend();
+ EnsureSettings();
return base.GetColorValues() with
{
@@ -31,18 +53,27 @@ internal class BrowserPlatformSettings : DefaultPlatformSettings
};
}
- public void OnValuesChanged(bool isDarkMode, bool isHighContrast)
+ public void OnColorValuesChanged(bool isDarkMode, bool isHighContrast)
{
_isDarkMode = isDarkMode;
_isHighContrast = isHighContrast;
OnColorValuesChanged(GetColorValues());
}
-
- private void EnsureBackend()
+
+ public void OnPreferredLanguageChanged(string? language)
+ {
+ if (language is not null && _lastLanguage != language)
+ {
+ _lastLanguage = language;
+ OnPreferredApplicationLanguageChanged();
+ }
+ }
+
+ private void EnsureSettings()
{
if (!_isInitialized)
{
- // WASM module has async nature of initialization. We can't native code right away during components registration.
+ // WASM module has async nature of initialization. We can't call platform code right away during components registration.
_isInitialized = true;
var values = DomHelper.GetDarkMode(BrowserWindowingPlatform.GlobalThis);
if (values.Length == 2)
@@ -50,6 +81,8 @@ internal class BrowserPlatformSettings : DefaultPlatformSettings
_isDarkMode = values[0] > 0;
_isHighContrast = values[1] > 0;
}
+
+ _lastLanguage = DomHelper.GetNavigatorLanguage(BrowserWindowingPlatform.GlobalThis);
}
}
}
diff --git a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs
index 4b929660e5..22b924b0bd 100644
--- a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs
+++ b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs
@@ -1,4 +1,4 @@
-using System.Runtime.InteropServices.JavaScript;
+using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform;
@@ -31,6 +31,9 @@ internal static partial class DomHelper
[JSImport("AvaloniaDOM.getDarkMode", AvaloniaModule.MainModuleName)]
public static partial int[] GetDarkMode(JSObject globalThis);
+ [JSImport("AvaloniaDOM.getNavigatorLanguage", AvaloniaModule.MainModuleName)]
+ public static partial string? GetNavigatorLanguage(JSObject globalThis);
+
[JSImport("AvaloniaDOM.addClass", AvaloniaModule.MainModuleName)]
public static partial void AddCssClass(JSObject element, string className);
@@ -40,7 +43,7 @@ internal static partial class DomHelper
[JSExport]
public static Task DarkModeChanged(bool isDarkMode, bool isHighContrast)
{
- (AvaloniaLocator.Current.GetService() as BrowserPlatformSettings)?.OnValuesChanged(isDarkMode, isHighContrast);
+ (AvaloniaLocator.Current.GetService() as BrowserPlatformSettings)?.OnColorValuesChanged(isDarkMode, isHighContrast);
return Task.CompletedTask;
}
@@ -51,6 +54,13 @@ internal static partial class DomHelper
return Task.CompletedTask;
}
+ [JSExport]
+ public static Task LanguageChanged(string language)
+ {
+ (AvaloniaLocator.Current.GetService() as BrowserPlatformSettings)?.OnPreferredLanguageChanged(language);
+ return Task.CompletedTask;
+ }
+
[JSExport]
public static Task ScreensChanged()
{
diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts
index 175e51c0da..a09f926c07 100644
--- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts
+++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts
@@ -136,6 +136,10 @@ export class AvaloniaDOM {
});
}
+ globalThis.addEventListener("languagechange", () => {
+ JsExports.DomHelper.LanguageChanged(globalThis.navigator.language);
+ });
+
globalThis.document.addEventListener("visibilitychange", () => {
JsExports.DomHelper.DocumentVisibilityChanged(globalThis.document.visibilityState);
});
@@ -167,4 +171,8 @@ export class AvaloniaDOM {
prefersContrastMedia.matches ? 1 : 0
];
}
+
+ public static getNavigatorLanguage(globalThis: Window): string | null {
+ return globalThis.navigator?.language ?? null;
+ }
}
diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
index 1f8b0bdce3..edfd4dd9b5 100644
--- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
+++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
@@ -1334,6 +1334,9 @@ namespace Avalonia.Win32.Interop
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "GetModuleHandleW", ExactSpelling = true)]
public static extern IntPtr GetModuleHandle(string? lpModuleName);
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ public static extern int GetUserDefaultLocaleName(char* lpLocaleName, int cchLocaleName);
+
[DllImport("user32.dll")]
public static extern int GetSystemMetrics(SystemMetric smIndex);
diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs
index 7903a62d8f..1fdf3e59fa 100644
--- a/src/Windows/Avalonia.Win32/Win32Platform.cs
+++ b/src/Windows/Avalonia.Win32/Win32Platform.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
@@ -183,6 +183,10 @@ namespace Avalonia.Win32
{
win32PlatformSettings.OnColorValuesChanged();
}
+ else if (changedSetting == "intl") // language/locale change
+ {
+ win32PlatformSettings.OnLanguageChanged();
+ }
}
if (msg == (uint)WindowsMessage.WM_TIMER)
diff --git a/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs b/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs
index c4b016c1b9..de2a871af7 100644
--- a/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs
+++ b/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs
@@ -13,6 +13,7 @@ internal class Win32PlatformSettings : DefaultPlatformSettings
&& WinRTApiInformation.IsTypePresent("Windows.UI.ViewManagement.AccessibilitySettings"));
private PlatformColorValues? _lastColorValues;
+ private string? _lastLanguage;
public override Size GetTapSize(PointerType type)
{
@@ -33,6 +34,27 @@ internal class Win32PlatformSettings : DefaultPlatformSettings
}
public override TimeSpan GetDoubleTapTime(PointerType type) => TimeSpan.FromMilliseconds(GetDoubleClickTime());
+
+ public override string PreferredApplicationLanguage
+ {
+ get
+ {
+ if (_lastLanguage is null)
+ {
+ unsafe
+ {
+ const int LOCALE_NAME_MAX_LENGTH = 85;
+ var buffer = stackalloc char[LOCALE_NAME_MAX_LENGTH];
+ var length = GetUserDefaultLocaleName(buffer, LOCALE_NAME_MAX_LENGTH);
+ _lastLanguage = length > 0
+ ? new string(buffer, 0, length - 1)
+ : base.PreferredApplicationLanguage;
+ }
+ }
+
+ return _lastLanguage;
+ }
+ }
public override PlatformColorValues GetColorValues()
{
@@ -88,4 +110,16 @@ internal class Win32PlatformSettings : DefaultPlatformSettings
OnColorValuesChanged(colorValues);
}
}
+
+ internal void OnLanguageChanged()
+ {
+ var oldLanguage = _lastLanguage;
+ _lastLanguage = null;
+ var newLanguage = PreferredApplicationLanguage;
+
+ if (oldLanguage != newLanguage)
+ {
+ OnPreferredApplicationLanguageChanged();
+ }
+ }
}