diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index 28f90d9b5d..1229e28f97 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..789a66013f 100644 --- a/doc/windows/package-manager/winget/settings.md +++ b/doc/windows/package-manager/winget/settings.md @@ -98,6 +98,25 @@ 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 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. + +```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 +152,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..c47e291aec 100644 --- a/schemas/JSON/settings/settings.schema.0.2.json +++ b/schemas/JSON/settings/settings.schema.0.2.json @@ -378,6 +378,23 @@ "descending" ], "default": "ascending" + }, + "locale": { + "description": "Overrides winget interface output language using a supported locale", + "type": "string", + "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 a5890bd9cf..3b65b87857 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,24 @@ namespace AppInstaller::CLI main.Wait = WaitOnMainWaitEvent; ShutdownMonitoring::ServerShutdownSynchronization::AddComponent(main); } + + std::string ApplyOutputLocaleOverride() + { + 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) + { + AICLI_LOG(CLI, Warning, << "Failed to apply output locale override for " << localeTag << ". HRESULT: 0x" << Logging::SetHRFormat << hre.code()); + return {}; + } + + return localeTag; + } } int CoreMain(int argc, wchar_t const** argv) try @@ -89,7 +108,6 @@ namespace AppInstaller::CLI std::signal(SIGABRT, abort_signal_handler); init_apartment(); - #ifndef AICLI_DISABLE_TEST_HOOKS // We have to do this here so the auto minidump config initialization gets caught Logging::OutputDebugStringLogger::Add(); @@ -120,6 +138,12 @@ namespace AppInstaller::CLI Logging::OutputDebugStringLogger::Remove(); Logging::EnableWilFailureTelemetry(); + std::string outputLocaleOverride = ApplyOutputLocaleOverride(); + if (outputLocaleOverride != Settings::OutputLocaleToString(Settings::OutputLocale::Unset)) + { + AICLI_LOG(CLI, Info, << "Applied output locale override from settings: " << outputLocaleOverride); + } + // Set output to UTF8 ConsoleOutputCPRestore utf8CP(CP_UTF8); 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..4218f9b1c1 100644 --- a/src/AppInstallerCLITests/UserSettings.cpp +++ b/src/AppInstallerCLITests/UserSettings.cpp @@ -925,6 +925,64 @@ TEST_CASE("SettingOutputSortDirection", "[settings]") } } +TEST_CASE("SettingOutputLocale", "[settings]") +{ + auto again = DeleteUserSettingsFiles(); + + SECTION("Default value") + { + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get() == OutputLocale::Unset); + 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() == OutputLocale::EnUS); + REQUIRE(userSettingTest.GetWarnings().size() == 0); + } + SECTION("Case insensitive locale") + { + std::string_view json = R"({ "output": { "locale": "EN-us" } })"; + SetSetting(Stream::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + 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() == OutputLocale::Unset); + 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() == OutputLocale::Unset); + 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..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 { @@ -144,6 +164,7 @@ namespace AppInstaller::Settings // Output behavior OutputSortOrder, OutputSortDirection, + OutputLocale, #ifndef AICLI_DISABLE_TEST_HOOKS // Debug EnableSelfInitiatedMinidump, @@ -242,6 +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, 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 a7f9bb3b93..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_) \ @@ -561,6 +594,24 @@ namespace AppInstaller::Settings return {}; } + + 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; + return {}; + } } #ifndef AICLI_DISABLE_TEST_HOOKS