diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index a1370b3f3..2092827cd 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -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(index) >= language_list.size()) diff --git a/src/core/host.h b/src/core/host.h index ac48f214d..1fbf22473 100644 --- a/src/core/host.h +++ b/src/core/host.h @@ -72,6 +72,9 @@ void ReportDebuggerMessage(std::string_view message); /// Returns a list of supported languages and codes (suffixes for translation files). std::span> 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); diff --git a/src/duckstation-mini/mini_host.cpp b/src/duckstation-mini/mini_host.cpp index 07cc81bad..41c0ab0d8 100644 --- a/src/duckstation-mini/mini_host.cpp +++ b/src/duckstation-mini/mini_host.cpp @@ -337,6 +337,11 @@ std::span> Host::GetAvailableLanguageL return {}; } +const char* Host::GetLanguageName(std::string_view language_code) +{ + return ""; +} + bool Host::ChangeLanguage(const char* new_language) { return false; diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt index d40b8047c..a789b0c5f 100644 --- a/src/duckstation-qt/CMakeLists.txt +++ b/src/duckstation-qt/CMakeLists.txt @@ -133,6 +133,7 @@ set(SRCS qtprogresscallback.cpp qtprogresscallback.h qtthemes.cpp + qttranslations.inl qtutils.cpp qtutils.h resource.h diff --git a/src/duckstation-qt/interfacesettingswidget.cpp b/src/duckstation-qt/interfacesettingswidget.cpp index f42043839..2305dddab 100644 --- a/src/duckstation-qt/interfacesettingswidget.cpp +++ b/src/duckstation-qt/interfacesettingswidget.cpp @@ -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::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::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)); } } diff --git a/src/duckstation-qt/interfacesettingswidget.h b/src/duckstation-qt/interfacesettingswidget.h index 35d66335b..1f085495c 100644 --- a/src/duckstation-qt/interfacesettingswidget.h +++ b/src/duckstation-qt/interfacesettingswidget.h @@ -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; }; diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index 749a0d59c..656e99d93 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -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 DownloadFile(QWidget* parent, const QString& title, std::string url, std::vector* 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> Host::GetAvailableLanguageList() { - static constexpr const std::pair 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 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 diff --git a/src/duckstation-qt/qthost.h b/src/duckstation-qt/qthost.h index 17e85431d..86f83311c 100644 --- a/src/duckstation-qt/qthost.h +++ b/src/duckstation-qt/qthost.h @@ -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); diff --git a/src/duckstation-qt/qtthemes.cpp b/src/duckstation-qt/qtthemes.cpp index 3b188fc9e..b43252710 100644 --- a/src/duckstation-qt/qtthemes.cpp +++ b/src/duckstation-qt/qtthemes.cpp @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin 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"; diff --git a/src/duckstation-qt/qttranslations.inl b/src/duckstation-qt/qttranslations.inl new file mode 100644 index 000000000..aa62c590c --- /dev/null +++ b/src/duckstation-qt/qttranslations.inl @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin +// 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") diff --git a/src/duckstation-qt/qtutils.cpp b/src/duckstation-qt/qtutils.cpp index 3394835a5..2d653cb77 100644 --- a/src/duckstation-qt/qtutils.cpp +++ b/src/duckstation-qt/qtutils.cpp @@ -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) diff --git a/src/duckstation-qt/qtutils.h b/src/duckstation-qt/qtutils.h index 942040fca..6480ca9e0 100644 --- a/src/duckstation-qt/qtutils.h +++ b/src/duckstation-qt/qtutils.h @@ -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); diff --git a/src/duckstation-qt/setupwizarddialog.cpp b/src/duckstation-qt/setupwizarddialog.cpp index 03798f6f4..c25efd0c8 100644 --- a/src/duckstation-qt/setupwizarddialog.cpp +++ b/src/duckstation-qt/setupwizarddialog.cpp @@ -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::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::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); diff --git a/src/duckstation-qt/setupwizarddialog.h b/src/duckstation-qt/setupwizarddialog.h index fe3768454..b7d4eb4de 100644 --- a/src/duckstation-qt/setupwizarddialog.h +++ b/src/duckstation-qt/setupwizarddialog.h @@ -64,7 +64,7 @@ private: }; void setupUi(); - void setupLanguagePage(); + void setupLanguagePage(bool initial); void setupBIOSPage(); void setupGameListPage(); void setupControllerPage(bool initial); diff --git a/src/duckstation-regtest/regtest_host.cpp b/src/duckstation-regtest/regtest_host.cpp index ca5abd817..dd9fee8ca 100644 --- a/src/duckstation-regtest/regtest_host.cpp +++ b/src/duckstation-regtest/regtest_host.cpp @@ -191,6 +191,11 @@ std::span> Host::GetAvailableLanguageL return {}; } +const char* Host::GetLanguageName(std::string_view language_code) +{ + return ""; +} + bool Host::ChangeLanguage(const char* new_language) { return false;