Add a language switcher using PrimaryLanguageOverride (#10309)

## Summary of the Pull Request

This PR adds a global "language" setting, which may be set to any supported BCP 47 tag.
Additionally a ComboBox is added to the settings UI under "Appearance", listing all languages with their localized names.

This PR introduces one new issue: If you change the language while the app is running, the UI will be in a torn state, as not all UI elements refresh automatically if the `PrimaryLanguageOverride` is changed.

## PR Checklist
* [x] Closes #5497
* [x] I work here
* [x] Tests added/passed
* [ ] Documentation updated. If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/terminal) and link it here: #xxx
* [x] Schema updated

## Validation Steps Performed

* UI language changes when changing the "language" in settings.json before starting WT / while WT is running. ✔️
* "language" field is removed from settings.json if "Use system default" is selected. ✔️
* "language" field is added or updated in settings.json if any other language is selected. ✔️
* Removes qps- languages if debugFeatures is false. ✔️
* Correctly refreshes all UI elements with the new language. 
This commit is contained in:
Leonard Hecker 2021-06-11 01:24:21 +02:00 committed by GitHub
parent 31a39b3b12
commit e34897cd1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 278 additions and 62 deletions

View File

@ -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

View File

@ -100,6 +100,6 @@
"**/bin/**": true,
"**/obj/**": true,
"**/packages/**": true,
"**/generated files/**": true
"**/Generated Files/**": true
}
}

View File

@ -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.",

View File

@ -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();

View File

@ -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();

View File

@ -25,40 +25,39 @@
#include <wil/cppwinrt.h>
#include <unknwn.h>
#include <hstring.h>
#include <winrt/Windows.ApplicationModel.h>
#include <winrt/Windows.ApplicationModel.DataTransfer.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Foundation.Metadata.h>
#include <winrt/Windows.Globalization.h>
#include <winrt/Windows.Graphics.Display.h>
#include <winrt/windows.ui.core.h>
#include <winrt/Windows.ui.input.h>
#include <winrt/Windows.System.h>
#include <winrt/Windows.UI.Core.h>
#include <winrt/Windows.UI.Input.h>
#include <winrt/Windows.UI.Text.h>
#include <winrt/Windows.UI.ViewManagement.h>
#include <winrt/Windows.UI.Xaml.Automation.Peers.h>
#include <winrt/Windows.UI.Xaml.Controls.h>
#include <winrt/Windows.UI.Xaml.Controls.Primitives.h>
#include <winrt/Windows.UI.Xaml.Data.h>
#include <winrt/Windows.ui.xaml.media.h>
#include <winrt/Windows.UI.Xaml.Documents.h>
#include <winrt/Windows.UI.Xaml.Input.h>
#include <winrt/Windows.UI.Xaml.Markup.h>
#include <winrt/Windows.UI.Xaml.Media.h>
#include <winrt/Windows.UI.Xaml.Media.Animation.h>
#include <winrt/Windows.ui.xaml.input.h>
#include <winrt/Windows.UI.Xaml.Hosting.h>
#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 <winrt/Windows.ApplicationModel.h>
#include <winrt/Windows.ApplicationModel.DataTransfer.h>
#include <winrt/Microsoft.Toolkit.Win32.UI.XamlHost.h>
#include <winrt/Microsoft.UI.Xaml.Controls.h>
#include <winrt/Microsoft.UI.Xaml.Controls.Primitives.h>
#include <winrt/Microsoft.UI.Xaml.XamlTypeInfo.h>
#include <windows.ui.xaml.media.dxinterop.h>
#include <winrt/Microsoft.Terminal.Core.h>
#include <winrt/Microsoft.Terminal.Control.h>
#include <winrt/Microsoft.Terminal.TerminalConnection.h>
#include <winrt/Microsoft.Terminal.Settings.Editor.h>
#include <winrt/Microsoft.Terminal.Settings.Model.h>
#include <winrt/Windows.System.h>
#include <windows.ui.xaml.media.dxinterop.h>
// Including TraceLogging essentials for the binary
#include <TraceLoggingProvider.h>
@ -70,14 +69,6 @@ TRACELOGGING_DECLARE_PROVIDER(g_hTerminalAppProvider);
#include <shellapi.h>
#include <shobjidl_core.h>
#include <winrt/Microsoft.Terminal.Core.h>
#include <winrt/Microsoft.Terminal.Control.h>
#include <winrt/Microsoft.Terminal.TerminalConnection.h>
#include <winrt/Microsoft.Terminal.Settings.Editor.h>
#include <winrt/Microsoft.Terminal.Settings.Model.h>
#include <winrt/Windows.UI.Popups.h>
#include <CLI11/CLI11.hpp>
// Manually include til after we include Windows.Foundation to give it winrt superpowers

View File

@ -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 <LibraryResources.h>
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<Editor::GlobalAppearancePageNavigationState>();
}
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<winrt::hstring> 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<winrt::hstring> 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<winrt::hstring>(tag);
const auto globals = _State.Globals();
if (_currentLanguage == systemLanguageTag)
{
globals.ClearLanguage();
}
else
{
globals.Language(_currentLanguage);
}
}
}

View File

@ -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<winrt::hstring> LanguageList();
winrt::Windows::Foundation::IInspectable CurrentLanguage();
void CurrentLanguage(const winrt::Windows::Foundation::IInspectable& tag);
private:
std::vector<winrt::hstring> _GetSupportedLanguageTags();
winrt::Windows::Foundation::Collections::IObservableVector<winrt::hstring> _languageList{ nullptr };
winrt::hstring _currentLanguage;
};
}

View File

@ -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<String> LanguageList { get; };
IInspectable CurrentLanguage;
IInspectable CurrentTheme;
Windows.Foundation.Collections.IObservableVector<Microsoft.Terminal.Settings.Editor.EnumEntry> ThemeList { get; };

View File

@ -28,9 +28,21 @@
<ScrollViewer>
<StackPanel Style="{StaticResource SettingsStackStyle}">
<!-- Theme -->
<local:SettingContainer x:Uid="Globals_Theme"
<!-- Language -->
<local:SettingContainer x:Uid="Globals_Language"
Margin="0">
<ComboBox ItemsSource="{x:Bind LanguageList}"
SelectedItem="{x:Bind CurrentLanguage, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="x:String">
<TextBlock Text="{x:Bind local:GlobalAppearance.LanguageDisplayConverter((x:String))}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</local:SettingContainer>
<!-- Theme -->
<local:SettingContainer x:Uid="Globals_Theme">
<muxc:RadioButtons ItemTemplate="{StaticResource EnumRadioButtonTemplate}"
ItemsSource="{x:Bind ThemeList, Mode=OneWay}"
SelectedItem="{x:Bind CurrentTheme, Mode=TwoWay}" />

View File

@ -215,6 +215,18 @@
<value>Yellow</value>
<comment>This is the header for a control that lets the user select the yellow color for text displayed on the screen.</comment>
</data>
<data name="Globals_Language.Header" xml:space="preserve">
<value>Language</value>
<comment>The header for a control allowing users to choose the app's language.</comment>
</data>
<data name="Globals_Language.HelpText" xml:space="preserve">
<value>Sets an override for the app's preferred language.</value>
<comment>A description explaining how this control changes the app's language.</comment>
</data>
<data name="Globals_LanguageDefault" xml:space="preserve">
<value>Use system default</value>
<comment>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.</comment>
</data>
<data name="Globals_AlwaysShowTabs.Header" xml:space="preserve">
<value>Always show tabs</value>
<comment>Header for a control to toggle if the app should always show the tabs (similar to a website browser).</comment>

View File

@ -20,21 +20,14 @@
#undef GetCurrentTime
#endif
#include <unknwn.h>
#include <hstring.h>
#include <winrt/Windows.ApplicationModel.Activation.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Storage.h>
#include <winrt/Windows.Storage.AccessCache.h>
#include <winrt/Windows.Globalization.h>
#include <winrt/Windows.UI.h>
#include <winrt/Windows.UI.Core.h>
#include <winrt/Windows.UI.Text.h>
#include <winrt/Windows.UI.Input.h>
#include <winrt/Windows.UI.Popups.h>
#include <winrt/Windows.UI.Text.h>
#include <winrt/Windows.UI.Xaml.h>
#include <winrt/Windows.UI.Xaml.Automation.h>
#include <winrt/Windows.UI.Xaml.Controls.h>

View File

@ -16,7 +16,6 @@ Author(s):
#pragma once
#include "pch.h"
#include "AppearanceConfig.g.h"
#include "JsonUtils.h"
#include "../inc/cppwinrt_utils.h"

View File

@ -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> 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);

View File

@ -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);

View File

@ -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);

View File

@ -20,34 +20,35 @@
#include <algorithm>
#include <atomic>
#include <cmath>
#include <deque>
#include <filesystem>
#include <fstream>
#include <functional>
#include <iomanip>
#include <iterator>
#include <list>
#include <memory>
#include <memory_resource>
#include <map>
#include <memory_resource>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <new>
#include <numeric>
#include <optional>
#include <queue>
#include <regex>
#include <set>
#include <shared_mutex>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <string>
#include <thread>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include <unordered_map>
#include <iterator>
#include <cmath>
#include <sstream>
#include <fstream>
#include <iomanip>
#include <filesystem>
#include <functional>
#include <set>
#include <unordered_set>
#include <regex>
// WIL
#include <wil/Common.h>

View File

@ -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

View File

@ -29,4 +29,23 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
{
return visualize_control_codes(std::wstring{ str });
}
template<typename T, typename Traits>
constexpr bool starts_with(const std::basic_string_view<T, Traits> str, const std::basic_string_view<T, Traits> 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);
};
}

View File

@ -15,11 +15,8 @@
#include <cmath>
#include <algorithm>
#include <exception>
#include <numeric>
#include <typeinfo>
#include <stdexcept>
#include <dcomp.h>