Skip to content

Commit

Permalink
Add a language switcher using PrimaryLanguageOverride (#10309)
Browse files Browse the repository at this point in the history
## 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. ❌
  • Loading branch information
lhecker authored Jun 10, 2021
1 parent 31a39b3 commit e34897c
Show file tree
Hide file tree
Showing 20 changed files with 278 additions and 62 deletions.
3 changes: 3 additions & 0 deletions .github/actions/spelling/allow/allow.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ downsides
dze
dzhe
Enum'd
formattings
ftp
geeksforgeeks
ghe
Expand All @@ -34,6 +35,7 @@ overlined
postmodern
ptys
qof
qps
reimplementation
reserialization
reserialize
Expand All @@ -46,6 +48,7 @@ tokenizes
tonos
tshe
UIs
und
versioned
We'd
wildcards
Expand Down
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,6 @@
"**/bin/**": true,
"**/obj/**": true,
"**/packages/**": true,
"**/generated files/**": true
"**/Generated Files/**": true
}
}
5 changes: 5 additions & 0 deletions doc/cascadia/profiles.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
15 changes: 15 additions & 0 deletions src/cascadia/TerminalApp/AppLogic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ namespace winrt::TerminalApp::implementation
});
_root->Create();

_ApplyLanguageSettingChange();
_ApplyTheme(_settings.GlobalSettings().Theme());
_ApplyStartupTaskStateChange();

Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -1023,6 +1037,7 @@ namespace winrt::TerminalApp::implementation
// Update the settings in TerminalPage
_root->SetSettings(_settings, true);

_ApplyLanguageSettingChange();
_RefreshThemeRoutine();
_ApplyStartupTaskStateChange();

Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalApp/AppLogic.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
45 changes: 18 additions & 27 deletions src/cascadia/TerminalApp/pch.h
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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
Expand Down
144 changes: 143 additions & 1 deletion src/cascadia/TerminalSettingsEditor/GlobalAppearance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand All @@ -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);
}
}
}
17 changes: 16 additions & 1 deletion src/cascadia/TerminalSettingsEditor/GlobalAppearance.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
}

Expand Down
6 changes: 5 additions & 1 deletion src/cascadia/TerminalSettingsEditor/GlobalAppearance.idl
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import "EnumEntry.idl";
import "EnumEntry.idl";

namespace Microsoft.Terminal.Settings.Editor
{
Expand All @@ -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; };

Expand Down
16 changes: 14 additions & 2 deletions src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}" />
Expand Down
Loading

0 comments on commit e34897c

Please sign in to comment.