// Copyright 2017 Dolphin Emulator Project // Licensed under GPLv2+ // Refer to the license.txt file included. #include "DolphinQt/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" #include "DolphinQt/QtUtils/ModalMessageBox.h" #include "UICommon/UICommon.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 static_cast(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) { WARN_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 (iter == end || 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 { if (disambiguation) { std::string combined_string = disambiguation; combined_string += '\4'; combined_string += source_text; return QString::fromUtf8(m_mo_file.Translate(combined_string.c_str())); } else { return QString::fromUtf8(m_mo_file.Translate(source_text)); } } private: MoFile m_mo_file; }; static 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. const int hans_index = possible_language_codes.indexOf(QStringLiteral("zh_Hans")); if (hans_index != -1) possible_language_codes.insert(hans_index + 1, QStringLiteral("zh_CN")); const int hant_index = possible_language_codes.indexOf(QStringLiteral("zh_Hant")); if (hant_index != -1) possible_language_codes.insert(hant_index + 1, 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()) #elif defined LINUX_LOCAL_DEV File::GetExeDirectory() + StringFromFormat("/../Source/Core/DolphinQt/%s/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); QLocale::setDefault(QLocale(exact_language_code)); UICommon::SetLocale(exact_language_code.toStdString()); return true; } translator->deleteLater(); } ERROR_LOG(COMMON, "No suitable translation file found"); return false; } void Translation::Initialize() { // Let the translation select the GUI directionality. This is intentionally excluded // from compilation, since all that matters is that xgettext sees it. We could use // QT_TRANSLATE_NOOP3 instead of tr, but that doesn't compile correctly when put // on a line of its own, so we would need to exclude it from compilation anyway. #if 0 QGuiApplication::tr( "QT_LAYOUT_DIRECTION", "Translate this string to the string 'LTR' in left-to-right languages or to 'RTL' in " "right-to-left languages (such as Hebrew and Arabic) to get proper widget layout."); #endif // Hook up Dolphin internal translation Common::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; ModalMessageBox::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; } }