Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion doc/ReleaseNotes.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
17 changes: 17 additions & 0 deletions doc/Settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 19 additions & 1 deletion doc/windows/package-manager/winget/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
Trenly marked this conversation as resolved.

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.
Expand Down Expand Up @@ -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.

17 changes: 17 additions & 0 deletions schemas/JSON/settings/settings.schema.0.2.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
}
}
Expand Down
27 changes: 26 additions & 1 deletion src/AppInstallerCLICore/Core.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "COMContext.h"
#include <AppInstallerFileLogger.h>
#include <winget/OutputDebugStringLogger.h>
#include <winrt/Windows.Globalization.h>
#include "Public/ShutdownMonitoring.h"

#ifndef AICLI_DISABLE_TEST_HOOKS
Expand Down Expand Up @@ -78,6 +79,25 @@ namespace AppInstaller::CLI
main.Wait = WaitOnMainWaitEvent;
ShutdownMonitoring::ServerShutdownSynchronization::AddComponent(main);
}

std::string ApplyOutputLocaleOverride()
{
std::string localeTag{ Settings::User().Get<Settings::Setting::OutputLocale>() };

try
{
// 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)
{
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
Expand All @@ -89,7 +109,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();
Expand Down Expand Up @@ -120,6 +139,12 @@ namespace AppInstaller::CLI
Logging::OutputDebugStringLogger::Remove();
Logging::EnableWilFailureTelemetry();

std::string outputLocaleOverride = ApplyOutputLocaleOverride();
if (!outputLocaleOverride.empty())
{
AICLI_LOG(CLI, Info, << "Applied output locale override from settings: " << outputLocaleOverride);
}

// Set output to UTF8
ConsoleOutputCPRestore utf8CP(CP_UTF8);

Expand Down
10 changes: 10 additions & 0 deletions src/AppInstallerCLIPackage/AppInstallerCLIPackage.wapproj
Comment thread
Trenly marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -187,15 +187,25 @@
<SubType>Designer</SubType>
</PRIResource>
<PRIResource Include="shared\strings\de-DE\winget.resw" Condition="Exists('shared\strings\de-DE\winget.resw')" />
<PRIResource Include="..\..\Localization\Resources\de-DE\winget.resw" Condition="!Exists('shared\strings\de-DE\winget.resw') and Exists('..\..\Localization\Resources\de-DE\winget.resw')" />
<PRIResource Include="shared\strings\es-ES\winget.resw" Condition="Exists('shared\strings\es-ES\winget.resw')" />
<PRIResource Include="..\..\Localization\Resources\es-ES\winget.resw" Condition="!Exists('shared\strings\es-ES\winget.resw') and Exists('..\..\Localization\Resources\es-ES\winget.resw')" />
<PRIResource Include="shared\strings\fr-FR\winget.resw" Condition="Exists('shared\strings\fr-FR\winget.resw')" />
<PRIResource Include="..\..\Localization\Resources\fr-FR\winget.resw" Condition="!Exists('shared\strings\fr-FR\winget.resw') and Exists('..\..\Localization\Resources\fr-FR\winget.resw')" />
<PRIResource Include="shared\strings\it-IT\winget.resw" Condition="Exists('shared\strings\it-IT\winget.resw')" />
<PRIResource Include="..\..\Localization\Resources\it-IT\winget.resw" Condition="!Exists('shared\strings\it-IT\winget.resw') and Exists('..\..\Localization\Resources\it-IT\winget.resw')" />
<PRIResource Include="shared\strings\ja-JP\winget.resw" Condition="Exists('shared\strings\ja-JP\winget.resw')" />
<PRIResource Include="..\..\Localization\Resources\ja-JP\winget.resw" Condition="!Exists('shared\strings\ja-JP\winget.resw') and Exists('..\..\Localization\Resources\ja-JP\winget.resw')" />
<PRIResource Include="shared\strings\ko-KR\winget.resw" Condition="Exists('shared\strings\ko-KR\winget.resw')" />
<PRIResource Include="..\..\Localization\Resources\ko-KR\winget.resw" Condition="!Exists('shared\strings\ko-KR\winget.resw') and Exists('..\..\Localization\Resources\ko-KR\winget.resw')" />
<PRIResource Include="shared\strings\pt-BR\winget.resw" Condition="Exists('shared\strings\pt-BR\winget.resw')" />
<PRIResource Include="..\..\Localization\Resources\pt-BR\winget.resw" Condition="!Exists('shared\strings\pt-BR\winget.resw') and Exists('..\..\Localization\Resources\pt-BR\winget.resw')" />
<PRIResource Include="shared\strings\ru-RU\winget.resw" Condition="Exists('shared\strings\ru-RU\winget.resw')" />
<PRIResource Include="..\..\Localization\Resources\ru-RU\winget.resw" Condition="!Exists('shared\strings\ru-RU\winget.resw') and Exists('..\..\Localization\Resources\ru-RU\winget.resw')" />
<PRIResource Include="shared\strings\zh-CN\winget.resw" Condition="Exists('shared\strings\zh-CN\winget.resw')" />
<PRIResource Include="..\..\Localization\Resources\zh-CN\winget.resw" Condition="!Exists('shared\strings\zh-CN\winget.resw') and Exists('..\..\Localization\Resources\zh-CN\winget.resw')" />
<PRIResource Include="shared\strings\zh-TW\winget.resw" Condition="Exists('shared\strings\zh-TW\winget.resw')" />
<PRIResource Include="..\..\Localization\Resources\zh-TW\winget.resw" Condition="!Exists('shared\strings\zh-TW\winget.resw') and Exists('..\..\Localization\Resources\zh-TW\winget.resw')" />
<PRIResource Include="Shared\Strings\en-us\Resources.resw" />
</ItemGroup>
<ItemGroup>
Expand Down
58 changes: 58 additions & 0 deletions src/AppInstallerCLITests/UserSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,64 @@ TEST_CASE("SettingOutputSortDirection", "[settings]")
}
}

TEST_CASE("SettingOutputLocale", "[settings]")
Comment thread
Trenly marked this conversation as resolved.
{
auto again = DeleteUserSettingsFiles();

SECTION("Default value")
{
UserSettingsTest userSettingTest;

REQUIRE(userSettingTest.Get<Setting::OutputLocale>().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<Setting::OutputLocale>() == "en-US"sv);
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<Setting::OutputLocale>() == "en-US"sv);
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<Setting::OutputLocale>().empty());
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<Setting::OutputLocale>().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<Setting::OutputLocale>().empty());
REQUIRE(userSettingTest.GetWarnings().size() == 1);
}
}

TEST_CASE("ConvertToSortField", "[settings]")
{
SECTION("Valid values - lowercase")
Expand Down
2 changes: 2 additions & 0 deletions src/AppInstallerCommonCore/Public/winget/UserSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ namespace AppInstaller::Settings
// Output behavior
OutputSortOrder,
OutputSortDirection,
OutputLocale,
#ifndef AICLI_DISABLE_TEST_HOOKS
// Debug
EnableSelfInitiatedMinidump,
Expand Down Expand Up @@ -242,6 +243,7 @@ namespace AppInstaller::Settings
// Output behavior
SETTINGMAPPING_SPECIALIZATION(Setting::OutputSortOrder, std::vector<std::string>, std::vector<SortField>, std::vector<SortField>{}, ".output.sortOrder"sv);
SETTINGMAPPING_SPECIALIZATION(Setting::OutputSortDirection, std::string, SortDirection, SortDirection::Ascending, ".output.sortDirection"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 <size_t... I>
Expand Down
41 changes: 41 additions & 0 deletions src/AppInstallerCommonCore/UserSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
#include "AppInstallerArchitecture.h"
#include "winget/Locale.h"

#include <array>

namespace AppInstaller::Settings
{
using namespace std::string_view_literals;
Expand Down Expand Up @@ -213,6 +215,34 @@ namespace AppInstaller::Settings

return path;
}

static constexpr std::array<std::string_view, 11> 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<std::string_view> NormalizeSupportedLocale(std::string_view localeTag)
{
for (const auto& supportedLocale : s_supportedOutputLocales)
{
if (Utility::CaseInsensitiveEquals(localeTag, supportedLocale))
{
return supportedLocale;
}
}

return {};
}
}

std::optional<SortField> ConvertToSortField(std::string_view value)
Expand Down Expand Up @@ -561,6 +591,17 @@ namespace AppInstaller::Settings

return {};
}

WINGET_VALIDATE_SIGNATURE(OutputLocale)
{
auto normalizedLocale = NormalizeSupportedLocale(value);
if (normalizedLocale)
{
return std::string{ normalizedLocale.value() };
}

return {};
}
}

#ifndef AICLI_DISABLE_TEST_HOOKS
Expand Down
Loading