From c1f7865a6e7cd55dd3113c23c35d1aa175a59bd5 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Sat, 20 Jun 2026 23:49:51 -0500 Subject: [PATCH 1/8] Add output locale override setting * Add output.locale setting and schema support * Apply runtime language qualifier override for resource resolution * Include localized resw fallbacks in dev package and update docs/release notes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/ReleaseNotes.md | 8 +++- doc/Settings.md | 17 ++++++++ .../package-manager/winget/settings.md | 18 ++++++++- .../JSON/settings/settings.schema.0.2.json | 6 +++ src/AppInstallerCLICore/Core.cpp | 31 ++++++++++++++ .../AppInstallerCLIPackage.wapproj | 10 +++++ src/AppInstallerCLITests/UserSettings.cpp | 40 +++++++++++++++++++ .../Public/winget/UserSettings.h | 2 + src/AppInstallerCommonCore/UserSettings.cpp | 10 +++++ 9 files changed, 140 insertions(+), 2 deletions(-) diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index 57c970aeb6..ae8120298f 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -1,6 +1,12 @@ ## New in v1.29 -Nothing yet. +## New Features + +### Output locale override + +Added a persistent `output.locale` setting to override winget interface language using a BCP47 tag. + +Usage: add `"output": { "locale": "de-DE" }` to `settings.json`. ## Bug Fixes diff --git a/doc/Settings.md b/doc/Settings.md index 0dfe03b3e4..7e7e68994b 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -388,6 +388,23 @@ The `interactivity` settings control whether winget may show interactive prompts If set to true, the `interactivity.disable` setting will prevent any interactive prompt from being shown. +## Output + +The `output` settings control how winget presents CLI output. + +### locale + +The `locale` setting overrides winget interface language by BCP47 tag (for example, `en-US`). If this setting is missing or invalid, winget uses the default Windows globalization behavior. + +> [!NOTE] +> This only affects winget interface strings. It does not change package metadata localization or installer selection behavior. + +```json + "output": { + "locale": "en-US" + }, +``` + ## Experimental Features To allow work to be done and distributed to early adopters for feedback, settings can be used to enable "experimental" features. diff --git a/doc/windows/package-manager/winget/settings.md b/doc/windows/package-manager/winget/settings.md index a4fc50e16d..902ac5c5eb 100644 --- a/doc/windows/package-manager/winget/settings.md +++ b/doc/windows/package-manager/winget/settings.md @@ -98,6 +98,23 @@ The `locale` behavior affects the choice of installer based on installer locale. }, ``` +### Output + +The `output` settings affect winget interface output behavior. + +#### locale + +The `locale` setting overrides winget interface language using a BCP47 language tag (for example, `en-US`). If not specified, winget uses the default Windows globalization behavior. + +> [!NOTE] +> This setting only affects winget interface strings and does not affect package metadata localization or installer locale selection. + +```json + "output": { + "locale": "en-US" + }, +``` + ### Telemetry The `telemetry` settings control whether winget writes ETW events that may be sent to Microsoft on a default installation of Windows. @@ -133,4 +150,3 @@ The `downloader` setting controls which code is used when downloading packages. ## Enabling Experimental features To discover which experimental features are available, go to [https://aka.ms/winget-settings](https://aka.ms/winget-settings) where you can see the experimental features available to you. - diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json index 06d6314d91..38ee8b33af 100644 --- a/schemas/JSON/settings/settings.schema.0.2.json +++ b/schemas/JSON/settings/settings.schema.0.2.json @@ -378,6 +378,12 @@ "descending" ], "default": "ascending" + }, + "locale": { + "description": "Overrides winget interface output language using a BCP47 language tag", + "type": "string", + "pattern": "^([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*$", + "maxLength": 20 } } } diff --git a/src/AppInstallerCLICore/Core.cpp b/src/AppInstallerCLICore/Core.cpp index a5890bd9cf..3850ad20e9 100644 --- a/src/AppInstallerCLICore/Core.cpp +++ b/src/AppInstallerCLICore/Core.cpp @@ -10,6 +10,7 @@ #include "COMContext.h" #include #include +#include #include "Public/ShutdownMonitoring.h" #ifndef AICLI_DISABLE_TEST_HOOKS @@ -78,6 +79,18 @@ namespace AppInstaller::CLI main.Wait = WaitOnMainWaitEvent; ShutdownMonitoring::ServerShutdownSynchronization::AddComponent(main); } + + std::optional ApplyOutputLocaleOverride() + { + std::string localePreference = Settings::User().Get(); + if (!localePreference.empty()) + { + winrt::Windows::Globalization::ApplicationLanguages::PrimaryLanguageOverride(Utility::ConvertToUTF16(localePreference)); + return localePreference; + } + + return {}; + } } int CoreMain(int argc, wchar_t const** argv) try @@ -89,6 +102,7 @@ namespace AppInstaller::CLI std::signal(SIGABRT, abort_signal_handler); init_apartment(); + auto outputLocaleOverride = ApplyOutputLocaleOverride(); #ifndef AICLI_DISABLE_TEST_HOOKS // We have to do this here so the auto minidump config initialization gets caught @@ -120,6 +134,11 @@ namespace AppInstaller::CLI Logging::OutputDebugStringLogger::Remove(); Logging::EnableWilFailureTelemetry(); + if (outputLocaleOverride) + { + AICLI_LOG(CLI, Info, << "Applied output locale override from settings: " << outputLocaleOverride.value()); + } + // Set output to UTF8 ConsoleOutputCPRestore utf8CP(CP_UTF8); @@ -212,6 +231,8 @@ namespace AppInstaller::CLI void ServerInitialize() { + auto outputLocaleOverride = ApplyOutputLocaleOverride(); + #ifndef AICLI_DISABLE_TEST_HOOKS // We have to do this here so the auto minidump config initialization gets caught Logging::OutputDebugStringLogger::Add(); @@ -227,10 +248,16 @@ namespace AppInstaller::CLI #endif AppInstaller::CLI::Execution::COMContext::SetLoggers(); + if (outputLocaleOverride) + { + AICLI_LOG(CLI, Info, << "Applied output locale override from settings: " << outputLocaleOverride.value()); + } } void InProcInitialize() { + auto outputLocaleOverride = ApplyOutputLocaleOverride(); + #ifndef AICLI_DISABLE_TEST_HOOKS // We have to do this here so the auto minidump config initialization gets caught Logging::OutputDebugStringLogger::Add(); @@ -247,5 +274,9 @@ namespace AppInstaller::CLI // Explicitly set default channel and level before user settings from PackageManagerSettings AppInstaller::CLI::Execution::COMContext::SetLoggers(AppInstaller::Logging::Channel::Defaults, AppInstaller::Logging::Level::Info); + if (outputLocaleOverride) + { + AICLI_LOG(CLI, Info, << "Applied output locale override from settings: " << outputLocaleOverride.value()); + } } } diff --git a/src/AppInstallerCLIPackage/AppInstallerCLIPackage.wapproj b/src/AppInstallerCLIPackage/AppInstallerCLIPackage.wapproj index 738507f0b0..020eb52aad 100644 --- a/src/AppInstallerCLIPackage/AppInstallerCLIPackage.wapproj +++ b/src/AppInstallerCLIPackage/AppInstallerCLIPackage.wapproj @@ -187,15 +187,25 @@ Designer + + + + + + + + + + diff --git a/src/AppInstallerCLITests/UserSettings.cpp b/src/AppInstallerCLITests/UserSettings.cpp index c4d63e0905..c8e13fb912 100644 --- a/src/AppInstallerCLITests/UserSettings.cpp +++ b/src/AppInstallerCLITests/UserSettings.cpp @@ -925,6 +925,46 @@ TEST_CASE("SettingOutputSortDirection", "[settings]") } } +TEST_CASE("SettingOutputLocale", "[settings]") +{ + auto again = DeleteUserSettingsFiles(); + + SECTION("Default value") + { + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get().empty()); + REQUIRE(userSettingTest.GetWarnings().size() == 0); + } + SECTION("Valid locale") + { + std::string_view json = R"({ "output": { "locale": "en-US" } })"; + SetSetting(Stream::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get() == "en-US"); + REQUIRE(userSettingTest.GetWarnings().size() == 0); + } + SECTION("Invalid locale") + { + std::string_view json = R"({ "output": { "locale": "en_US.UTF-8" } })"; + SetSetting(Stream::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get().empty()); + REQUIRE(userSettingTest.GetWarnings().size() == 1); + } + SECTION("Wrong type") + { + std::string_view json = R"({ "output": { "locale": ["en-US"] } })"; + SetSetting(Stream::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get().empty()); + REQUIRE(userSettingTest.GetWarnings().size() == 1); + } +} + TEST_CASE("ConvertToSortField", "[settings]") { SECTION("Valid values - lowercase") diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index 0b194d3ce9..510e98aff0 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -144,6 +144,7 @@ namespace AppInstaller::Settings // Output behavior OutputSortOrder, OutputSortDirection, + OutputLocale, #ifndef AICLI_DISABLE_TEST_HOOKS // Debug EnableSelfInitiatedMinidump, @@ -242,6 +243,7 @@ namespace AppInstaller::Settings // Output behavior SETTINGMAPPING_SPECIALIZATION(Setting::OutputSortOrder, std::vector, std::vector, std::vector{}, ".output.sortOrder"sv); SETTINGMAPPING_SPECIALIZATION(Setting::OutputSortDirection, std::string, SortDirection, SortDirection::Ascending, ".output.sortDirection"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::OutputLocale, std::string, std::string, {}, ".output.locale"sv); // Used to deduce the SettingVariant type; making a variant that includes std::monostate and all SettingMapping types. template diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index a7f9bb3b93..f0a66c02c3 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -561,6 +561,16 @@ namespace AppInstaller::Settings return {}; } + + WINGET_VALIDATE_SIGNATURE(OutputLocale) + { + if (Locale::IsWellFormedBcp47Tag(value)) + { + return value; + } + + return {}; + } } #ifndef AICLI_DISABLE_TEST_HOOKS From 5e8451f9eb9f5926c172def8481435213722a4f1 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Tue, 23 Jun 2026 19:38:45 -0500 Subject: [PATCH 2/8] Ignore server and core --- src/AppInstallerCLICore/Core.cpp | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/AppInstallerCLICore/Core.cpp b/src/AppInstallerCLICore/Core.cpp index 3850ad20e9..a0a04da75c 100644 --- a/src/AppInstallerCLICore/Core.cpp +++ b/src/AppInstallerCLICore/Core.cpp @@ -231,8 +231,6 @@ namespace AppInstaller::CLI void ServerInitialize() { - auto outputLocaleOverride = ApplyOutputLocaleOverride(); - #ifndef AICLI_DISABLE_TEST_HOOKS // We have to do this here so the auto minidump config initialization gets caught Logging::OutputDebugStringLogger::Add(); @@ -248,16 +246,10 @@ namespace AppInstaller::CLI #endif AppInstaller::CLI::Execution::COMContext::SetLoggers(); - if (outputLocaleOverride) - { - AICLI_LOG(CLI, Info, << "Applied output locale override from settings: " << outputLocaleOverride.value()); - } } void InProcInitialize() { - auto outputLocaleOverride = ApplyOutputLocaleOverride(); - #ifndef AICLI_DISABLE_TEST_HOOKS // We have to do this here so the auto minidump config initialization gets caught Logging::OutputDebugStringLogger::Add(); @@ -274,9 +266,5 @@ namespace AppInstaller::CLI // Explicitly set default channel and level before user settings from PackageManagerSettings AppInstaller::CLI::Execution::COMContext::SetLoggers(AppInstaller::Logging::Channel::Defaults, AppInstaller::Logging::Level::Info); - if (outputLocaleOverride) - { - AICLI_LOG(CLI, Info, << "Applied output locale override from settings: " << outputLocaleOverride.value()); - } } } From dd556b2c04973fefb77bb2a8ba6e858ed7617ab8 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Tue, 23 Jun 2026 19:42:27 -0500 Subject: [PATCH 3/8] Move application of override locale to same location as log --- src/AppInstallerCLICore/Core.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/AppInstallerCLICore/Core.cpp b/src/AppInstallerCLICore/Core.cpp index a0a04da75c..290a02414b 100644 --- a/src/AppInstallerCLICore/Core.cpp +++ b/src/AppInstallerCLICore/Core.cpp @@ -101,9 +101,7 @@ namespace AppInstaller::CLI std::signal(SIGABRT, abort_signal_handler); - init_apartment(); - auto outputLocaleOverride = ApplyOutputLocaleOverride(); - + init_apartment() #ifndef AICLI_DISABLE_TEST_HOOKS // We have to do this here so the auto minidump config initialization gets caught Logging::OutputDebugStringLogger::Add(); @@ -134,6 +132,7 @@ namespace AppInstaller::CLI Logging::OutputDebugStringLogger::Remove(); Logging::EnableWilFailureTelemetry(); + std::optional outputLocaleOverride = ApplyOutputLocaleOverride(); if (outputLocaleOverride) { AICLI_LOG(CLI, Info, << "Applied output locale override from settings: " << outputLocaleOverride.value()); From 62d7b580ecdb2a89d0391aa82c2ad0e998313e47 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Mon, 29 Jun 2026 15:01:57 -0500 Subject: [PATCH 4/8] Add missing semicolon --- src/AppInstallerCLICore/Core.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AppInstallerCLICore/Core.cpp b/src/AppInstallerCLICore/Core.cpp index 290a02414b..8d43dccd37 100644 --- a/src/AppInstallerCLICore/Core.cpp +++ b/src/AppInstallerCLICore/Core.cpp @@ -101,7 +101,7 @@ namespace AppInstaller::CLI std::signal(SIGABRT, abort_signal_handler); - init_apartment() + init_apartment(); #ifndef AICLI_DISABLE_TEST_HOOKS // We have to do this here so the auto minidump config initialization gets caught Logging::OutputDebugStringLogger::Add(); From bf625f80887f95877cb0e1a3017d553ea204a7a0 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Mon, 29 Jun 2026 15:02:15 -0500 Subject: [PATCH 5/8] Test for valid but unsupported locale --- src/AppInstallerCLITests/UserSettings.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/AppInstallerCLITests/UserSettings.cpp b/src/AppInstallerCLITests/UserSettings.cpp index c8e13fb912..d167aecf51 100644 --- a/src/AppInstallerCLITests/UserSettings.cpp +++ b/src/AppInstallerCLITests/UserSettings.cpp @@ -945,6 +945,15 @@ TEST_CASE("SettingOutputLocale", "[settings]") REQUIRE(userSettingTest.Get() == "en-US"); REQUIRE(userSettingTest.GetWarnings().size() == 0); } + SECTION("Valid but unsupported locale") + { + std::string_view json = R"({ "output": { "locale": "el-GR" } })"; + SetSetting(Stream::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get() == "el-GR"); + REQUIRE(userSettingTest.GetWarnings().size() == 0); + } SECTION("Invalid locale") { std::string_view json = R"({ "output": { "locale": "en_US.UTF-8" } })"; From e7a72b2cef4b9347018108ea097bc5e0d8319ca1 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Mon, 29 Jun 2026 20:26:17 -0500 Subject: [PATCH 6/8] Restrict to valid locales --- .../package-manager/winget/settings.md | 4 +- .../JSON/settings/settings.schema.0.2.json | 17 +++++-- src/AppInstallerCLICore/Core.cpp | 24 +++++---- src/AppInstallerCLITests/UserSettings.cpp | 23 ++++++--- .../Public/winget/UserSettings.h | 22 +++++++- src/AppInstallerCommonCore/UserSettings.cpp | 51 +++++++++++++++++-- 6 files changed, 115 insertions(+), 26 deletions(-) diff --git a/doc/windows/package-manager/winget/settings.md b/doc/windows/package-manager/winget/settings.md index 902ac5c5eb..789a66013f 100644 --- a/doc/windows/package-manager/winget/settings.md +++ b/doc/windows/package-manager/winget/settings.md @@ -104,7 +104,9 @@ The `output` settings affect winget interface output behavior. #### locale -The `locale` setting overrides winget interface language using a BCP47 language tag (for example, `en-US`). If not specified, winget uses the default Windows globalization behavior. +The `locale` setting overrides winget interface language using a supported locale value. If not specified, winget uses the default Windows globalization behavior. + +Supported values: `en-US`, `de-DE`, `es-ES`, `fr-FR`, `it-IT`, `ja-JP`, `ko-KR`, `pt-BR`, `ru-RU`, `zh-CN`, `zh-TW`. > [!NOTE] > This setting only affects winget interface strings and does not affect package metadata localization or installer locale selection. diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json index 38ee8b33af..c47e291aec 100644 --- a/schemas/JSON/settings/settings.schema.0.2.json +++ b/schemas/JSON/settings/settings.schema.0.2.json @@ -380,10 +380,21 @@ "default": "ascending" }, "locale": { - "description": "Overrides winget interface output language using a BCP47 language tag", + "description": "Overrides winget interface output language using a supported locale", "type": "string", - "pattern": "^([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*$", - "maxLength": 20 + "enum": [ + "en-US", + "de-DE", + "es-ES", + "fr-FR", + "it-IT", + "ja-JP", + "ko-KR", + "pt-BR", + "ru-RU", + "zh-CN", + "zh-TW" + ] } } } diff --git a/src/AppInstallerCLICore/Core.cpp b/src/AppInstallerCLICore/Core.cpp index 8d43dccd37..3b65b87857 100644 --- a/src/AppInstallerCLICore/Core.cpp +++ b/src/AppInstallerCLICore/Core.cpp @@ -80,16 +80,22 @@ namespace AppInstaller::CLI ShutdownMonitoring::ServerShutdownSynchronization::AddComponent(main); } - std::optional ApplyOutputLocaleOverride() + std::string ApplyOutputLocaleOverride() { - std::string localePreference = Settings::User().Get(); - if (!localePreference.empty()) + std::string localeTag{ Settings::OutputLocaleToString(Settings::User().Get()) }; + + try + { + // Always apply the setting value, including empty string, to clear any prior override. + winrt::Windows::Globalization::ApplicationLanguages::PrimaryLanguageOverride(Utility::ConvertToUTF16(localeTag)); + } + catch (const winrt::hresult_error& hre) { - winrt::Windows::Globalization::ApplicationLanguages::PrimaryLanguageOverride(Utility::ConvertToUTF16(localePreference)); - return localePreference; + AICLI_LOG(CLI, Warning, << "Failed to apply output locale override for " << localeTag << ". HRESULT: 0x" << Logging::SetHRFormat << hre.code()); + return {}; } - return {}; + return localeTag; } } @@ -132,10 +138,10 @@ namespace AppInstaller::CLI Logging::OutputDebugStringLogger::Remove(); Logging::EnableWilFailureTelemetry(); - std::optional outputLocaleOverride = ApplyOutputLocaleOverride(); - if (outputLocaleOverride) + std::string outputLocaleOverride = ApplyOutputLocaleOverride(); + if (outputLocaleOverride != Settings::OutputLocaleToString(Settings::OutputLocale::Unset)) { - AICLI_LOG(CLI, Info, << "Applied output locale override from settings: " << outputLocaleOverride.value()); + AICLI_LOG(CLI, Info, << "Applied output locale override from settings: " << outputLocaleOverride); } // Set output to UTF8 diff --git a/src/AppInstallerCLITests/UserSettings.cpp b/src/AppInstallerCLITests/UserSettings.cpp index d167aecf51..4218f9b1c1 100644 --- a/src/AppInstallerCLITests/UserSettings.cpp +++ b/src/AppInstallerCLITests/UserSettings.cpp @@ -933,7 +933,7 @@ TEST_CASE("SettingOutputLocale", "[settings]") { UserSettingsTest userSettingTest; - REQUIRE(userSettingTest.Get().empty()); + REQUIRE(userSettingTest.Get() == OutputLocale::Unset); REQUIRE(userSettingTest.GetWarnings().size() == 0); } SECTION("Valid locale") @@ -942,25 +942,34 @@ TEST_CASE("SettingOutputLocale", "[settings]") SetSetting(Stream::PrimaryUserSettings, json); UserSettingsTest userSettingTest; - REQUIRE(userSettingTest.Get() == "en-US"); + REQUIRE(userSettingTest.Get() == OutputLocale::EnUS); REQUIRE(userSettingTest.GetWarnings().size() == 0); } - SECTION("Valid but unsupported locale") + SECTION("Case insensitive locale") { - std::string_view json = R"({ "output": { "locale": "el-GR" } })"; + std::string_view json = R"({ "output": { "locale": "EN-us" } })"; SetSetting(Stream::PrimaryUserSettings, json); UserSettingsTest userSettingTest; - REQUIRE(userSettingTest.Get() == "el-GR"); + REQUIRE(userSettingTest.Get() == OutputLocale::EnUS); REQUIRE(userSettingTest.GetWarnings().size() == 0); } + SECTION("Unsupported locale") + { + std::string_view json = R"({ "output": { "locale": "el-GR" } })"; + SetSetting(Stream::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get() == OutputLocale::Unset); + REQUIRE(userSettingTest.GetWarnings().size() == 1); + } SECTION("Invalid locale") { std::string_view json = R"({ "output": { "locale": "en_US.UTF-8" } })"; SetSetting(Stream::PrimaryUserSettings, json); UserSettingsTest userSettingTest; - REQUIRE(userSettingTest.Get().empty()); + REQUIRE(userSettingTest.Get() == OutputLocale::Unset); REQUIRE(userSettingTest.GetWarnings().size() == 1); } SECTION("Wrong type") @@ -969,7 +978,7 @@ TEST_CASE("SettingOutputLocale", "[settings]") SetSetting(Stream::PrimaryUserSettings, json); UserSettingsTest userSettingTest; - REQUIRE(userSettingTest.Get().empty()); + REQUIRE(userSettingTest.Get() == OutputLocale::Unset); REQUIRE(userSettingTest.GetWarnings().size() == 1); } } diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index 510e98aff0..3f997e9eb0 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -66,6 +66,26 @@ namespace AppInstaller::Settings // Converts a string to SortField. Returns std::nullopt for unrecognized values. std::optional ConvertToSortField(std::string_view value); + // Supported output locale overrides. + enum class OutputLocale + { + Unset, + EnUS, + DeDE, + EsES, + FrFR, + ItIT, + JaJP, + KoKR, + PtBR, + RuRU, + ZhCN, + ZhTW, + }; + + // Converts OutputLocale to its locale tag value. Returns empty for OutputLocale::Unset. + std::string_view OutputLocaleToString(OutputLocale locale); + // Sort direction for output ordering. enum class SortDirection { @@ -243,7 +263,7 @@ namespace AppInstaller::Settings // Output behavior SETTINGMAPPING_SPECIALIZATION(Setting::OutputSortOrder, std::vector, std::vector, std::vector{}, ".output.sortOrder"sv); SETTINGMAPPING_SPECIALIZATION(Setting::OutputSortDirection, std::string, SortDirection, SortDirection::Ascending, ".output.sortDirection"sv); - SETTINGMAPPING_SPECIALIZATION(Setting::OutputLocale, std::string, std::string, {}, ".output.locale"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::OutputLocale, std::string, OutputLocale, OutputLocale::Unset, ".output.locale"sv); // Used to deduce the SettingVariant type; making a variant that includes std::monostate and all SettingMapping types. template diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index f0a66c02c3..fea721e5b1 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -235,6 +235,39 @@ namespace AppInstaller::Settings return std::nullopt; } + std::string_view OutputLocaleToString(OutputLocale locale) + { + switch (locale) + { + case OutputLocale::EnUS: + return "en-US"sv; + case OutputLocale::DeDE: + return "de-DE"sv; + case OutputLocale::EsES: + return "es-ES"sv; + case OutputLocale::FrFR: + return "fr-FR"sv; + case OutputLocale::ItIT: + return "it-IT"sv; + case OutputLocale::JaJP: + return "ja-JP"sv; + case OutputLocale::KoKR: + return "ko-KR"sv; + case OutputLocale::PtBR: + return "pt-BR"sv; + case OutputLocale::RuRU: + return "ru-RU"sv; + case OutputLocale::ZhCN: + return "zh-CN"sv; + case OutputLocale::ZhTW: + return "zh-TW"sv; + case OutputLocale::Unset: + return {}; + } + + return {}; + } + namespace details { #define WINGET_VALIDATE_SIGNATURE(_setting_) \ @@ -564,11 +597,19 @@ namespace AppInstaller::Settings WINGET_VALIDATE_SIGNATURE(OutputLocale) { - if (Locale::IsWellFormedBcp47Tag(value)) - { - return value; - } - + std::string lowered = Utility::ToLower(value); + + if (lowered == "en-us"sv) return OutputLocale::EnUS; + if (lowered == "de-de"sv) return OutputLocale::DeDE; + if (lowered == "es-es"sv) return OutputLocale::EsES; + if (lowered == "fr-fr"sv) return OutputLocale::FrFR; + if (lowered == "it-it"sv) return OutputLocale::ItIT; + if (lowered == "ja-jp"sv) return OutputLocale::JaJP; + if (lowered == "ko-kr"sv) return OutputLocale::KoKR; + if (lowered == "pt-br"sv) return OutputLocale::PtBR; + if (lowered == "ru-ru"sv) return OutputLocale::RuRU; + if (lowered == "zh-cn"sv) return OutputLocale::ZhCN; + if (lowered == "zh-tw"sv) return OutputLocale::ZhTW; return {}; } } From 4933fcb1ee20b11ffde2dfb856f1bbcc23b964ff Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Wed, 1 Jul 2026 08:02:13 -0500 Subject: [PATCH 7/8] Use strings directly instead of enum roundtripping --- src/AppInstallerCLICore/Core.cpp | 7 +- src/AppInstallerCLITests/UserSettings.cpp | 12 +-- .../Public/winget/UserSettings.h | 22 +----- src/AppInstallerCommonCore/UserSettings.cpp | 75 +++++++++---------- 4 files changed, 45 insertions(+), 71 deletions(-) diff --git a/src/AppInstallerCLICore/Core.cpp b/src/AppInstallerCLICore/Core.cpp index 3b65b87857..bc29971540 100644 --- a/src/AppInstallerCLICore/Core.cpp +++ b/src/AppInstallerCLICore/Core.cpp @@ -82,11 +82,12 @@ namespace AppInstaller::CLI std::string ApplyOutputLocaleOverride() { - std::string localeTag{ Settings::OutputLocaleToString(Settings::User().Get()) }; + std::string localeTag{ Settings::User().Get() }; try { - // Always apply the setting value, including empty string, to clear any prior override. + // Always apply the setting value, including empty string, to clear a persisted override from + // previous sessions. PrimaryLanguageOverride is package-scoped and persists across sessions. winrt::Windows::Globalization::ApplicationLanguages::PrimaryLanguageOverride(Utility::ConvertToUTF16(localeTag)); } catch (const winrt::hresult_error& hre) @@ -139,7 +140,7 @@ namespace AppInstaller::CLI Logging::EnableWilFailureTelemetry(); std::string outputLocaleOverride = ApplyOutputLocaleOverride(); - if (outputLocaleOverride != Settings::OutputLocaleToString(Settings::OutputLocale::Unset)) + if (!outputLocaleOverride.empty()) { AICLI_LOG(CLI, Info, << "Applied output locale override from settings: " << outputLocaleOverride); } diff --git a/src/AppInstallerCLITests/UserSettings.cpp b/src/AppInstallerCLITests/UserSettings.cpp index 4218f9b1c1..8f9e31e150 100644 --- a/src/AppInstallerCLITests/UserSettings.cpp +++ b/src/AppInstallerCLITests/UserSettings.cpp @@ -933,7 +933,7 @@ TEST_CASE("SettingOutputLocale", "[settings]") { UserSettingsTest userSettingTest; - REQUIRE(userSettingTest.Get() == OutputLocale::Unset); + REQUIRE(userSettingTest.Get().empty()); REQUIRE(userSettingTest.GetWarnings().size() == 0); } SECTION("Valid locale") @@ -942,7 +942,7 @@ TEST_CASE("SettingOutputLocale", "[settings]") SetSetting(Stream::PrimaryUserSettings, json); UserSettingsTest userSettingTest; - REQUIRE(userSettingTest.Get() == OutputLocale::EnUS); + REQUIRE(userSettingTest.Get() == "en-US"sv); REQUIRE(userSettingTest.GetWarnings().size() == 0); } SECTION("Case insensitive locale") @@ -951,7 +951,7 @@ TEST_CASE("SettingOutputLocale", "[settings]") SetSetting(Stream::PrimaryUserSettings, json); UserSettingsTest userSettingTest; - REQUIRE(userSettingTest.Get() == OutputLocale::EnUS); + REQUIRE(userSettingTest.Get() == "en-US"sv); REQUIRE(userSettingTest.GetWarnings().size() == 0); } SECTION("Unsupported locale") @@ -960,7 +960,7 @@ TEST_CASE("SettingOutputLocale", "[settings]") SetSetting(Stream::PrimaryUserSettings, json); UserSettingsTest userSettingTest; - REQUIRE(userSettingTest.Get() == OutputLocale::Unset); + REQUIRE(userSettingTest.Get().empty()); REQUIRE(userSettingTest.GetWarnings().size() == 1); } SECTION("Invalid locale") @@ -969,7 +969,7 @@ TEST_CASE("SettingOutputLocale", "[settings]") SetSetting(Stream::PrimaryUserSettings, json); UserSettingsTest userSettingTest; - REQUIRE(userSettingTest.Get() == OutputLocale::Unset); + REQUIRE(userSettingTest.Get().empty()); REQUIRE(userSettingTest.GetWarnings().size() == 1); } SECTION("Wrong type") @@ -978,7 +978,7 @@ TEST_CASE("SettingOutputLocale", "[settings]") SetSetting(Stream::PrimaryUserSettings, json); UserSettingsTest userSettingTest; - REQUIRE(userSettingTest.Get() == OutputLocale::Unset); + REQUIRE(userSettingTest.Get().empty()); REQUIRE(userSettingTest.GetWarnings().size() == 1); } } diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index 3f997e9eb0..52fcf2c46e 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -66,26 +66,6 @@ namespace AppInstaller::Settings // Converts a string to SortField. Returns std::nullopt for unrecognized values. std::optional ConvertToSortField(std::string_view value); - // Supported output locale overrides. - enum class OutputLocale - { - Unset, - EnUS, - DeDE, - EsES, - FrFR, - ItIT, - JaJP, - KoKR, - PtBR, - RuRU, - ZhCN, - ZhTW, - }; - - // Converts OutputLocale to its locale tag value. Returns empty for OutputLocale::Unset. - std::string_view OutputLocaleToString(OutputLocale locale); - // Sort direction for output ordering. enum class SortDirection { @@ -263,7 +243,7 @@ namespace AppInstaller::Settings // Output behavior SETTINGMAPPING_SPECIALIZATION(Setting::OutputSortOrder, std::vector, std::vector, std::vector{}, ".output.sortOrder"sv); SETTINGMAPPING_SPECIALIZATION(Setting::OutputSortDirection, std::string, SortDirection, SortDirection::Ascending, ".output.sortDirection"sv); - SETTINGMAPPING_SPECIALIZATION(Setting::OutputLocale, std::string, OutputLocale, OutputLocale::Unset, ".output.locale"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::OutputLocale, std::string, std::string, std::string{}, ".output.locale"sv); // Used to deduce the SettingVariant type; making a variant that includes std::monostate and all SettingMapping types. template diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index fea721e5b1..96c2b4646c 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -12,6 +12,8 @@ #include "AppInstallerArchitecture.h" #include "winget/Locale.h" +#include + namespace AppInstaller::Settings { using namespace std::string_view_literals; @@ -235,37 +237,35 @@ namespace AppInstaller::Settings return std::nullopt; } - std::string_view OutputLocaleToString(OutputLocale locale) + namespace { - switch (locale) - { - case OutputLocale::EnUS: - return "en-US"sv; - case OutputLocale::DeDE: - return "de-DE"sv; - case OutputLocale::EsES: - return "es-ES"sv; - case OutputLocale::FrFR: - return "fr-FR"sv; - case OutputLocale::ItIT: - return "it-IT"sv; - case OutputLocale::JaJP: - return "ja-JP"sv; - case OutputLocale::KoKR: - return "ko-KR"sv; - case OutputLocale::PtBR: - return "pt-BR"sv; - case OutputLocale::RuRU: - return "ru-RU"sv; - case OutputLocale::ZhCN: - return "zh-CN"sv; - case OutputLocale::ZhTW: - return "zh-TW"sv; - case OutputLocale::Unset: + static constexpr std::array s_supportedOutputLocales = + { + "en-US"sv, + "de-DE"sv, + "es-ES"sv, + "fr-FR"sv, + "it-IT"sv, + "ja-JP"sv, + "ko-KR"sv, + "pt-BR"sv, + "ru-RU"sv, + "zh-CN"sv, + "zh-TW"sv, + }; + + std::optional NormalizeSupportedLocale(std::string_view localeTag) + { + for (const auto& supportedLocale : s_supportedOutputLocales) + { + if (Utility::CaseInsensitiveEquals(localeTag, supportedLocale)) + { + return supportedLocale; + } + } + return {}; } - - return {}; } namespace details @@ -597,19 +597,12 @@ namespace AppInstaller::Settings WINGET_VALIDATE_SIGNATURE(OutputLocale) { - std::string lowered = Utility::ToLower(value); - - if (lowered == "en-us"sv) return OutputLocale::EnUS; - if (lowered == "de-de"sv) return OutputLocale::DeDE; - if (lowered == "es-es"sv) return OutputLocale::EsES; - if (lowered == "fr-fr"sv) return OutputLocale::FrFR; - if (lowered == "it-it"sv) return OutputLocale::ItIT; - if (lowered == "ja-jp"sv) return OutputLocale::JaJP; - if (lowered == "ko-kr"sv) return OutputLocale::KoKR; - if (lowered == "pt-br"sv) return OutputLocale::PtBR; - if (lowered == "ru-ru"sv) return OutputLocale::RuRU; - if (lowered == "zh-cn"sv) return OutputLocale::ZhCN; - if (lowered == "zh-tw"sv) return OutputLocale::ZhTW; + auto normalizedLocale = NormalizeSupportedLocale(value); + if (normalizedLocale) + { + return std::string{ normalizedLocale.value() }; + } + return {}; } } From 6f7c3b4ad11207ecd3d2e26ca3c3f317ae4709fa Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Wed, 1 Jul 2026 22:59:49 -0500 Subject: [PATCH 8/8] Merge anonymous namespaces --- src/AppInstallerCommonCore/UserSettings.cpp | 43 ++++++++++----------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index 96c2b4646c..876cb4467d 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -215,30 +215,7 @@ namespace AppInstaller::Settings return path; } - } - - std::optional ConvertToSortField(std::string_view value) - { - static constexpr std::string_view s_sortField_relevance = "relevance"; - static constexpr std::string_view s_sortField_name = "name"; - static constexpr std::string_view s_sortField_id = "id"; - static constexpr std::string_view s_sortField_version = "version"; - static constexpr std::string_view s_sortField_source = "source"; - static constexpr std::string_view s_sortField_available = "available"; - - std::string lowered = Utility::ToLower(value); - if (lowered == s_sortField_relevance) return SortField::Relevance; - if (lowered == s_sortField_name) return SortField::Name; - if (lowered == s_sortField_id) return SortField::Id; - if (lowered == s_sortField_version) return SortField::Version; - if (lowered == s_sortField_source) return SortField::Source; - if (lowered == s_sortField_available) return SortField::Available; - return std::nullopt; - } - - namespace - { static constexpr std::array s_supportedOutputLocales = { "en-US"sv, @@ -268,6 +245,26 @@ namespace AppInstaller::Settings } } + std::optional ConvertToSortField(std::string_view value) + { + static constexpr std::string_view s_sortField_relevance = "relevance"; + static constexpr std::string_view s_sortField_name = "name"; + static constexpr std::string_view s_sortField_id = "id"; + static constexpr std::string_view s_sortField_version = "version"; + static constexpr std::string_view s_sortField_source = "source"; + static constexpr std::string_view s_sortField_available = "available"; + + std::string lowered = Utility::ToLower(value); + + if (lowered == s_sortField_relevance) return SortField::Relevance; + if (lowered == s_sortField_name) return SortField::Name; + if (lowered == s_sortField_id) return SortField::Id; + if (lowered == s_sortField_version) return SortField::Version; + if (lowered == s_sortField_source) return SortField::Source; + if (lowered == s_sortField_available) return SortField::Available; + return std::nullopt; + } + namespace details { #define WINGET_VALIDATE_SIGNATURE(_setting_) \