diff options
Diffstat (limited to 'meshmc/launcher/translations/POTranslator.cpp')
| -rw-r--r-- | meshmc/launcher/translations/POTranslator.cpp | 351 |
1 files changed, 351 insertions, 0 deletions
diff --git a/meshmc/launcher/translations/POTranslator.cpp b/meshmc/launcher/translations/POTranslator.cpp new file mode 100644 index 0000000000..397657451c --- /dev/null +++ b/meshmc/launcher/translations/POTranslator.cpp @@ -0,0 +1,351 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "POTranslator.h" + +#include <QDebug> +#include "FileSystem.h" + +struct POEntry { + QString text; + bool fuzzy; +}; + +struct POTranslatorPrivate { + QString filename; + QHash<QByteArray, POEntry> mapping; + QHash<QByteArray, POEntry> mapping_disambiguatrion; + bool loaded = false; + + void reload(); +}; + +class ParserArray : public QByteArray +{ + public: + ParserArray(const QByteArray& in) : QByteArray(in) {} + bool chomp(const char* data, int length) + { + if (startsWith(data)) { + remove(0, length); + return true; + } + return false; + } + bool chompString(QByteArray& appendHere) + { + QByteArray msg; + bool escape = false; + if (size() < 2) { + qDebug() << "String fragment is too short"; + return false; + } + if (!startsWith('"')) { + qDebug() << "String fragment does not start with \""; + return false; + } + if (!endsWith('"')) { + qDebug() + << "String fragment does not end with \", instead, there is" + << at(size() - 1); + return false; + } + for (int i = 1; i < size() - 1; i++) { + char c = operator[](i); + if (escape) { + switch (c) { + case 'r': + msg += '\r'; + break; + case 'n': + msg += '\n'; + break; + case 't': + msg += '\t'; + break; + case 'v': + msg += '\v'; + break; + case 'a': + msg += '\a'; + break; + case 'b': + msg += '\b'; + break; + case 'f': + msg += '\f'; + break; + case '"': + msg += '"'; + break; + case '\\': + msg.append('\\'); + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': { + int octal_start = i; + while ((c = operator[](i)) >= '0' && c <= '7') { + i++; + if (i == length() - 1) { + qDebug() << "Something went bad while parsing " + "an octal escape string..."; + return false; + } + } + msg += mid(octal_start, i - octal_start).toUInt(0, 8); + break; + } + case 'x': { + // chomp the 'x' + i++; + int hex_start = i; + while (isxdigit(operator[](i))) { + i++; + if (i == length() - 1) { + qDebug() << "Something went bad while parsing " + "a hex escape string..."; + return false; + } + } + msg += mid(hex_start, i - hex_start).toUInt(0, 16); + break; + } + default: { + qDebug() << "Invalid escape sequence character:" << c; + return false; + } + } + escape = false; + } else if (c == '\\') { + escape = true; + } else { + msg += c; + } + } + if (escape) { + qDebug() << "Unterminated escape sequence..."; + return false; + } + appendHere += msg; + return true; + } +}; + +void POTranslatorPrivate::reload() +{ + QFile file(filename); + if (!file.open(QFile::OpenMode::enum_type::ReadOnly | + QFile::OpenMode::enum_type::Text)) { + qDebug() << "Failed to open PO file:" << filename; + return; + } + + QByteArray context; + QByteArray disambiguation; + QByteArray id; + QByteArray str; + bool fuzzy = false; + bool nextFuzzy = false; + + enum class Mode { + First, + MessageContext, + MessageId, + MessageString + } mode = Mode::First; + + int lineNumber = 0; + QHash<QByteArray, POEntry> newMapping; + QHash<QByteArray, POEntry> newMapping_disambiguation; + auto endEntry = [&]() { + auto strStr = QString::fromUtf8(str); + // NOTE: PO header has empty id. We skip it. + if (!id.isEmpty()) { + auto normalKey = context + "|" + id; + newMapping.insert(normalKey, {strStr, fuzzy}); + if (!disambiguation.isEmpty()) { + auto disambiguationKey = + context + "|" + id + "@" + disambiguation; + newMapping_disambiguation.insert(disambiguationKey, + {strStr, fuzzy}); + } + } + context.clear(); + disambiguation.clear(); + id.clear(); + str.clear(); + fuzzy = nextFuzzy; + nextFuzzy = false; + }; + while (!file.atEnd()) { + ParserArray line = file.readLine(); + if (line.endsWith('\n')) { + line.resize(line.size() - 1); + } + if (line.endsWith('\r')) { + line.resize(line.size() - 1); + } + + if (!line.size()) { + // NIL + } else if (line[0] == '#') { + if (line.contains(", fuzzy")) { + nextFuzzy = true; + } + } else if (line.startsWith('"')) { + QByteArray temp; + QByteArray* out = &temp; + + switch (mode) { + case Mode::First: + qDebug() << "Unexpected escaped string during initial " + "state... line:" + << lineNumber; + return; + case Mode::MessageString: + out = &str; + break; + case Mode::MessageContext: + out = &context; + break; + case Mode::MessageId: + out = &id; + break; + } + if (!line.chompString(*out)) { + qDebug() << "Badly formatted string on line:" << lineNumber; + return; + } + } else if (line.chomp("msgctxt ", 8)) { + switch (mode) { + case Mode::First: + break; + case Mode::MessageString: + endEntry(); + break; + case Mode::MessageContext: + case Mode::MessageId: + qDebug() << "Unexpected msgctxt line:" << lineNumber; + return; + } + if (line.chompString(context)) { + auto parts = context.split('|'); + context = parts[0]; + if (parts.size() > 1 && !parts[1].isEmpty()) { + disambiguation = parts[1]; + } + mode = Mode::MessageContext; + } + } else if (line.chomp("msgid ", 6)) { + switch (mode) { + case Mode::MessageContext: + case Mode::First: + break; + case Mode::MessageString: + endEntry(); + break; + case Mode::MessageId: + qDebug() << "Unexpected msgid line:" << lineNumber; + return; + } + if (line.chompString(id)) { + mode = Mode::MessageId; + } + } else if (line.chomp("msgstr ", 7)) { + switch (mode) { + case Mode::First: + case Mode::MessageString: + case Mode::MessageContext: + qDebug() << "Unexpected msgstr line:" << lineNumber; + return; + case Mode::MessageId: + break; + } + if (line.chompString(str)) { + mode = Mode::MessageString; + } + } else { + qDebug() << "I did not understand line: " << lineNumber << ":" + << QString::fromUtf8(line); + } + lineNumber++; + } + endEntry(); + mapping = std::move(newMapping); + mapping_disambiguatrion = std::move(newMapping_disambiguation); + loaded = true; +} + +POTranslator::POTranslator(const QString& filename, QObject* parent) + : QTranslator(parent) +{ + d = new POTranslatorPrivate; + d->filename = filename; + d->reload(); +} + +QString POTranslator::translate(const char* context, const char* sourceText, + const char* disambiguation, int n) const +{ + if (disambiguation) { + auto disambiguationKey = QByteArray(context) + "|" + + QByteArray(sourceText) + "@" + + QByteArray(disambiguation); + auto iter = d->mapping_disambiguatrion.find(disambiguationKey); + if (iter != d->mapping_disambiguatrion.end()) { + auto& entry = *iter; + if (entry.text.isEmpty()) { + qDebug() << "Translation entry has no content:" + << disambiguationKey; + } + if (entry.fuzzy) { + qDebug() << "Translation entry is fuzzy:" << disambiguationKey + << "->" << entry.text; + } + return entry.text; + } + } + auto key = QByteArray(context) + "|" + QByteArray(sourceText); + auto iter = d->mapping.find(key); + if (iter != d->mapping.end()) { + auto& entry = *iter; + if (entry.text.isEmpty()) { + qDebug() << "Translation entry has no content:" << key; + } + if (entry.fuzzy) { + qDebug() << "Translation entry is fuzzy:" << key << "->" + << entry.text; + } + return entry.text; + } + return QString(); +} + +bool POTranslator::isEmpty() const +{ + return !d->loaded; +} |
