diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt index 3452bf4a8..fcaaf797a 100644 --- a/.github/actions/spelling/allow/allow.txt +++ b/.github/actions/spelling/allow/allow.txt @@ -12,6 +12,7 @@ downsides dze dzhe Enum'd +formattings ftp geeksforgeeks ghe @@ -34,6 +35,7 @@ overlined postmodern ptys qof +qps reimplementation reserialization reserialize @@ -46,6 +48,7 @@ tokenizes tonos tshe UIs +und versioned We'd wildcards diff --git a/.vscode/settings.json b/.vscode/settings.json index 2b746e3d1..142fa20f6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -100,6 +100,6 @@ "**/bin/**": true, "**/obj/**": true, "**/packages/**": true, - "**/generated files/**": true + "**/Generated Files/**": true } } \ No newline at end of file diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index c17f5662e..66e914fa9 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -1109,6 +1109,11 @@ }, "type": "array" }, + "language": { + "default": "", + "description": "Sets an override for the app's preferred language, expressed as a BCP-47 language tag like en-US.", + "type": "string" + }, "theme": { "default": "system", "description": "Sets the theme of the application. The special value \"system\" refers to the active Windows system theme.", diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index 449d19b6b..52ab7e0a1 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -306,6 +306,7 @@ namespace winrt::TerminalApp::implementation }); _root->Create(); + _ApplyLanguageSettingChange(); _ApplyTheme(_settings.GlobalSettings().Theme()); _ApplyStartupTaskStateChange(); @@ -904,6 +905,19 @@ namespace winrt::TerminalApp::implementation } } + void AppLogic::_ApplyLanguageSettingChange() + { + using ApplicationLanguages = winrt::Windows::Globalization::ApplicationLanguages; + + const auto language = _settings.GlobalSettings().Language(); + const auto primaryLanguageOverride = ApplicationLanguages::PrimaryLanguageOverride(); + + if (primaryLanguageOverride != language) + { + ApplicationLanguages::PrimaryLanguageOverride(language); + } + } + fire_and_forget AppLogic::_LoadErrorsDialogRoutine() { co_await winrt::resume_foreground(_root->Dispatcher()); @@ -1023,6 +1037,7 @@ namespace winrt::TerminalApp::implementation // Update the settings in TerminalPage _root->SetSettings(_settings, true); + _ApplyLanguageSettingChange(); _RefreshThemeRoutine(); _ApplyStartupTaskStateChange(); diff --git a/src/cascadia/TerminalApp/AppLogic.h b/src/cascadia/TerminalApp/AppLogic.h index 50b7b95f0..5a046242d 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -133,6 +133,7 @@ namespace winrt::TerminalApp::implementation bool _IsKeyboardServiceEnabled(); void _ShowKeyboardServiceDisabledDialog(); + void _ApplyLanguageSettingChange(); fire_and_forget _LoadErrorsDialogRoutine(); fire_and_forget _ShowLoadWarningsDialogRoutine(); fire_and_forget _RefreshThemeRoutine(); diff --git a/src/cascadia/TerminalApp/pch.h b/src/cascadia/TerminalApp/pch.h index f2c566352..299d7de63 100644 --- a/src/cascadia/TerminalApp/pch.h +++ b/src/cascadia/TerminalApp/pch.h @@ -25,40 +25,39 @@ #include -#include - -#include - +#include +#include #include #include #include +#include #include -#include -#include +#include +#include +#include #include +#include +#include #include #include -#include -#include +#include +#include +#include +#include #include -#include -#include -#include "winrt/Windows.UI.Xaml.Markup.h" -#include "winrt/Windows.UI.Xaml.Documents.h" -#include "winrt/Windows.UI.Xaml.Automation.h" -#include "winrt/Windows.UI.Xaml.Automation.Peers.h" -#include "winrt/Windows.UI.ViewManagement.h" -#include -#include #include #include #include #include -#include +#include +#include +#include +#include +#include -#include +#include // Including TraceLogging essentials for the binary #include @@ -70,14 +69,6 @@ TRACELOGGING_DECLARE_PROVIDER(g_hTerminalAppProvider); #include #include -#include -#include -#include -#include -#include - -#include - #include // Manually include til after we include Windows.Foundation to give it winrt superpowers diff --git a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.cpp b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.cpp index 7556dc067..1c53db7dd 100644 --- a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.cpp +++ b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.cpp @@ -2,10 +2,12 @@ // Licensed under the MIT license. #include "pch.h" +#include "EnumEntry.h" #include "GlobalAppearance.h" #include "GlobalAppearance.g.cpp" #include "GlobalAppearancePageNavigationState.g.cpp" -#include "EnumEntry.h" + +#include using namespace winrt; using namespace winrt::Windows::UI::Xaml; @@ -16,6 +18,11 @@ using namespace winrt::Windows::Foundation::Collections; namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { + // For ComboBox an empty SelectedItem string denotes no selection. + // What we want instead is for "Use system language" to be selected by default. + // --> "und" is synonymous for "Use system language". + constexpr std::wstring_view systemLanguageTag{ L"und" }; + GlobalAppearance::GlobalAppearance() { InitializeComponent(); @@ -28,4 +35,139 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { _State = e.Parameter().as(); } + + winrt::hstring GlobalAppearance::LanguageDisplayConverter(const winrt::hstring& tag) + { + if (tag == systemLanguageTag) + { + return RS_(L"Globals_LanguageDefault"); + } + + winrt::Windows::Globalization::Language language{ tag }; + return language.NativeName(); + } + + // Returns the list of languages the user may override the application language with. + // The returned list are BCP 47 language tags like {"und", "en-US", "de-DE", "es-ES", ...}. + // "und" is short for "undefined" and is synonymous for "Use system language" in this code. + winrt::Windows::Foundation::Collections::IObservableVector GlobalAppearance::LanguageList() + { + if (_languageList) + { + return _languageList; + } + + // In order to return the language list this code does the following: + // [1] Get all possible languages we want to allow the user to choose. + // We have to acquire languages from multiple sources, creating duplicates. See below at [1]. + // [2] Sort languages by their ASCII tags, forcing the UI in a consistent/stable order. + // I wanted to sort the localized language names initially, but it turned out to be complex. + // [3] Remove potential duplicates in our language list from [1]. + // We don't want to have en-US twice in the list, do we? + // [4] Optionally remove unwanted language tags (like pseudo-localizations). + + std::vector tags; + + // [1]: + { + // ManifestLanguages contains languages the app ships with. + // + // Languages is a computed list that merges the ManifestLanguages with the + // user's ranked list of preferred languages taken from the system settings. + // As is tradition the API documentation is incomplete though, as it can also + // contain regional language variants. If our app supports en-US, but the user + // has en-GB or en-DE in their system's preferred language list, Languages will + // contain those as well, as they're variants from a supported language. We should + // allow a user to select those, as regional formattings can vary significantly. + const std::array tagSources{ + winrt::Windows::Globalization::ApplicationLanguages::ManifestLanguages(), + winrt::Windows::Globalization::ApplicationLanguages::Languages() + }; + + // tags will hold all the flattened results from tagSources. + // We resize() the vector to the proper size in order to efficiently GetMany() all items. + tags.resize(std::accumulate( + tagSources.begin(), + tagSources.end(), + // tags[0] will be "und" - the "Use system language" item + // tags[1..n] will contain tags from tagSources. + // --> totalTags is offset by 1 + 1, + [](uint32_t sum, const auto& v) -> uint32_t { + return sum + v.Size(); + })); + + // As per the function definition, the first item + // is always "Use system language" ("und"). + auto data = tags.data(); + *data++ = systemLanguageTag; + + // Finally GetMany() all the tags from tagSources. + for (const auto& v : tagSources) + { + const auto size = v.Size(); + v.GetMany(0, winrt::array_view(data, size)); + data += size; + } + } + + // NOTE: The size of tags is always >0, due to tags[0] being hardcoded to "und". + const auto tagsBegin = ++tags.begin(); + const auto tagsEnd = tags.end(); + + // [2]: + std::sort(tagsBegin, tagsEnd); + + // I'd love for both, std::unique and std::remove_if, to occur in a single loop, + // but the code turned out to be complex and even less maintainable, so I gave up. + { + // [3] part 1: + auto it = std::unique(tagsBegin, tagsEnd); + + // The qps- languages are useful for testing ("pseudo-localization"). + // --> Leave them in if debug features are enabled. + if (!_State.Globals().DebugFeaturesEnabled()) + { + // [4] part 1: + it = std::remove_if(tagsBegin, it, [](const winrt::hstring& tag) -> bool { + return til::starts_with(tag, L"qps-"); + }); + } + + // [3], [4] part 2 (completing the so called "erase-remove idiom"): + tags.erase(it, tagsEnd); + } + + _languageList = winrt::single_threaded_observable_vector(std::move(tags)); + return _languageList; + } + + winrt::Windows::Foundation::IInspectable GlobalAppearance::CurrentLanguage() + { + if (_currentLanguage.empty()) + { + _currentLanguage = winrt::Windows::Globalization::ApplicationLanguages::PrimaryLanguageOverride(); + if (_currentLanguage.empty()) + { + _currentLanguage = systemLanguageTag; + } + } + + return winrt::box_value(_currentLanguage); + } + + void GlobalAppearance::CurrentLanguage(const winrt::Windows::Foundation::IInspectable& tag) + { + _currentLanguage = winrt::unbox_value(tag); + + const auto globals = _State.Globals(); + if (_currentLanguage == systemLanguageTag) + { + globals.ClearLanguage(); + } + else + { + globals.Language(_currentLanguage); + } + } } diff --git a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.h b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.h index dddbf23f5..484fb1441 100644 --- a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.h +++ b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.h @@ -26,9 +26,24 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void OnNavigatedTo(const winrt::Windows::UI::Xaml::Navigation::NavigationEventArgs& e); WINRT_PROPERTY(Editor::GlobalAppearancePageNavigationState, State, nullptr); - GETSET_BINDABLE_ENUM_SETTING(Theme, winrt::Windows::UI::Xaml::ElementTheme, State().Globals, Theme); GETSET_BINDABLE_ENUM_SETTING(TabWidthMode, winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode, State().Globals, TabWidthMode); + + public: + // LanguageDisplayConverter maps the given BCP 47 tag to a localized string. + // For instance "en-US" produces "English (United States)", while "de-DE" produces + // "Deutsch (Deutschland)". This works independently of the user's locale. + static winrt::hstring LanguageDisplayConverter(const winrt::hstring& tag); + + winrt::Windows::Foundation::Collections::IObservableVector LanguageList(); + winrt::Windows::Foundation::IInspectable CurrentLanguage(); + void CurrentLanguage(const winrt::Windows::Foundation::IInspectable& tag); + + private: + std::vector _GetSupportedLanguageTags(); + + winrt::Windows::Foundation::Collections::IObservableVector _languageList{ nullptr }; + winrt::hstring _currentLanguage; }; } diff --git a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.idl b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.idl index 294f73022..8901b9934 100644 --- a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.idl +++ b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.idl @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import "EnumEntry.idl"; +import "EnumEntry.idl"; namespace Microsoft.Terminal.Settings.Editor { @@ -15,6 +15,10 @@ namespace Microsoft.Terminal.Settings.Editor GlobalAppearance(); GlobalAppearancePageNavigationState State { get; }; + static String LanguageDisplayConverter(String tag); + Windows.Foundation.Collections.IObservableVector LanguageList { get; }; + IInspectable CurrentLanguage; + IInspectable CurrentTheme; Windows.Foundation.Collections.IObservableVector ThemeList { get; }; diff --git a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml index 84c1408ae..7b33b45d0 100644 --- a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml +++ b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml @@ -28,9 +28,21 @@ - - + + + + + + + + + + + + diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index a42629596..495191792 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -215,6 +215,18 @@ Yellow This is the header for a control that lets the user select the yellow color for text displayed on the screen. + + Language + The header for a control allowing users to choose the app's language. + + + Sets an override for the app's preferred language. + A description explaining how this control changes the app's language. + + + Use system default + The app contains a control allowing users to choose the app's language. If this value is chosen, the language preference list of the system settings is used instead. + Always show tabs Header for a control to toggle if the app should always show the tabs (similar to a website browser). diff --git a/src/cascadia/TerminalSettingsEditor/pch.h b/src/cascadia/TerminalSettingsEditor/pch.h index df45f6bd2..4ee6261bf 100644 --- a/src/cascadia/TerminalSettingsEditor/pch.h +++ b/src/cascadia/TerminalSettingsEditor/pch.h @@ -20,21 +20,14 @@ #undef GetCurrentTime #endif -#include - -#include - -#include #include #include -#include -#include - +#include #include #include -#include #include #include +#include #include #include #include diff --git a/src/cascadia/TerminalSettingsModel/AppearanceConfig.h b/src/cascadia/TerminalSettingsModel/AppearanceConfig.h index d0b08e9e2..cdcdc9613 100644 --- a/src/cascadia/TerminalSettingsModel/AppearanceConfig.h +++ b/src/cascadia/TerminalSettingsModel/AppearanceConfig.h @@ -16,7 +16,6 @@ Author(s): #pragma once -#include "pch.h" #include "AppearanceConfig.g.h" #include "JsonUtils.h" #include "../inc/cppwinrt_utils.h" diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp index 39c370228..466031b05 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp @@ -26,6 +26,7 @@ static constexpr std::string_view InitialColsKey{ "initialCols" }; static constexpr std::string_view InitialPositionKey{ "initialPosition" }; static constexpr std::string_view CenterOnLaunchKey{ "centerOnLaunch" }; static constexpr std::string_view ShowTitleInTitlebarKey{ "showTerminalTitleInTitlebar" }; +static constexpr std::string_view LanguageKey{ "language" }; static constexpr std::string_view ThemeKey{ "theme" }; static constexpr std::string_view TabWidthModeKey{ "tabWidthMode" }; static constexpr std::string_view ShowTabsInTitlebarKey{ "showTabsInTitlebar" }; @@ -101,6 +102,7 @@ winrt::com_ptr GlobalAppSettings::Copy() const globals->_AlwaysShowTabs = _AlwaysShowTabs; globals->_ShowTitleInTitlebar = _ShowTitleInTitlebar; globals->_ConfirmCloseAllTabs = _ConfirmCloseAllTabs; + globals->_Language = _Language; globals->_Theme = _Theme; globals->_TabWidthMode = _TabWidthMode; globals->_ShowTabsInTitlebar = _ShowTabsInTitlebar; @@ -279,6 +281,8 @@ void GlobalAppSettings::LayerJson(const Json::Value& json) JsonUtils::GetValueForKey(json, LaunchModeKey, _LaunchMode); + JsonUtils::GetValueForKey(json, LanguageKey, _Language); + JsonUtils::GetValueForKey(json, ThemeKey, _Theme); JsonUtils::GetValueForKey(json, TabWidthModeKey, _TabWidthMode); @@ -393,6 +397,7 @@ Json::Value GlobalAppSettings::ToJson() const JsonUtils::SetValueForKey(json, WarnAboutLargePasteKey, _WarnAboutLargePaste); JsonUtils::SetValueForKey(json, WarnAboutMultiLinePasteKey, _WarnAboutMultiLinePaste); JsonUtils::SetValueForKey(json, LaunchModeKey, _LaunchMode); + JsonUtils::SetValueForKey(json, LanguageKey, _Language); JsonUtils::SetValueForKey(json, ThemeKey, _Theme); JsonUtils::SetValueForKey(json, TabWidthModeKey, _TabWidthMode); JsonUtils::SetValueForKey(json, SnapToGridOnResizeKey, _SnapToGridOnResize); diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h index f93bcd0e3..e11807b2a 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h @@ -65,6 +65,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::GlobalAppSettings, bool, AlwaysShowTabs, true); INHERITABLE_SETTING(Model::GlobalAppSettings, bool, ShowTitleInTitlebar, true); INHERITABLE_SETTING(Model::GlobalAppSettings, bool, ConfirmCloseAllTabs, true); + INHERITABLE_SETTING(Model::GlobalAppSettings, hstring, Language); INHERITABLE_SETTING(Model::GlobalAppSettings, winrt::Windows::UI::Xaml::ElementTheme, Theme, winrt::Windows::UI::Xaml::ElementTheme::Default); INHERITABLE_SETTING(Model::GlobalAppSettings, winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode, TabWidthMode, winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode::Equal); INHERITABLE_SETTING(Model::GlobalAppSettings, bool, ShowTabsInTitlebar, true); diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl index 9f3c8c745..fba59e1a4 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl @@ -43,6 +43,7 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_SETTING(Boolean, AlwaysShowTabs); INHERITABLE_SETTING(Boolean, ShowTitleInTitlebar); INHERITABLE_SETTING(Boolean, ConfirmCloseAllTabs); + INHERITABLE_SETTING(String, Language); INHERITABLE_SETTING(Windows.UI.Xaml.ElementTheme, Theme); INHERITABLE_SETTING(Microsoft.UI.Xaml.Controls.TabViewWidthMode, TabWidthMode); INHERITABLE_SETTING(Boolean, ShowTabsInTitlebar); diff --git a/src/inc/LibraryIncludes.h b/src/inc/LibraryIncludes.h index 75bc23816..996f68d24 100644 --- a/src/inc/LibraryIncludes.h +++ b/src/inc/LibraryIncludes.h @@ -20,34 +20,35 @@ #include #include +#include #include +#include +#include +#include +#include +#include #include -#include -#include #include +#include +#include #include -#include #include +#include #include #include +#include +#include +#include +#include #include -#include #include +#include #include #include +#include +#include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include // WIL #include diff --git a/src/inc/til.h b/src/inc/til.h index 9e8792b4c..509f02db9 100644 --- a/src/inc/til.h +++ b/src/inc/til.h @@ -19,7 +19,7 @@ #include "til/spsc.h" #include "til/coalesce.h" #include "til/replace.h" -#include "til/visualize_control_codes.h" +#include "til/string.h" #include "til/pmr.h" // Use keywords on TraceLogging providers to specify the category diff --git a/src/inc/til/visualize_control_codes.h b/src/inc/til/string.h similarity index 50% rename from src/inc/til/visualize_control_codes.h rename to src/inc/til/string.h index db5ae73d5..a60ae3659 100644 --- a/src/inc/til/visualize_control_codes.h +++ b/src/inc/til/string.h @@ -29,4 +29,23 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" { return visualize_control_codes(std::wstring{ str }); } + + template + constexpr bool starts_with(const std::basic_string_view str, const std::basic_string_view prefix) noexcept + { +#ifdef __cpp_lib_starts_ends_with +#error This code can be replaced in C++20, which natively supports .starts_with(). +#endif + return str.size() >= prefix.size() && Traits::compare(str.data(), prefix.data(), prefix.size()) == 0; + }; + + constexpr bool starts_with(const std::string_view str, const std::string_view prefix) noexcept + { + return starts_with<>(str, prefix); + }; + + constexpr bool starts_with(const std::wstring_view str, const std::wstring_view prefix) noexcept + { + return starts_with<>(str, prefix); + }; } diff --git a/src/renderer/dx/precomp.h b/src/renderer/dx/precomp.h index d3d51af5f..9a09c25bb 100644 --- a/src/renderer/dx/precomp.h +++ b/src/renderer/dx/precomp.h @@ -15,11 +15,8 @@ #include -#include #include -#include #include -#include #include