Qt: Implement 'System Language' language option

This commit is contained in:
Stenzek 2025-07-26 22:00:16 +10:00
parent 7f5f90338f
commit c82351a14a
No known key found for this signature in database
15 changed files with 180 additions and 97 deletions

View File

@ -4097,18 +4097,13 @@ void FullscreenUI::DrawInterfaceSettingsPage()
// Have to do this the annoying way, because it's host-derived.
const auto language_list = Host::GetAvailableLanguageList();
TinyString current_language = bsi->GetTinyStringValue("Main", "Language", "");
const char* current_language_name = "Unknown";
for (const auto& [language, code] : language_list)
{
if (current_language == code)
current_language_name = language;
}
if (MenuButtonWithValue(FSUI_ICONVSTR(ICON_FA_LANGUAGE, "Language"),
FSUI_VSTR("Chooses the language used for UI elements."), current_language_name))
FSUI_VSTR("Chooses the language used for UI elements."),
Host::GetLanguageName(current_language)))
{
ImGuiFullscreen::ChoiceDialogOptions options;
for (const auto& [language, code] : language_list)
options.emplace_back(fmt::format("{} [{}]", language, code), (current_language == code));
options.emplace_back(Host::GetLanguageName(code), (current_language == code));
OpenChoiceDialog(FSUI_ICONVSTR(ICON_FA_LANGUAGE, "UI Language"), false, std::move(options),
[language_list](s32 index, const std::string& title, bool checked) {
if (static_cast<u32>(index) >= language_list.size())

View File

@ -72,6 +72,9 @@ void ReportDebuggerMessage(std::string_view message);
/// Returns a list of supported languages and codes (suffixes for translation files).
std::span<const std::pair<const char*, const char*>> GetAvailableLanguageList();
/// Returns the localized language name for the specified language code.
const char* GetLanguageName(std::string_view language_code);
/// Refreshes the UI when the language is changed.
bool ChangeLanguage(const char* new_language);

View File

@ -337,6 +337,11 @@ std::span<const std::pair<const char*, const char*>> Host::GetAvailableLanguageL
return {};
}
const char* Host::GetLanguageName(std::string_view language_code)
{
return "";
}
bool Host::ChangeLanguage(const char* new_language)
{
return false;

View File

@ -133,6 +133,7 @@ set(SRCS
qtprogresscallback.cpp
qtprogresscallback.h
qtthemes.cpp
qttranslations.inl
qtutils.cpp
qtutils.h
resource.h

View File

@ -49,8 +49,6 @@ const char* InterfaceSettingsWidget::THEME_VALUES[] = {
nullptr,
};
const char* InterfaceSettingsWidget::DEFAULT_THEME_NAME = "darkfusion";
InterfaceSettingsWidget::InterfaceSettingsWidget(SettingsWindow* dialog, QWidget* parent)
: QWidget(parent), m_dialog(dialog)
{
@ -86,8 +84,7 @@ InterfaceSettingsWidget::InterfaceSettingsWidget(SettingsWindow* dialog, QWidget
connect(m_ui.theme, QOverload<int>::of(&QComboBox::currentIndexChanged), [this]() { emit themeChanged(); });
populateLanguageDropdown(m_ui.language);
SettingWidgetBinder::BindWidgetToStringSetting(sif, m_ui.language, "Main", "Language",
QtHost::GetDefaultLanguage());
SettingWidgetBinder::BindWidgetToStringSetting(sif, m_ui.language, "Main", "Language", {});
connect(m_ui.language, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&InterfaceSettingsWidget::onLanguageChanged);
@ -180,16 +177,8 @@ void InterfaceSettingsWidget::populateLanguageDropdown(QComboBox* cb)
{
for (const auto& [language, code] : Host::GetAvailableLanguageList())
{
QString icon_filename(QStringLiteral(":/icons/flags/%1.png").arg(QLatin1StringView(code)));
if (!QFile::exists(icon_filename))
{
// try without the suffix (e.g. es-es -> es)
const char* pos = std::strrchr(code, '-');
if (pos)
icon_filename = QStringLiteral(":/icons/flags/%1.png").arg(QLatin1StringView(pos));
}
cb->addItem(QIcon(icon_filename), QString::fromUtf8(language), QString::fromLatin1(code));
cb->addItem(QtUtils::GetIconForTranslationLanguage(code), QString::fromUtf8(Host::GetLanguageName(code)),
QString::fromLatin1(code));
}
}

View File

@ -27,6 +27,8 @@ private Q_SLOTS:
void onLanguageChanged();
private:
void setupAdditionalUi();
Ui::InterfaceSettingsWidget m_ui;
SettingsWindow* m_dialog;
@ -34,5 +36,4 @@ private:
public:
static const char* THEME_NAMES[];
static const char* THEME_VALUES[];
static const char* DEFAULT_THEME_NAME;
};

View File

@ -124,6 +124,7 @@ static void SaveSettings();
static bool RunSetupWizard();
static void UpdateFontOrder(std::string_view language);
static void UpdateApplicationLocale(std::string_view language);
static std::string_view GetSystemLanguage();
static std::optional<bool> DownloadFile(QWidget* parent, const QString& title, std::string url, std::vector<u8>* data);
static void InitializeEarlyConsole();
static void HookSignals();
@ -2263,9 +2264,11 @@ void QtHost::UpdateApplicationLanguage(QWidget* dialog_parent)
}
s_translators.clear();
// Fix old language names.
const std::string language = Host::GetBaseStringSettingValue("Main", "Language", GetDefaultLanguage());
const QString qlanguage = QString::fromStdString(language);
// Fixup automatic language.
std::string language = Host::GetBaseStringSettingValue("Main", "Language", "");
if (language.empty())
language = GetSystemLanguage();
QString qlanguage = QString::fromStdString(language);
// install the base qt translation first
#ifndef __APPLE__
@ -2377,25 +2380,64 @@ SmallString Host::TranslatePluralToSmallString(const char* context, const char*
std::span<const std::pair<const char*, const char*>> Host::GetAvailableLanguageList()
{
static constexpr const std::pair<const char*, const char*> languages[] = {{"English", "en"},
{"Español de Latinoamérica", "es"},
{"Español de España", "es-ES"},
{"Français", "fr"},
{"Bahasa Indonesia", "id"},
{"日本語", "ja"},
{"한국어", "ko"},
{"Italiano", "it"},
{"Polski", "pl"},
{"Português (Pt)", "pt-PT"},
{"Português (Br)", "pt-BR"},
{"Русский", "ru"},
{"Svenska", "sv"},
{"Türkçe", "tr"},
{"简体中文", "zh-CN"}};
static constexpr const std::pair<const char*, const char*> languages[] = {
{QT_TRANSLATE_NOOP("QtHost", "System Language"), ""},
#define TRANSLATION_LIST_ENTRY(name, our_translation_code, locale_code) \
{name " [" our_translation_code "]", our_translation_code},
#include "qttranslations.inl"
#undef TRANSLATION_LIST_ENTRY
};
return languages;
}
const char* Host::GetLanguageName(std::string_view language_code)
{
for (const auto& [name, code] : GetAvailableLanguageList())
{
if (language_code == code)
return Host::TranslateToCString("QtHost", name);
}
return TRANSLATE("QtHost", "Unknown");
}
std::string_view QtHost::GetSystemLanguage()
{
std::string locname = QLocale::system().name().toStdString();
// Does this match any of our translations?
for (const auto& [lname, lcode] : Host::GetAvailableLanguageList())
{
if (locname == lcode)
return lcode;
}
// Check for a partial match, e.g. "zh" for "zh-CN".
if (const std::string::size_type pos = locname.find('-'); pos != std::string::npos)
{
const std::string_view plocname = std::string_view(locname).substr(0, pos);
for (const auto& [lname, lcode] : Host::GetAvailableLanguageList())
{
// Only some languages have a country code, so we need to check both.
const std::string_view lcodev(lcode);
if (lcodev == plocname)
{
return lcode;
}
else if (const std::string_view::size_type lpos = lcodev.find('-'); lpos != std::string::npos)
{
if (lcodev.substr(0, lpos) == plocname)
return lcode;
}
}
}
// Fallback to English.
return "en";
}
bool Host::ChangeLanguage(const char* new_language)
{
Host::RunOnUIThread([new_language = std::string(new_language)]() {
@ -2407,12 +2449,6 @@ bool Host::ChangeLanguage(const char* new_language)
return true;
}
const char* QtHost::GetDefaultLanguage()
{
// TODO: Default system language instead.
return "en";
}
void QtHost::UpdateFontOrder(std::string_view language)
{
// Why is this a thing? Because we want all glyphs to be available, but don't want to conflict

View File

@ -339,9 +339,6 @@ const QLocale& GetApplicationLocale();
/// Default theme name for the platform.
const char* GetDefaultThemeName();
/// Default language for the platform.
const char* GetDefaultLanguage();
/// Sets application theme according to settings.
void UpdateApplicationTheme();
@ -366,9 +363,6 @@ bool CanRenderToMainWindow();
/// Returns true if the separate-window display widget should use the main window coordinates.
bool UseMainWindowGeometryForDisplayWindow();
/// Default language for the platform.
const char* GetDefaultLanguage();
/// Call when the language changes.
void UpdateApplicationLanguage(QWidget* dialog_parent);

View File

@ -1,7 +1,6 @@
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com> and contributors.
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "interfacesettingswidget.h"
#include "qthost.h"
#include "util/imgui_fullscreen.h"
@ -42,8 +41,7 @@ void QtHost::UpdateApplicationTheme()
void QtHost::SetStyleFromSettings()
{
const TinyString theme =
Host::GetBaseTinyStringSettingValue("UI", "Theme", InterfaceSettingsWidget::DEFAULT_THEME_NAME);
const TinyString theme = Host::GetBaseTinyStringSettingValue("UI", "Theme", QtHost::GetDefaultThemeName());
if (theme == "qdarkstyle")
{
@ -202,40 +200,40 @@ void QtHost::SetStyleFromSettings()
qApp->setPalette(darkPalette);
qApp->setStyleSheet(QString());
}
else if (theme == "greengiant")
{
// Custom palette by RedDevilus, Tame (Light/Washed out) Green as main color and Grayish Blue as complimentary.
// Alternative white theme.
qApp->setStyle(QStyleFactory::create("Fusion"));
else if (theme == "greengiant")
{
// Custom palette by RedDevilus, Tame (Light/Washed out) Green as main color and Grayish Blue as complimentary.
// Alternative white theme.
qApp->setStyle(QStyleFactory::create("Fusion"));
const QColor black(25, 25, 25);
const QColor gray(111, 111, 111);
const QColor limerick(176, 196, 0);
const QColor brown(135, 100, 50);
const QColor pear(213, 222, 46);
const QColor black(25, 25, 25);
const QColor gray(111, 111, 111);
const QColor limerick(176, 196, 0);
const QColor brown(135, 100, 50);
const QColor pear(213, 222, 46);
QPalette greenGiantPalette;
greenGiantPalette.setColor(QPalette::Window, pear);
greenGiantPalette.setColor(QPalette::WindowText, black);
greenGiantPalette.setColor(QPalette::Base, limerick);
greenGiantPalette.setColor(QPalette::AlternateBase, brown.lighter());
greenGiantPalette.setColor(QPalette::ToolTipBase, brown);
greenGiantPalette.setColor(QPalette::ToolTipText, Qt::white);
greenGiantPalette.setColor(QPalette::Text, black);
greenGiantPalette.setColor(QPalette::Button, brown.lighter());
greenGiantPalette.setColor(QPalette::ButtonText, black.lighter());
greenGiantPalette.setColor(QPalette::Link, brown.lighter());
greenGiantPalette.setColor(QPalette::Highlight, brown);
greenGiantPalette.setColor(QPalette::HighlightedText, Qt::white);
QPalette greenGiantPalette;
greenGiantPalette.setColor(QPalette::Window, pear);
greenGiantPalette.setColor(QPalette::WindowText, black);
greenGiantPalette.setColor(QPalette::Base, limerick);
greenGiantPalette.setColor(QPalette::AlternateBase, brown.lighter());
greenGiantPalette.setColor(QPalette::ToolTipBase, brown);
greenGiantPalette.setColor(QPalette::ToolTipText, Qt::white);
greenGiantPalette.setColor(QPalette::Text, black);
greenGiantPalette.setColor(QPalette::Button, brown.lighter());
greenGiantPalette.setColor(QPalette::ButtonText, black.lighter());
greenGiantPalette.setColor(QPalette::Link, brown.lighter());
greenGiantPalette.setColor(QPalette::Highlight, brown);
greenGiantPalette.setColor(QPalette::HighlightedText, Qt::white);
greenGiantPalette.setColor(QPalette::Disabled, QPalette::ButtonText, gray);
greenGiantPalette.setColor(QPalette::Disabled, QPalette::WindowText, gray.darker());
greenGiantPalette.setColor(QPalette::Disabled, QPalette::Text, gray.darker());
greenGiantPalette.setColor(QPalette::Disabled, QPalette::Light, gray);
greenGiantPalette.setColor(QPalette::Disabled, QPalette::ButtonText, gray);
greenGiantPalette.setColor(QPalette::Disabled, QPalette::WindowText, gray.darker());
greenGiantPalette.setColor(QPalette::Disabled, QPalette::Text, gray.darker());
greenGiantPalette.setColor(QPalette::Disabled, QPalette::Light, gray);
qApp->setPalette(greenGiantPalette);
qApp->setStyleSheet(QString());
}
qApp->setPalette(greenGiantPalette);
qApp->setStyleSheet(QString());
}
else if (theme == "pinkypals")
{
qApp->setStyle(QStyleFactory::create("Fusion"));
@ -398,8 +396,7 @@ void QtHost::SetIconThemeFromStyle()
const char* Host::GetDefaultFullscreenUITheme()
{
const TinyString theme =
Host::GetBaseTinyStringSettingValue("UI", "Theme", InterfaceSettingsWidget::DEFAULT_THEME_NAME);
const TinyString theme = Host::GetBaseTinyStringSettingValue("UI", "Theme", QtHost::GetDefaultThemeName());
if (theme == "cobaltsky")
return "CobaltSky";

View File

@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
// TRANSLATION_LIST_ENTRY(name, our_translation_code, locale_code)
TRANSLATION_LIST_ENTRY("English", "en", "en-US")
TRANSLATION_LIST_ENTRY("Español de Latinoamérica", "es", "es-ES")
TRANSLATION_LIST_ENTRY("Español de España", "es-ES", "es-ES")
TRANSLATION_LIST_ENTRY("Français", "fr", "fr-FR")
TRANSLATION_LIST_ENTRY("Bahasa Indonesia", "id", "id-ID")
TRANSLATION_LIST_ENTRY("日本語", "ja", "ja-JA")
TRANSLATION_LIST_ENTRY("한국어", "ko", "ko-KO")
TRANSLATION_LIST_ENTRY("Italiano", "it", "it-IT")
TRANSLATION_LIST_ENTRY("Polski", "pl", "pl-PL")
TRANSLATION_LIST_ENTRY("Português (Pt)", "pt-PT", "pt-PT")
TRANSLATION_LIST_ENTRY("Português (Br)", "pt-BR", "pt-BR")
TRANSLATION_LIST_ENTRY("Русский", "ru", "ru-RU")
TRANSLATION_LIST_ENTRY("Svenska", "sv", "sv-SV")
TRANSLATION_LIST_ENTRY("Türkçe", "tr", "tr-TR")
TRANSLATION_LIST_ENTRY("简体中文", "zh-CN", "zh-CN")

View File

@ -257,6 +257,31 @@ void QtUtils::ResizePotentiallyFixedSizeWindow(QWidget* widget, int width, int h
widget->resize(width, height);
}
QIcon QtUtils::GetIconForTranslationLanguage(std::string_view language_name)
{
QString icon_path;
if (!language_name.empty())
{
const QLatin1StringView qlanguage_name(language_name.data(), language_name.length());
icon_path = QStringLiteral(":/icons/flags/%1.png").arg(qlanguage_name);
if (!QFile::exists(icon_path))
{
// try without the suffix (e.g. es-es -> es)
const int index = qlanguage_name.indexOf('-');
if (index >= 0)
icon_path = QStringLiteral(":/icons/flags/%1.png").arg(qlanguage_name.left(index));
}
}
else
{
// no language specified, use the default icon
icon_path = QStringLiteral(":/icons/applications-system.png");
}
return QIcon(icon_path);
}
QIcon QtUtils::GetIconForRegion(ConsoleRegion region)
{
switch (region)

View File

@ -103,6 +103,9 @@ void SetWindowResizeable(QWidget* widget, bool resizeable);
/// Adjusts the fixed size for a window if it's not resizeable.
void ResizePotentiallyFixedSizeWindow(QWidget* widget, int width, int height);
/// Returns icon for language.
QIcon GetIconForTranslationLanguage(std::string_view language_name);
/// Returns icon for region.
QIcon GetIconForRegion(ConsoleRegion region);
QIcon GetIconForRegion(DiscRegion region);

View File

@ -183,7 +183,7 @@ void SetupWizardDialog::setupUi()
connect(m_ui.next, &QPushButton::clicked, this, &SetupWizardDialog::nextPage);
connect(m_ui.cancel, &QPushButton::clicked, this, &SetupWizardDialog::confirmCancel);
setupLanguagePage();
setupLanguagePage(true);
setupBIOSPage();
setupGameListPage();
setupControllerPage(true);
@ -191,20 +191,27 @@ void SetupWizardDialog::setupUi()
setupAchievementsPage(true);
}
void SetupWizardDialog::setupLanguagePage()
void SetupWizardDialog::setupLanguagePage(bool initial)
{
SettingWidgetBinder::DisconnectWidget(m_ui.theme);
m_ui.theme->clear();
SettingWidgetBinder::BindWidgetToEnumSetting(nullptr, m_ui.theme, "UI", "Theme", InterfaceSettingsWidget::THEME_NAMES,
InterfaceSettingsWidget::THEME_VALUES,
InterfaceSettingsWidget::DEFAULT_THEME_NAME, "InterfaceSettingsWidget");
InterfaceSettingsWidget::THEME_VALUES, QtHost::GetDefaultThemeName(),
"MainWindow");
connect(m_ui.theme, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &SetupWizardDialog::themeChanged);
SettingWidgetBinder::DisconnectWidget(m_ui.language);
m_ui.language->clear();
InterfaceSettingsWidget::populateLanguageDropdown(m_ui.language);
SettingWidgetBinder::BindWidgetToStringSetting(nullptr, m_ui.language, "Main", "Language",
QtHost::GetDefaultLanguage());
SettingWidgetBinder::BindWidgetToStringSetting(nullptr, m_ui.language, "Main", "Language", {});
connect(m_ui.language, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&SetupWizardDialog::languageChanged);
SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.autoUpdateEnabled, "AutoUpdater", "CheckAtStartup", true);
if (initial)
{
SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.autoUpdateEnabled, "AutoUpdater", "CheckAtStartup",
true);
}
}
void SetupWizardDialog::themeChanged()
@ -218,6 +225,7 @@ void SetupWizardDialog::languageChanged()
// Skip the recreation, since we don't have many dynamic UI elements.
QtHost::UpdateApplicationLanguage(this);
m_ui.retranslateUi(this);
setupLanguagePage(false);
setupControllerPage(false);
setupGraphicsPage(false);
setupAchievementsPage(false);
@ -631,7 +639,8 @@ void SetupWizardDialog::setupAchievementsPage(bool initial)
{
if (initial)
{
m_ui.achievementsIconLabel->setPixmap(QPixmap(QString::fromStdString(QtHost::GetResourcePath("images/ra-icon.webp", true))));
m_ui.achievementsIconLabel->setPixmap(
QPixmap(QString::fromStdString(QtHost::GetResourcePath("images/ra-icon.webp", true))));
QFont title_font(m_ui.achievementsTitleLabel->font());
title_font.setBold(true);
title_font.setPixelSize(20);

View File

@ -64,7 +64,7 @@ private:
};
void setupUi();
void setupLanguagePage();
void setupLanguagePage(bool initial);
void setupBIOSPage();
void setupGameListPage();
void setupControllerPage(bool initial);

View File

@ -191,6 +191,11 @@ std::span<const std::pair<const char*, const char*>> Host::GetAvailableLanguageL
return {};
}
const char* Host::GetLanguageName(std::string_view language_code)
{
return "";
}
bool Host::ChangeLanguage(const char* new_language)
{
return false;