Qt: use translations

This commit is contained in:
Michael M 2017-07-17 14:50:40 -07:00
parent bdcd6df459
commit 60cba5722c
5 changed files with 348 additions and 0 deletions

View File

@ -28,6 +28,7 @@ set(SRCS
Resources.cpp Resources.cpp
Settings.cpp Settings.cpp
ToolBar.cpp ToolBar.cpp
Translation.cpp
WiiUpdate.cpp WiiUpdate.cpp
WiiUpdate.h WiiUpdate.h
Config/ControllersWindow.cpp Config/ControllersWindow.cpp
@ -99,6 +100,39 @@ set(DOLPHINQT2_BINARY dolphin-emu-qt2)
add_executable(${DOLPHINQT2_BINARY} ${SRCS} ${UI_HEADERS}) add_executable(${DOLPHINQT2_BINARY} ${SRCS} ${UI_HEADERS})
target_link_libraries(${DOLPHINQT2_BINARY} ${LIBS} Qt5::Widgets) 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) if(APPLE)
# Note: This is copied from DolphinQt, based on the DolphinWX version. # Note: This is copied from DolphinQt, based on the DolphinWX version.

View File

@ -218,6 +218,7 @@
<ClCompile Include="Settings\InterfacePane.cpp" /> <ClCompile Include="Settings\InterfacePane.cpp" />
<ClCompile Include="Settings\PathPane.cpp" /> <ClCompile Include="Settings\PathPane.cpp" />
<ClCompile Include="ToolBar.cpp" /> <ClCompile Include="ToolBar.cpp" />
<ClCompile Include="Translation.cpp" />
<ClCompile Include="WiiUpdate.cpp" /> <ClCompile Include="WiiUpdate.cpp" />
</ItemGroup> </ItemGroup>
<!--Put standard C/C++ headers here--> <!--Put standard C/C++ headers here-->
@ -243,6 +244,7 @@
<ClInclude Include="QtUtils\ListTabWidget.h" /> <ClInclude Include="QtUtils\ListTabWidget.h" />
<ClInclude Include="Resources.h" /> <ClInclude Include="Resources.h" />
<ClInclude Include="Settings\PathPane.h" /> <ClInclude Include="Settings\PathPane.h" />
<ClInclude Include="Translation.h" />
<ClInclude Include="WiiUpdate.h" /> <ClInclude Include="WiiUpdate.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -18,6 +18,7 @@
#include "DolphinQt2/QtUtils/RunOnObject.h" #include "DolphinQt2/QtUtils/RunOnObject.h"
#include "DolphinQt2/Resources.h" #include "DolphinQt2/Resources.h"
#include "DolphinQt2/Settings.h" #include "DolphinQt2/Settings.h"
#include "DolphinQt2/Translation.h"
#include "UICommon/CommandLineParse.h" #include "UICommon/CommandLineParse.h"
#include "UICommon/UICommon.h" #include "UICommon/UICommon.h"
@ -75,6 +76,9 @@ int main(int argc, char* argv[])
// Hook up alerts from core // Hook up alerts from core
RegisterMsgAlertHandler(QtMsgAlertHandler); RegisterMsgAlertHandler(QtMsgAlertHandler);
// Hook up translations
Translation::Initialize();
// Whenever the event loop is about to go to sleep, dispatch the jobs // Whenever the event loop is about to go to sleep, dispatch the jobs
// queued in the Core first. // queued in the Core first.
QObject::connect(QAbstractEventDispatcher::instance(), &QAbstractEventDispatcher::aboutToBlock, QObject::connect(QAbstractEventDispatcher::instance(), &QAbstractEventDispatcher::aboutToBlock,

View File

@ -0,0 +1,298 @@
// Copyright 2017 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#include "DolphinQt2/Translation.h"
#include <QApplication>
#include <QLocale>
#include <QMessageBox>
#include <QTranslator>
#include <algorithm>
#include <cstring>
#include <iterator>
#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<char> 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;
}
}

View File

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