From 60cba5722cae678ccd65501bb2f9336d682ced7a Mon Sep 17 00:00:00 2001 From: Michael M Date: Mon, 17 Jul 2017 14:50:40 -0700 Subject: [PATCH] Qt: use translations --- Source/Core/DolphinQt2/CMakeLists.txt | 34 +++ Source/Core/DolphinQt2/DolphinQt2.vcxproj | 2 + Source/Core/DolphinQt2/Main.cpp | 4 + Source/Core/DolphinQt2/Translation.cpp | 298 ++++++++++++++++++++++ Source/Core/DolphinQt2/Translation.h | 10 + 5 files changed, 348 insertions(+) create mode 100644 Source/Core/DolphinQt2/Translation.cpp create mode 100644 Source/Core/DolphinQt2/Translation.h 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/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(); +}