mirror of
https://github.com/Microsoft/calculator.git
synced 2025-08-21 22:03:11 -07:00
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:
parent
5b21e4c58b
commit
f3a8cf1462
5 changed files with 384 additions and 1 deletions
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
256
src/Calculator/Utils/LanguageHelper.cs
Normal file
256
src/Calculator/Utils/LanguageHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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=""/>
|
||||
</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}"
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue