Telegram Desktop uses static language system with code generation, which requires almost full app rebuild on any string modification, no matter how small it is. Since Kotatogram is options-driven, static language system will slow the development. This language system solves the problem by using JSON and runtime string search instead.
		
			
				
	
	
		
			294 lines
		
	
	
	
		
			7.7 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			294 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
 |