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