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