mirror of
https://github.com/kotatogram/kotatogram-desktop
synced 2025-08-26 12:27:12 +00:00
295 lines
7.7 KiB
C++
295 lines
7.7 KiB
C++
|
/*
|
||
|
This file is part of Kotatogram Desktop,
|
||
|
the unofficial app based on Telegram Desktop.
|
||
|
|
||
|
For license and copyright information please follow this link:
|
||
|
https://github.com/kotatogram/kotatogram-desktop/blob/dev/LEGAL
|
||
|
*/
|
||
|
#include "kotato/kotato_lang.h"
|
||
|
|
||
|
#include "base/parse_helper.h"
|
||
|
#include "lang/lang_tag.h"
|
||
|
|
||
|
#include <QtCore/QJsonDocument>
|
||
|
#include <QtCore/QJsonObject>
|
||
|
#include <QtCore/QJsonArray>
|
||
|
#include <QtCore/QDir>
|
||
|
|
||
|
namespace Kotato {
|
||
|
namespace Lang {
|
||
|
namespace {
|
||
|
|
||
|
const auto kDefaultLanguage = qsl("en");
|
||
|
const std::vector<QString> kPostfixes = {
|
||
|
"#zero",
|
||
|
"#one",
|
||
|
"#two",
|
||
|
"#few",
|
||
|
"#many",
|
||
|
"#other"
|
||
|
};
|
||
|
|
||
|
QString BaseLangCode;
|
||
|
QString LangCode;
|
||
|
|
||
|
QMap<QString, QString> DefaultValues;
|
||
|
QMap<QString, QString> CurrentValues;
|
||
|
|
||
|
rpl::event_stream<> LangChanges;
|
||
|
|
||
|
QString LangDir() {
|
||
|
return cWorkingDir() + "tdata/ktg_lang/";
|
||
|
}
|
||
|
|
||
|
void ParseLanguageData(
|
||
|
const QString &langCode,
|
||
|
bool isDefault) {
|
||
|
const auto filename = isDefault
|
||
|
? qsl(":/ktg_lang/%1.json").arg(langCode)
|
||
|
: LangDir() + (qsl("%1.json").arg(langCode));
|
||
|
|
||
|
QFile file(filename);
|
||
|
if (!file.exists()) {
|
||
|
return;
|
||
|
}
|
||
|
if (!file.open(QIODevice::ReadOnly)) {
|
||
|
LOG(("Kotato::Lang Info: file %1 could not be read.").arg(filename));
|
||
|
return;
|
||
|
}
|
||
|
auto error = QJsonParseError{ 0, QJsonParseError::NoError };
|
||
|
const auto document = QJsonDocument::fromJson(
|
||
|
base::parse::stripComments(file.readAll()),
|
||
|
&error);
|
||
|
file.close();
|
||
|
|
||
|
if (error.error != QJsonParseError::NoError) {
|
||
|
LOG(("Kotato::Lang Info: file %1 has failed to parse. Error: %2"
|
||
|
).arg(filename
|
||
|
).arg(error.errorString()));
|
||
|
return;
|
||
|
} else if (!document.isObject()) {
|
||
|
LOG(("Kotato::Lang Info: file %1 has failed to parse. Error: object expected"
|
||
|
).arg(filename));
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const auto applyValue = [&](const QString &name, const QString &translation) {
|
||
|
if (langCode == kDefaultLanguage) {
|
||
|
DefaultValues.insert(name, translation);
|
||
|
} else {
|
||
|
CurrentValues.insert(name, translation);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const auto langKeys = document.object();
|
||
|
const auto keyList = langKeys.keys();
|
||
|
|
||
|
for (auto i = keyList.constBegin(), e = keyList.constEnd(); i != e; ++i) {
|
||
|
const auto key = *i;
|
||
|
if (key.startsWith("dummy_")) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
const auto value = langKeys.constFind(key);
|
||
|
|
||
|
if ((*value).isString()) {
|
||
|
|
||
|
applyValue(key, (*value).toString());
|
||
|
|
||
|
} else if ((*value).isObject()) {
|
||
|
|
||
|
const auto keyPlurals = (*value).toObject();
|
||
|
const auto pluralList = keyPlurals.keys();
|
||
|
|
||
|
for (auto pli = pluralList.constBegin(), ple = pluralList.constEnd(); pli != ple; ++pli) {
|
||
|
const auto plural = *pli;
|
||
|
const auto pluralValue = keyPlurals.constFind(plural);
|
||
|
|
||
|
if (!(*pluralValue).isString()) {
|
||
|
LOG(("Kotato::Lang Info: wrong value for key %1 in %2 in file %3, string expected")
|
||
|
.arg(plural).arg(key).arg(filename));
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
const auto name = QString(key + "#" + plural);
|
||
|
const auto translation = (*pluralValue).toString();
|
||
|
|
||
|
applyValue(name, translation);
|
||
|
}
|
||
|
} else {
|
||
|
LOG(("Kotato::Lang Info: wrong value for key %1 in file %2, string or object expected")
|
||
|
.arg(key).arg(filename));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void UnpackDefault() {
|
||
|
const auto langDir = LangDir();
|
||
|
if (!QDir().exists(langDir)) QDir().mkpath(langDir);
|
||
|
|
||
|
const auto langs = QDir(":/ktg_lang").entryList(QStringList() << "*.json", QDir::Files);
|
||
|
auto neededLangs = QStringList() << kDefaultLanguage << LangCode << BaseLangCode;
|
||
|
neededLangs.removeDuplicates();
|
||
|
|
||
|
for (auto language : langs) {
|
||
|
language.chop(5);
|
||
|
if (!neededLangs.contains(language)) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
const auto path = langDir + language + ".default.json";
|
||
|
auto input = QFile(qsl(":/ktg_lang/%1.json").arg(language));
|
||
|
auto output = QFile(path);
|
||
|
if (input.open(QIODevice::ReadOnly)) {
|
||
|
auto inputData = input.readAll();
|
||
|
if (output.open(QIODevice::WriteOnly)) {
|
||
|
output.write(inputData);
|
||
|
output.close();
|
||
|
}
|
||
|
input.close();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
} // namespace
|
||
|
|
||
|
void Load(const QString &baseLangCode, const QString &langCode) {
|
||
|
BaseLangCode = baseLangCode;
|
||
|
if (BaseLangCode.endsWith("-raw")) {
|
||
|
BaseLangCode.chop(4);
|
||
|
}
|
||
|
|
||
|
LangCode = langCode.isEmpty() ? baseLangCode : langCode;
|
||
|
if (LangCode.endsWith("-raw")) {
|
||
|
LangCode.chop(4);
|
||
|
}
|
||
|
|
||
|
DefaultValues.clear();
|
||
|
CurrentValues.clear();
|
||
|
|
||
|
if (BaseLangCode != kDefaultLanguage) {
|
||
|
ParseLanguageData(kDefaultLanguage, true);
|
||
|
ParseLanguageData(kDefaultLanguage, false);
|
||
|
}
|
||
|
|
||
|
ParseLanguageData(BaseLangCode, true);
|
||
|
ParseLanguageData(BaseLangCode, false);
|
||
|
|
||
|
if (LangCode != BaseLangCode) {
|
||
|
ParseLanguageData(LangCode, true);
|
||
|
ParseLanguageData(LangCode, false);
|
||
|
}
|
||
|
|
||
|
UnpackDefault();
|
||
|
LangChanges.fire({});
|
||
|
}
|
||
|
|
||
|
QString Translate(const QString &key, Var var1, Var var2, Var var3, Var var4) {
|
||
|
auto phrase = (CurrentValues.contains(key) && !CurrentValues.value(key).isEmpty())
|
||
|
? CurrentValues.value(key)
|
||
|
: DefaultValues.value(key);
|
||
|
|
||
|
for (const auto &v : { var1, var2, var3, var4 }) {
|
||
|
if (!v.key.isEmpty()) {
|
||
|
auto skipNext = false;
|
||
|
const auto key = qsl("{%1}").arg(v.key);
|
||
|
const auto neededLength = phrase.length() - key.length();
|
||
|
for (auto i = 0; i <= neededLength; i++) {
|
||
|
if (skipNext) {
|
||
|
skipNext = false;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (phrase.at(i) == QChar('\\')) {
|
||
|
skipNext = true;
|
||
|
} else if (phrase.at(i) == QChar('{') && phrase.mid(i, key.length()) == key) {
|
||
|
phrase.replace(i, key.length(), v.value);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return phrase;
|
||
|
}
|
||
|
|
||
|
QString Translate(const QString &key, float64 value, Var var1, Var var2, Var var3, Var var4) {
|
||
|
const auto shift = ::Lang::PluralShift(value);
|
||
|
return Translate(key + kPostfixes.at(shift), var1, var2, var3);
|
||
|
}
|
||
|
|
||
|
TextWithEntities TranslateWithEntities(const QString &key, EntVar var1, EntVar var2, EntVar var3, EntVar var4) {
|
||
|
TextWithEntities phrase = {
|
||
|
(CurrentValues.contains(key) && !CurrentValues.value(key).isEmpty())
|
||
|
? CurrentValues.value(key)
|
||
|
: DefaultValues.value(key)
|
||
|
};
|
||
|
|
||
|
for (const auto &v : { var1, var2, var3, var4 }) {
|
||
|
if (!v.key.isEmpty()) {
|
||
|
auto skipNext = false;
|
||
|
const auto key = qsl("{%1}").arg(v.key);
|
||
|
const auto neededLength = phrase.text.length() - key.length();
|
||
|
for (auto i = 0; i <= neededLength; i++) {
|
||
|
if (skipNext) {
|
||
|
skipNext = false;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (phrase.text.at(i) == QChar('\\')) {
|
||
|
skipNext = true;
|
||
|
} else if (phrase.text.at(i) == QChar('{') && phrase.text.mid(i, key.length()) == key) {
|
||
|
phrase.text.replace(i, key.length(), v.value.text);
|
||
|
const auto endOld = i + key.length();
|
||
|
const auto endNew = i + v.value.text.length();
|
||
|
|
||
|
// Shift existing entities
|
||
|
if (endNew > endOld) {
|
||
|
const auto diff = endNew - endOld;
|
||
|
for (auto &entity : phrase.entities) {
|
||
|
if (entity.offset() > endOld) {
|
||
|
entity.shiftRight(diff);
|
||
|
} else if (entity.offset() <= i && entity.offset() + entity.length() >= endOld) {
|
||
|
entity.extendToRight(diff);
|
||
|
}
|
||
|
}
|
||
|
} else if (endNew < endOld) {
|
||
|
const auto diff = endOld - endNew;
|
||
|
for (auto &entity : phrase.entities) {
|
||
|
if (entity.offset() > endNew) {
|
||
|
entity.shiftLeft(diff);
|
||
|
} else if (entity.offset() <= i && entity.offset() + entity.length() >= endNew) {
|
||
|
entity.shrinkFromRight(diff);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Add new entities
|
||
|
for (auto entity : v.value.entities) {
|
||
|
phrase.entities.append(EntityInText(
|
||
|
entity.type(),
|
||
|
entity.offset() + i,
|
||
|
entity.length(),
|
||
|
entity.data()));
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return phrase;
|
||
|
}
|
||
|
|
||
|
TextWithEntities TranslateWithEntities(const QString &key, float64 value, EntVar var1, EntVar var2, EntVar var3, EntVar var4) {
|
||
|
const auto shift = ::Lang::PluralShift(value);
|
||
|
return TranslateWithEntities(key + kPostfixes.at(shift), var1, var2, var3, var4);
|
||
|
}
|
||
|
|
||
|
rpl::producer<> Events() {
|
||
|
return LangChanges.events();
|
||
|
}
|
||
|
|
||
|
} // namespace Lang
|
||
|
} // namespace Kotato
|