diff --git a/Source/Core/DolphinQt2/CMakeLists.txt b/Source/Core/DolphinQt2/CMakeLists.txt index 3b3530e2ff..e0e94d01fb 100644 --- a/Source/Core/DolphinQt2/CMakeLists.txt +++ b/Source/Core/DolphinQt2/CMakeLists.txt @@ -28,6 +28,7 @@ set(SRCS Resources.cpp Settings.cpp ToolBar.cpp + Translation.cpp WiiUpdate.cpp WiiUpdate.h Config/ControllersWindow.cpp @@ -99,6 +100,39 @@ set(DOLPHINQT2_BINARY dolphin-emu-qt2) add_executable(${DOLPHINQT2_BINARY} ${SRCS} ${UI_HEADERS}) target_link_libraries(${DOLPHINQT2_BINARY} ${LIBS} Qt5::Widgets) +# Handle localization +find_package(Gettext) +if(GETTEXT_MSGMERGE_EXECUTABLE AND GETTEXT_MSGFMT_EXECUTABLE) + set(pot_file "${CMAKE_SOURCE_DIR}/Languages/po/dolphin-emu.pot") + file(GLOB LINGUAS ${CMAKE_SOURCE_DIR}/Languages/po/*.po) + + target_sources(dolphin-emu-qt2 PRIVATE ${pot_file} ${LINGUAS}) + source_group("Localization" FILES ${LINGUAS}) + source_group("Localization\\\\Generated" FILES ${pot_file}) + + foreach(po ${LINGUAS}) + get_filename_component(lang ${po} NAME_WE) + set(mo_dir ${CMAKE_CURRENT_BINARY_DIR}/${lang}) + set(mo ${mo_dir}/dolphin-emu.mo) + + target_sources(dolphin-emu-qt2 PRIVATE ${mo}) + source_group("Localization\\\\Generated" FILES ${mo}) + + if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set_source_files_properties(${mo} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/${lang}.lproj") + else() + install(FILES ${mo} DESTINATION share/locale/${lang}/LC_MESSAGES) + endif() + + add_custom_command(OUTPUT ${mo} + COMMAND mkdir -p ${mo_dir} + COMMAND ${GETTEXT_MSGMERGE_EXECUTABLE} --quiet --update --backup=none -s ${po} ${pot_file} + COMMAND ${GETTEXT_MSGFMT_EXECUTABLE} -o ${mo} ${po} + DEPENDS ${po} + ) + endforeach() +endif() + if(APPLE) # Note: This is copied from DolphinQt, based on the DolphinWX version. diff --git a/Source/Core/DolphinQt2/DolphinQt2.vcxproj b/Source/Core/DolphinQt2/DolphinQt2.vcxproj index adc9952276..319d34c552 100644 --- a/Source/Core/DolphinQt2/DolphinQt2.vcxproj +++ b/Source/Core/DolphinQt2/DolphinQt2.vcxproj @@ -218,6 +218,7 @@ + @@ -243,6 +244,7 @@ + diff --git a/Source/Core/DolphinQt2/Main.cpp b/Source/Core/DolphinQt2/Main.cpp index e00889ab65..fea2b56379 100644 --- a/Source/Core/DolphinQt2/Main.cpp +++ b/Source/Core/DolphinQt2/Main.cpp @@ -18,6 +18,7 @@ #include "DolphinQt2/QtUtils/RunOnObject.h" #include "DolphinQt2/Resources.h" #include "DolphinQt2/Settings.h" +#include "DolphinQt2/Translation.h" #include "UICommon/CommandLineParse.h" #include "UICommon/UICommon.h" @@ -75,6 +76,9 @@ int main(int argc, char* argv[]) // Hook up alerts from core RegisterMsgAlertHandler(QtMsgAlertHandler); + // Hook up translations + Translation::Initialize(); + // Whenever the event loop is about to go to sleep, dispatch the jobs // queued in the Core first. QObject::connect(QAbstractEventDispatcher::instance(), &QAbstractEventDispatcher::aboutToBlock, diff --git a/Source/Core/DolphinQt2/Settings/InterfacePane.cpp b/Source/Core/DolphinQt2/Settings/InterfacePane.cpp index 877321818b..5832b442a3 100644 --- a/Source/Core/DolphinQt2/Settings/InterfacePane.cpp +++ b/Source/Core/DolphinQt2/Settings/InterfacePane.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -19,6 +20,56 @@ #include "DolphinQt2/Settings.h" +static QComboBox* MakeLanguageComboBox() +{ + static const struct + { + const QString name; + const char* id; + } languages[] = { + {QStringLiteral("Bahasa Melayu"), "ms"}, // Malay + {QStringLiteral("Catal\u00E0"), "ca"}, // Catalan + {QStringLiteral("\u010Ce\u0161tina"), "cs"}, // Czech + {QStringLiteral("Dansk"), "da"}, // Danish + {QStringLiteral("Deutsch"), "de"}, // German + {QStringLiteral("English"), "en"}, // English + {QStringLiteral("Espa\u00F1ol"), "es"}, // Spanish + {QStringLiteral("Fran\u00E7ais"), "fr"}, // French + {QStringLiteral("Hrvatski"), "hr"}, // Croatian + {QStringLiteral("Italiano"), "it"}, // Italian + {QStringLiteral("Magyar"), "hu"}, // Hungarian + {QStringLiteral("Nederlands"), "nl"}, // Dutch + {QStringLiteral("Norsk bokm\u00E5l"), "nb"}, // Norwegian + {QStringLiteral("Polski"), "pl"}, // Polish + {QStringLiteral("Portugu\u00EAs"), "pt"}, // Portuguese + {QStringLiteral("Portugu\u00EAs (Brasil)"), "pt_BR"}, // Portuguese (Brazil) + {QStringLiteral("Rom\u00E2n\u0103"), "ro"}, // Romanian + {QStringLiteral("Srpski"), "sr"}, // Serbian + {QStringLiteral("Svenska"), "sv"}, // Swedish + {QStringLiteral("T\u00FCrk\u00E7e"), "tr"}, // Turkish + {QStringLiteral("\u0395\u03BB\u03BB\u03B7\u03BD\u03B9\u03BA\u03AC"), "el"}, // Greek + {QStringLiteral("\u0420\u0443\u0441\u0441\u043A\u0438\u0439"), "ru"}, // Russian + {QStringLiteral("\u0627\u0644\u0639\u0631\u0628\u064A\u0629"), "ar"}, // Arabic + {QStringLiteral("\u0641\u0627\u0631\u0633\u06CC"), "fa"}, // Farsi + {QStringLiteral("\uD55C\uAD6D\uC5B4"), "ko"}, // Korean + {QStringLiteral("\u65E5\u672C\u8A9E"), "ja"}, // Japanese + {QStringLiteral("\u7B80\u4F53\u4E2D\u6587"), "zh_CN"}, // Simplified Chinese + {QStringLiteral("\u7E41\u9AD4\u4E2D\u6587"), "zh_TW"}, // Traditional Chinese + }; + + auto* combobox = new QComboBox(); + combobox->addItem(QObject::tr(""), QStringLiteral("")); + for (const auto& lang : languages) + combobox->addItem(lang.name, QString::fromLatin1(lang.id)); + + // The default, QComboBox::AdjustToContentsOnFirstShow, causes a noticeable pause when opening the + // SettingWindow for the first time. The culprit seems to be non-Latin graphemes in the above + // list. QComboBox::AdjustToContents still has some lag but it's much less noticeable. + combobox->setSizeAdjustPolicy(QComboBox::AdjustToContents); + + return combobox; +} + InterfacePane::InterfacePane(QWidget* parent) : QWidget(parent) { CreateLayout(); @@ -48,9 +99,7 @@ void InterfacePane::CreateUI() auto* combobox_layout = new QFormLayout; groupbox_layout->addLayout(combobox_layout); - m_combobox_language = new QComboBox; - // TODO: Support more languages other then English - m_combobox_language->addItem(tr("English")); + m_combobox_language = MakeLanguageComboBox(); combobox_layout->addRow(tr("&Language:"), m_combobox_language); // Theme Combobox @@ -111,8 +160,8 @@ void InterfacePane::ConnectLayout() &InterfacePane::OnSaveConfig); connect(m_combobox_theme, static_cast(&QComboBox::activated), &Settings::Instance(), &Settings::SetThemeName); - connect(m_combobox_language, static_cast(&QComboBox::activated), - [this](int index) { OnSaveConfig(); }); + connect(m_combobox_language, static_cast(&QComboBox::activated), this, + &InterfacePane::OnSaveConfig); connect(m_checkbox_confirm_on_stop, &QCheckBox::clicked, this, &InterfacePane::OnSaveConfig); connect(m_checkbox_use_panic_handlers, &QCheckBox::clicked, this, &InterfacePane::OnSaveConfig); connect(m_checkbox_enable_osd, &QCheckBox::clicked, this, &InterfacePane::OnSaveConfig); @@ -128,6 +177,8 @@ void InterfacePane::LoadConfig() m_checkbox_top_window->setChecked(startup_params.bKeepWindowOnTop); m_checkbox_render_to_window->setChecked(startup_params.bRenderToMain); m_checkbox_use_builtin_title_database->setChecked(startup_params.m_use_builtin_title_database); + m_combobox_language->setCurrentIndex(m_combobox_language->findData( + QString::fromStdString(SConfig::GetInstance().m_InterfaceLanguage))); m_combobox_theme->setCurrentIndex( m_combobox_theme->findText(QString::fromStdString(SConfig::GetInstance().theme_name))); @@ -155,5 +206,14 @@ void InterfacePane::OnSaveConfig() settings.m_show_active_title = m_checkbox_show_active_title->isChecked(); settings.m_PauseOnFocusLost = m_checkbox_pause_on_focus_lost->isChecked(); + auto new_language = m_combobox_language->currentData().toString().toStdString(); + if (new_language != SConfig::GetInstance().m_InterfaceLanguage) + { + SConfig::GetInstance().m_InterfaceLanguage = new_language; + QMessageBox::information( + this, tr("Restart Required"), + tr("You must restart Dolphin in order for the change to take effect.")); + } + settings.SaveSettings(); } diff --git a/Source/Core/DolphinQt2/Translation.cpp b/Source/Core/DolphinQt2/Translation.cpp new file mode 100644 index 0000000000..b73701c555 --- /dev/null +++ b/Source/Core/DolphinQt2/Translation.cpp @@ -0,0 +1,298 @@ +// Copyright 2017 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "DolphinQt2/Translation.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "Common/File.h" +#include "Common/FileUtil.h" +#include "Common/Logging/Log.h" +#include "Common/MsgHandler.h" +#include "Common/StringUtil.h" +#include "Core/ConfigManager.h" + +constexpr u32 MO_MAGIC_NUMBER = 0x950412de; + +static u16 ReadU16(const char* data) +{ + u16 value; + std::memcpy(&value, data, sizeof(value)); + return value; +} + +static u32 ReadU32(const char* data) +{ + u32 value; + std::memcpy(&value, data, sizeof(value)); + return value; +} + +class MoIterator +{ +public: + using iterator_category = std::random_access_iterator_tag; + using value_type = const char*; + using difference_type = s64; + using pointer = value_type; + using reference = value_type; + + explicit MoIterator(const char* data, u32 table_offset, u32 index = 0) + : m_data{data}, m_table_offset{table_offset}, m_index{index} + { + } + + // This is the actual underlying logic of accessing a Mo file. Patterned after the + // boost::iterator_facade library, which nicely separates out application logic from + // iterator-concept logic. + void advance(difference_type n) { m_index += n; } + difference_type distance_to(const MoIterator& other) const { return other.m_index - m_index; } + reference dereference() const + { + u32 offset = ReadU32(&m_data[m_table_offset + m_index * 8 + 4]); + return &m_data[offset]; + } + + // Needed for Iterator concept + reference operator*() const { return dereference(); } + MoIterator& operator++() + { + advance(1); + return *this; + } + + // Needed for InputIterator concept + bool operator==(const MoIterator& other) const { return distance_to(other) == 0; } + bool operator!=(const MoIterator& other) const { return !(*this == other); } + pointer operator->() const { return dereference(); } + MoIterator operator++(int) + { + MoIterator tmp(*this); + advance(1); + return tmp; + } + + // Needed for BidirectionalIterator concept + MoIterator& operator--() + { + advance(-1); + return *this; + } + MoIterator operator--(int) + { + MoIterator tmp(*this); + advance(-1); + return tmp; + } + + // Needed for RandomAccessIterator concept + bool operator<(const MoIterator& other) const { return distance_to(other) > 0; } + bool operator<=(const MoIterator& other) const { return distance_to(other) >= 0; } + bool operator>(const MoIterator& other) const { return distance_to(other) < 0; } + bool operator>=(const MoIterator& other) const { return distance_to(other) <= 0; } + reference operator[](difference_type n) const { return *(*this + n); } + MoIterator& operator+=(difference_type n) + { + advance(n); + return *this; + } + MoIterator& operator-=(difference_type n) + { + advance(-n); + return *this; + } + friend MoIterator operator+(difference_type n, const MoIterator& it) { return it + n; } + friend MoIterator operator+(const MoIterator& it, difference_type n) + { + MoIterator tmp(it); + tmp += n; + return tmp; + } + difference_type operator-(const MoIterator& other) const { return other.distance_to(*this); } + friend MoIterator operator-(difference_type n, const MoIterator& it) { return it - n; } + friend MoIterator operator-(const MoIterator& it, difference_type n) + { + MoIterator tmp(it); + tmp -= n; + return tmp; + } + +private: + const char* m_data; + u32 m_table_offset; + u32 m_index; +}; + +class MoFile +{ +public: + MoFile() = default; + explicit MoFile(const std::string& filename) + { + File::IOFile file(filename, "rb"); + m_data.resize(file.GetSize()); + file.ReadBytes(m_data.data(), m_data.size()); + + if (!file) + { + ERROR_LOG(COMMON, "Error reading MO file '%s'", filename.c_str()); + m_data = {}; + return; + } + + u32 magic = ReadU32(&m_data[0]); + if (magic != MO_MAGIC_NUMBER) + { + ERROR_LOG(COMMON, "MO file '%s' has bad magic number %x\n", filename.c_str(), magic); + m_data = {}; + return; + } + + u16 version_major = ReadU16(&m_data[4]); + if (version_major > 1) + { + ERROR_LOG(COMMON, "MO file '%s' has unsupported version number %i", filename.c_str(), + version_major); + m_data = {}; + return; + } + + m_number_of_strings = ReadU32(&m_data[8]); + m_offset_original_table = ReadU32(&m_data[12]); + m_offset_translation_table = ReadU32(&m_data[16]); + } + + u32 GetNumberOfStrings() const { return m_number_of_strings; } + const char* Translate(const char* original_string) const + { + const MoIterator begin(m_data.data(), m_offset_original_table); + const MoIterator end(m_data.data(), m_offset_original_table, m_number_of_strings); + auto iter = std::lower_bound(begin, end, original_string, + [](const char* a, const char* b) { return strcmp(a, b) < 0; }); + + if (strcmp(*iter, original_string) != 0) + return original_string; + + u32 offset = ReadU32(&m_data[m_offset_translation_table + std::distance(begin, iter) * 8 + 4]); + return &m_data[offset]; + } + +private: + std::vector m_data; + u32 m_number_of_strings = 0; + u32 m_offset_original_table = 0; + u32 m_offset_translation_table = 0; +}; + +class MoTranslator : public QTranslator +{ +public: + using QTranslator::QTranslator; + + bool isEmpty() const override { return m_mo_file.GetNumberOfStrings() == 0; } + bool load(const std::string& filename) + { + m_mo_file = MoFile(filename); + return !isEmpty(); + } + + QString translate(const char* context, const char* source_text, + const char* disambiguation = nullptr, int n = -1) const override + { + return QString::fromUtf8(m_mo_file.Translate(source_text)); + } + +private: + MoFile m_mo_file; +}; + +QStringList FindPossibleLanguageCodes(const QString& exact_language_code) +{ + QStringList possible_language_codes; + possible_language_codes << exact_language_code; + + // Qt likes to separate language, script, and country by hyphen, but on disk they're separated by + // underscores. + possible_language_codes.replaceInStrings(QStringLiteral("-"), QStringLiteral("_")); + + // Try successively dropping subtags (like the stock QTranslator, and as specified by RFC 4647 + // "Matching of Language Tags"). + // Example: fr_Latn_CA -> fr_Latn -> fr + for (auto lang : QStringList(possible_language_codes)) + { + while (lang.contains(QLatin1Char('_'))) + { + lang = lang.left(lang.lastIndexOf(QLatin1Char('_'))); + possible_language_codes << lang; + } + } + + // On macOS, Chinese (Simplified) and Chinese (Traditional) are represented as zh-Hans and + // zh-Hant, but on Linux they're represented as zh-CN and zh-TW. Qt should probably include the + // script subtags on Linux, but it doesn't. + if (possible_language_codes.contains(QStringLiteral("zh_Hans"))) + possible_language_codes << QStringLiteral("zh_CN"); + if (possible_language_codes.contains(QStringLiteral("zh_Hant"))) + possible_language_codes << QStringLiteral("zh_TW"); + + return possible_language_codes; +} + +static bool TryInstallTranslator(const QString& exact_language_code) +{ + for (const auto& qlang : FindPossibleLanguageCodes(exact_language_code)) + { + std::string lang = qlang.toStdString(); + auto filename = +#if defined _WIN32 + File::GetExeDirectory() + StringFromFormat("/Languages/%s/dolphin-emu.mo", lang.c_str()) +#elif defined __APPLE__ + File::GetBundleDirectory() + + StringFromFormat("/Contents/Resources/%s.lproj/dolphin-emu.mo", lang.c_str()) +#else + StringFromFormat(DATA_DIR "/../locale/%s/LC_MESSAGES/dolphin-emu.mo", lang.c_str()) +#endif + ; + + auto* translator = new MoTranslator(QApplication::instance()); + if (translator->load(filename)) + { + QApplication::instance()->installTranslator(translator); + return true; + } + translator->deleteLater(); + } + return false; +} + +void Translation::Initialize() +{ + // Hook up Dolphin internal translation + RegisterStringTranslator([](const char* text) { return QObject::tr(text).toStdString(); }); + + // Hook up Qt translations + auto& configured_language = SConfig::GetInstance().m_InterfaceLanguage; + if (!configured_language.empty()) + { + if (TryInstallTranslator(QString::fromStdString(configured_language))) + return; + + QMessageBox::warning( + nullptr, QObject::tr("Error"), + QObject::tr("Error loading selected language. Falling back to system default.")); + configured_language.clear(); + } + + for (const auto& lang : QLocale::system().uiLanguages()) + { + if (TryInstallTranslator(lang)) + break; + } +} diff --git a/Source/Core/DolphinQt2/Translation.h b/Source/Core/DolphinQt2/Translation.h new file mode 100644 index 0000000000..04118e3ffa --- /dev/null +++ b/Source/Core/DolphinQt2/Translation.h @@ -0,0 +1,10 @@ +// Copyright 2017 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +namespace Translation +{ +void Initialize(); +} diff --git a/Source/Core/DolphinWX/Config/InterfaceConfigPane.cpp b/Source/Core/DolphinWX/Config/InterfaceConfigPane.cpp index cef2b2a995..a455e9d013 100644 --- a/Source/Core/DolphinWX/Config/InterfaceConfigPane.cpp +++ b/Source/Core/DolphinWX/Config/InterfaceConfigPane.cpp @@ -36,11 +36,10 @@ static const std::array language_ids{{ "", - "ms", "ca", "cs", "da", "de", "en", "es", "fr", "hr", "it", "hu", "nl", - "nb", // wxWidgets won't accept "no" - "pl", "pt", "pt_BR", "ro", "sr", "sv", "tr", + "ms", "ca", "cs", "da", "de", "en", "es", "fr", "hr", "it", + "hu", "nl", "nb", "pl", "pt", "pt_BR", "ro", "sr", "sv", "tr", - "el", "ru", "ar", "fa", "ko", "ja", "zh_CN", "zh_TW", + "el", "ru", "ar", "fa", "ko", "ja", "zh_CN", "zh_TW", }}; InterfaceConfigPane::InterfaceConfigPane(wxWindow* parent, wxWindowID id) : wxPanel(parent, id)