Add language selection and system display language support

Introduces a LanguageHelper utility to manage application language settings, preferring system display language over regional settings. Adds a language selection UI to Settings, persists user choice, and applies language overrides at startup and view creation. Updates LocalizationService to use display language when no override is set.
This commit is contained in:
Vlad (Kuzmin) Erium 2025-08-14 21:04:34 +09:00
commit f3a8cf1462
5 changed files with 384 additions and 1 deletions

View file

@ -74,8 +74,27 @@ void LocalizationService::OverrideWithLanguage(_In_ const wchar_t* const languag
/// <param name="overridedLanguage">RFC-5646 identifier of the language to use, if null, will use the current language of the system</param>
LocalizationService::LocalizationService(_In_ const wchar_t * const overridedLanguage)
{
using namespace Windows::System::UserProfile;
m_isLanguageOverrided = overridedLanguage != nullptr;
m_language = m_isLanguageOverrided ? ref new Platform::String(overridedLanguage) : ApplicationLanguages::Languages->GetAt(0);
if (m_isLanguageOverrided)
{
m_language = ref new Platform::String(overridedLanguage);
}
else
{
// Prefer the system Display Language over Regional Settings
auto displayLanguages = GlobalizationPreferences::Languages;
if (displayLanguages != nullptr && displayLanguages->Size > 0)
{
m_language = ref new Platform::String(displayLanguages->GetAt(0)->Data());
}
else
{
// Fallback to the default application language list
m_language = ApplicationLanguages::Languages->GetAt(0);
}
}
m_flowDirection = ResourceContext::GetForViewIndependentUse()->QualifierValues->Lookup(L"LayoutDirection")
!= L"LTR" ? FlowDirection::RightToLeft : FlowDirection::LeftToRight;
wstring localeName = wstring(m_language->Data());

View file

@ -35,6 +35,20 @@ namespace CalculatorApp
/// </summary>
public App()
{
// Ensure we choose the UI language based on System Display Language
// before any resources are loaded or singletons are created.
// If user selected a forced language, apply it; otherwise use Display Language
var selected = LanguageHelper.SelectedLanguage;
if (!string.IsNullOrEmpty(selected) && !string.Equals(selected, "system", StringComparison.OrdinalIgnoreCase))
{
LanguageHelper.ApplyUserLanguageSelection(selected);
}
else
{
LanguageHelper.InitializeDisplayLanguage();
}
LanguageHelper.LogLanguageDebugInfo(); // Debug
InitializeComponent();
NarratorNotifier.RegisterDependencyProperties();
@ -149,6 +163,18 @@ namespace CalculatorApp
// Place the frame in the current Window
Window.Current.Content = rootFrame;
ThemeHelper.InitializeAppTheme();
// Initialize Calculator to use System Display Language instead of Regional Settings
// Re-apply language at view creation based on persisted selection
var selected = LanguageHelper.SelectedLanguage;
var effective = selected == "system" ? LanguageHelper.GetSystemDisplayLanguage() : selected;
if (!string.IsNullOrEmpty(effective))
{
LanguageHelper.ApplyViewResourceLanguages(effective);
LanguageHelper.ApplyGlobalResourceLanguages(effective);
}
LanguageHelper.LogLanguageDebugInfo(); // Debug
Window.Current.Activate();
}

View file

@ -0,0 +1,256 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Windows.Storage;
namespace CalculatorApp.Utils
{
/// <summary>
/// Helper class for managing application language settings
/// </summary>
public static class LanguageHelper
{
private const string SelectedLanguageKey = "SelectedLanguage"; // "system" or BCP-47 tag
/// <summary>
/// Initialize the application to use the System Display Language instead of Regional Settings
/// </summary>
public static void InitializeDisplayLanguage()
{
try
{
var displayLanguage = GetSystemDisplayLanguage();
if (!string.IsNullOrEmpty(displayLanguage))
{
Debug.WriteLine($"Setting Calculator to use Display Language: {displayLanguage}");
SetApplicationLanguage(displayLanguage);
}
else
{
Debug.WriteLine("Could not detect system display language, using default behavior");
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error initializing display language: {ex.Message}");
// Don't throw - let the app continue with default behavior
}
}
/// <summary>
/// Persisted selected language code. "system" means follow system (Display Language)
/// </summary>
public static string SelectedLanguage
{
get => ApplicationData.Current.LocalSettings.Values[SelectedLanguageKey]?.ToString() ?? "system";
set => ApplicationData.Current.LocalSettings.Values[SelectedLanguageKey] = value;
}
/// <summary>
/// Return available UI languages for selection (System Default + supported app languages)
/// </summary>
public static List<LanguageInfo> GetAvailableLanguages()
{
var list = new List<LanguageInfo>();
list.Add(new LanguageInfo { Code = "system", NativeName = "System Default" });
foreach (var code in GetSupportedLanguageCodes())
{
try
{
var lang = new Windows.Globalization.Language(code);
list.Add(new LanguageInfo { Code = code, NativeName = lang.NativeName });
}
catch
{
list.Add(new LanguageInfo { Code = code, NativeName = code });
}
}
return list;
}
private static IEnumerable<string> GetSupportedLanguageCodes()
{
// Full list based on src/Calculator/Resources/* folders
return new[]
{
"af-ZA","am-ET","ar-SA","az-Latn-AZ","bg-BG","ca-ES","cs-CZ","da-DK","de-DE","el-GR",
"en-GB","en-US","es-ES","es-MX","et-EE","eu-ES","fa-IR","fi-FI","fil-PH","fr-CA","fr-FR",
"gl-ES","he-IL","hi-IN","hr-HR","hu-HU","id-ID","is-IS","it-IT","ja-JP","kk-KZ","km-KH",
"kn-IN","ko-KR","lo-LA","lt-LT","lv-LV","mk-MK","ml-IN","ms-MY","nb-NO","nl-NL","pl-PL",
"pt-BR","pt-PT","ro-RO","ru-RU","sk-SK","sl-SI","sq-AL","sr-Latn-RS","sv-SE","ta-IN",
"te-IN","th-TH","tr-TR","uk-UA","vi-VN","zh-CN","zh-TW"
};
}
/// <summary>
/// Apply a user-selected language code (or "system") immediately and persist preference
/// </summary>
public static void ApplyUserLanguageSelection(string code)
{
SelectedLanguage = code;
string languageToUse = code == "system" ? GetSystemDisplayLanguage() : code;
if (string.IsNullOrEmpty(languageToUse))
{
Debug.WriteLine("No language resolved; skipping override");
return;
}
SetApplicationLanguage(languageToUse);
ApplyViewResourceLanguages(languageToUse);
ApplyGlobalResourceLanguages(languageToUse);
}
/// <summary>
/// Gets the system's Display Language (not Regional Settings)
/// </summary>
/// <returns>The primary display language code (e.g., "en-US", "ja-JP")</returns>
public static string GetSystemDisplayLanguage()
{
try
{
// Primary method: Get the user's preferred UI languages from GlobalizationPreferences
// This returns the Display Language, not Regional Settings
var userLanguages = Windows.System.UserProfile.GlobalizationPreferences.Languages;
if (userLanguages.Count > 0)
{
var primaryLanguage = userLanguages[0];
Debug.WriteLine($"Display Language from GlobalizationPreferences: {primaryLanguage}");
return primaryLanguage;
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error getting GlobalizationPreferences: {ex.Message}");
// Fallback method: Try ResourceContext
try
{
var resourceContext = Windows.ApplicationModel.Resources.Core.ResourceContext.GetForCurrentView();
if (resourceContext.Languages.Count > 0)
{
var primaryLanguage = resourceContext.Languages[0];
Debug.WriteLine($"Display Language from ResourceContext: {primaryLanguage}");
return primaryLanguage;
}
}
catch (Exception ex2)
{
Debug.WriteLine($"Error getting ResourceContext languages: {ex2.Message}");
}
}
return null;
}
/// <summary>
/// Sets the application language using ApplicationLanguages.PrimaryLanguageOverride
/// </summary>
/// <param name="languageCode">Language code (e.g., "en-US", "ja-JP")</param>
public static void SetApplicationLanguage(string languageCode)
{
try
{
// Set the primary language override for this application
// This overrides the default language resolution and forces the app to use the specified language
Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = languageCode;
Debug.WriteLine($"ApplicationLanguages.PrimaryLanguageOverride set to: {languageCode}");
}
catch (Exception ex)
{
Debug.WriteLine($"Error setting application language to {languageCode}: {ex.Message}");
throw;
}
}
/// <summary>
/// Apply the specified language to ResourceContext used outside of any view
/// </summary>
public static void ApplyGlobalResourceLanguages(string languageCode)
{
try
{
// Set process-wide qualifier for language
Windows.ApplicationModel.Resources.Core.ResourceContext.SetGlobalQualifierValue("Language", languageCode);
Debug.WriteLine($"Global qualifier 'Language' set to: {languageCode}");
}
catch (Exception ex)
{
Debug.WriteLine($"Error applying global resource language: {ex.Message}");
}
}
/// <summary>
/// Apply the specified language to ResourceContext for the current view
/// </summary>
public static void ApplyViewResourceLanguages(string languageCode)
{
try
{
var ctx = Windows.ApplicationModel.Resources.Core.ResourceContext.GetForCurrentView();
// Set qualifier value for this view
ctx.QualifierValues["Language"] = languageCode;
Debug.WriteLine($"View qualifier 'Language' set to: {languageCode}");
}
catch (Exception ex)
{
Debug.WriteLine($"Error applying view resource language: {ex.Message}");
}
}
/// <summary>
/// Clears the application language override (for debugging/testing)
/// </summary>
public static void ClearApplicationLanguageOverride()
{
try
{
Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = "";
Debug.WriteLine("ApplicationLanguages.PrimaryLanguageOverride cleared");
}
catch (Exception ex)
{
Debug.WriteLine($"Error clearing application language override: {ex.Message}");
}
}
/// <summary>
/// Gets debug information about current language settings
/// </summary>
public static void LogLanguageDebugInfo()
{
try
{
var currentOverride = Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride;
var systemLanguages = Windows.System.UserProfile.GlobalizationPreferences.Languages;
var appLanguages = Windows.Globalization.ApplicationLanguages.Languages;
Debug.WriteLine("=== Language Debug Info ===");
Debug.WriteLine($"PrimaryLanguageOverride: '{currentOverride}'");
Debug.WriteLine($"System Display Languages: {string.Join(", ", systemLanguages)}");
Debug.WriteLine($"Application Languages: {string.Join(", ", appLanguages)}");
Debug.WriteLine("===========================");
}
catch (Exception ex)
{
Debug.WriteLine($"Error getting language debug info: {ex.Message}");
}
}
}
public class LanguageInfo
{
public string Code { get; set; }
public string NativeName { get; set; }
public override string ToString()
{
return string.IsNullOrEmpty(NativeName) ? Code : NativeName;
}
}
}

View file

@ -109,6 +109,21 @@
</toolkit:SettingsExpander.Items>
</toolkit:SettingsExpander>
<toolkit:SettingsExpander x:Name="LanguageExpander"
Header="Language"
Description="Choose application language (requires restart for full effect)">
<toolkit:SettingsExpander.HeaderIcon>
<FontIcon Glyph="&#xF2B7;"/>
</toolkit:SettingsExpander.HeaderIcon>
<toolkit:SettingsExpander.Items>
<toolkit:SettingsCard ContentAlignment="Left">
<ComboBox x:Name="LanguageComboBox"
MinWidth="220"
SelectionChanged="OnLanguageSelectionChanged"/>
</toolkit:SettingsCard>
</toolkit:SettingsExpander.Items>
</toolkit:SettingsExpander>
<TextBlock x:Name="AboutGroupTitle"
Margin="0,30,0,4"
Style="{StaticResource BodyStrongTextBlockStyle}"

View file

@ -13,6 +13,7 @@ using Windows.UI.Xaml;
using Windows.UI.Xaml.Automation.Peers;
using Windows.UI.Xaml.Automation.Provider;
using Windows.UI.Xaml.Controls;
using System.Collections.Generic;
// The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236
@ -21,6 +22,7 @@ namespace CalculatorApp
public sealed partial class Settings : UserControl
{
private const string BUILD_YEAR = "2025";
private bool _isSettingLanguageProgrammatically = false;
public event Windows.UI.Xaml.RoutedEventHandler BackButtonClick;
@ -48,6 +50,8 @@ namespace CalculatorApp
AboutExpander.Description = copyrightText;
InitializeContributeTextBlock();
InitializeLanguageSettings();
}
private void OnThemeSelectionChanged(object sender, SelectionChangedEventArgs e)
@ -73,6 +77,27 @@ namespace CalculatorApp
var currentTheme = ThemeHelper.RootTheme.ToString();
(ThemeRadioButtons.Items.Cast<RadioButton>().FirstOrDefault(c => c?.Tag?.ToString() == currentTheme)).IsChecked = true;
// Initialize language selection to current preference
try
{
var items = LanguageComboBox.ItemsSource as List<CalculatorApp.Utils.LanguageInfo>;
var selected = CalculatorApp.Utils.LanguageHelper.SelectedLanguage;
if (items != null)
{
foreach (var it in items)
{
if (it.Code == selected)
{
_isSettingLanguageProgrammatically = true;
LanguageComboBox.SelectedItem = it;
_isSettingLanguageProgrammatically = false;
break;
}
}
}
}
catch { _isSettingLanguageProgrammatically = false; }
SetDefaultFocus();
}
@ -139,6 +164,48 @@ namespace CalculatorApp
ContributeRunAfterLink.Text = contributeTextAfterHyperlink;
}
private void InitializeLanguageSettings()
{
try
{
var list = CalculatorApp.Utils.LanguageHelper.GetAvailableLanguages();
LanguageComboBox.ItemsSource = list;
}
catch (Exception ex)
{
Debug.WriteLine($"Language init error: {ex.Message}");
}
}
private void OnLanguageSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_isSettingLanguageProgrammatically)
{
return;
}
if (e.AddedItems.Count == 0)
return;
if (e.AddedItems[0] is CalculatorApp.Utils.LanguageInfo lang)
{
var current = CalculatorApp.Utils.LanguageHelper.SelectedLanguage;
if (string.Equals(current, lang.Code, StringComparison.OrdinalIgnoreCase))
{
return; // no actual change
}
CalculatorApp.Utils.LanguageHelper.ApplyUserLanguageSelection(lang.Code);
// Inform user that full app restart may be required to update all resources
var title = AppResourceProvider.GetInstance().GetResourceString("LanguageChangeDialog/Title");
var message = AppResourceProvider.GetInstance().GetResourceString("LanguageChangeDialog/Message");
if (string.IsNullOrEmpty(title)) title = "Language Changed";
if (string.IsNullOrEmpty(message)) message = "Some UI may update after restart.";
_ = new ContentDialog { Title = title, Content = message, PrimaryButtonText = "OK" }.ShowAsync();
}
}
private void System_BackRequested(object sender, BackRequestedEventArgs e)
{
if (!e.Handled && BackButton.IsEnabled)