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();
+}