From f3a8cf14628c17afc628c3ede4cfe6088338c866 Mon Sep 17 00:00:00 2001 From: "Vlad (Kuzmin) Erium" Date: Thu, 14 Aug 2025 21:04:34 +0900 Subject: [PATCH] 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. --- .../Common/LocalizationService.cpp | 21 +- src/Calculator/App.xaml.cs | 26 ++ src/Calculator/Utils/LanguageHelper.cs | 256 ++++++++++++++++++ src/Calculator/Views/Settings.xaml | 15 + src/Calculator/Views/Settings.xaml.cs | 67 +++++ 5 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 src/Calculator/Utils/LanguageHelper.cs diff --git a/src/CalcViewModel/Common/LocalizationService.cpp b/src/CalcViewModel/Common/LocalizationService.cpp index 5c28e42d..a331b5d4 100644 --- a/src/CalcViewModel/Common/LocalizationService.cpp +++ b/src/CalcViewModel/Common/LocalizationService.cpp @@ -74,8 +74,27 @@ void LocalizationService::OverrideWithLanguage(_In_ const wchar_t* const languag /// RFC-5646 identifier of the language to use, if null, will use the current language of the system 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()); diff --git a/src/Calculator/App.xaml.cs b/src/Calculator/App.xaml.cs index 13fe4a6f..1c5bc286 100644 --- a/src/Calculator/App.xaml.cs +++ b/src/Calculator/App.xaml.cs @@ -35,6 +35,20 @@ namespace CalculatorApp /// 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(); } diff --git a/src/Calculator/Utils/LanguageHelper.cs b/src/Calculator/Utils/LanguageHelper.cs new file mode 100644 index 00000000..a6483775 --- /dev/null +++ b/src/Calculator/Utils/LanguageHelper.cs @@ -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 +{ + /// + /// Helper class for managing application language settings + /// + public static class LanguageHelper + { + private const string SelectedLanguageKey = "SelectedLanguage"; // "system" or BCP-47 tag + + /// + /// Initialize the application to use the System Display Language instead of Regional Settings + /// + 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 + } + } + + /// + /// Persisted selected language code. "system" means follow system (Display Language) + /// + public static string SelectedLanguage + { + get => ApplicationData.Current.LocalSettings.Values[SelectedLanguageKey]?.ToString() ?? "system"; + set => ApplicationData.Current.LocalSettings.Values[SelectedLanguageKey] = value; + } + + /// + /// Return available UI languages for selection (System Default + supported app languages) + /// + public static List GetAvailableLanguages() + { + var list = new List(); + 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 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" + }; + } + + /// + /// Apply a user-selected language code (or "system") immediately and persist preference + /// + 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); + } + + /// + /// Gets the system's Display Language (not Regional Settings) + /// + /// The primary display language code (e.g., "en-US", "ja-JP") + 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; + } + + /// + /// Sets the application language using ApplicationLanguages.PrimaryLanguageOverride + /// + /// Language code (e.g., "en-US", "ja-JP") + 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; + } + } + + /// + /// Apply the specified language to ResourceContext used outside of any view + /// + 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}"); + } + } + + /// + /// Apply the specified language to ResourceContext for the current view + /// + 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}"); + } + } + + /// + /// Clears the application language override (for debugging/testing) + /// + 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}"); + } + } + + /// + /// Gets debug information about current language settings + /// + 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; + } + } +} diff --git a/src/Calculator/Views/Settings.xaml b/src/Calculator/Views/Settings.xaml index dbd1fc01..3c2ab130 100644 --- a/src/Calculator/Views/Settings.xaml +++ b/src/Calculator/Views/Settings.xaml @@ -109,6 +109,21 @@ + + + + + + + + + + + ().FirstOrDefault(c => c?.Tag?.ToString() == currentTheme)).IsChecked = true; + // Initialize language selection to current preference + try + { + var items = LanguageComboBox.ItemsSource as List; + 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)