MultiMC5/api/logic/translations/POTranslator.cpp
Petr Mrázek 4b7971f60f NOISSUE hotloading of translations and use of local PO files
The hotloading is still inefficient
2019-01-02 01:41:07 +01:00

374 lines
10 KiB
C++

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