diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 6fe5f0b7f..bd1aefa61 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1023,6 +1023,8 @@ PRIVATE iv/iv_delegate_impl.h iv/iv_instance.cpp iv/iv_instance.h + kotato/kotato_lang.cpp + kotato/kotato_lang.h lang/lang_cloud_manager.cpp lang/lang_cloud_manager.h lang/lang_instance.cpp diff --git a/Telegram/Resources/langs/rewrites/en.json b/Telegram/Resources/langs/rewrites/en.json new file mode 100644 index 000000000..e58768fe8 --- /dev/null +++ b/Telegram/Resources/langs/rewrites/en.json @@ -0,0 +1,3 @@ +{ + "dummy_last_string": "" +} diff --git a/Telegram/Resources/qrc/telegram/telegram.qrc b/Telegram/Resources/qrc/telegram/telegram.qrc index 7d5710aea..8a8a61953 100644 --- a/Telegram/Resources/qrc/telegram/telegram.qrc +++ b/Telegram/Resources/qrc/telegram/telegram.qrc @@ -60,4 +60,7 @@ ../../default_shortcuts-custom.json ../../../../lib/xdg/io.github.kotatogram.desktop + + ../../langs/rewrites/en.json + diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index 7b831c3a9..99ddef9ae 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "core/application.h" +#include "kotato/kotato_lang.h" #include "data/data_abstract_structure.h" #include "data/data_photo.h" #include "data/data_document.h" @@ -257,6 +258,7 @@ void Application::run() { style::internal::StartFonts(); ValidateScale(); + Kotato::Lang::Load(Lang::GetInstance().baseId(), Lang::GetInstance().id()); refreshGlobalProxy(); // Depends on app settings being read. diff --git a/Telegram/SourceFiles/kotato/kotato_lang.cpp b/Telegram/SourceFiles/kotato/kotato_lang.cpp new file mode 100644 index 000000000..edbec05f8 --- /dev/null +++ b/Telegram/SourceFiles/kotato/kotato_lang.cpp @@ -0,0 +1,294 @@ +/* +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 +#include +#include +#include + +namespace Kotato { +namespace Lang { +namespace { + +const auto kDefaultLanguage = qsl("en"); +const std::vector kPostfixes = { + "#zero", + "#one", + "#two", + "#few", + "#many", + "#other" +}; + +QString BaseLangCode; +QString LangCode; + +QMap DefaultValues; +QMap 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 diff --git a/Telegram/SourceFiles/kotato/kotato_lang.h b/Telegram/SourceFiles/kotato/kotato_lang.h new file mode 100644 index 000000000..195e25ae0 --- /dev/null +++ b/Telegram/SourceFiles/kotato/kotato_lang.h @@ -0,0 +1,166 @@ +/* +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 +*/ +#pragma once + +namespace Kotato { +namespace Lang { + +struct Var { + Var() {}; + Var(const QString &k, const QString &v) { + key = k; + value = v; + } + + QString key; + QString value; +}; + +struct EntVar { + EntVar() {}; + EntVar(const QString &k, TextWithEntities v) { + key = k; + value = v; + } + + QString key; + TextWithEntities value; +}; + +void Load(const QString &baseLangCode, const QString &langCode); + +QString Translate( + const QString &key, + Var var1 = Var(), + Var var2 = Var(), + Var var3 = Var(), + Var var4 = Var()); +QString Translate( + const QString &key, + float64 value, + Var var1 = Var(), + Var var2 = Var(), + Var var3 = Var(), + Var var4 = Var()); + +TextWithEntities TranslateWithEntities( + const QString &key, + EntVar var1 = EntVar(), + EntVar var2 = EntVar(), + EntVar var3 = EntVar(), + EntVar var4 = EntVar()); +TextWithEntities TranslateWithEntities( + const QString &key, + float64 value, + EntVar var1 = EntVar(), + EntVar var2 = EntVar(), + EntVar var3 = EntVar(), + EntVar var4 = EntVar()); + +rpl::producer<> Events(); + +} // namespace Lang +} // namespace Kotato + +// Shorthands + +inline QString ktr( + const QString &key, + ::Kotato::Lang::Var var1 = ::Kotato::Lang::Var(), + ::Kotato::Lang::Var var2 = ::Kotato::Lang::Var(), + ::Kotato::Lang::Var var3 = ::Kotato::Lang::Var(), + ::Kotato::Lang::Var var4 = ::Kotato::Lang::Var()) { + return ::Kotato::Lang::Translate(key, var1, var2, var3, var4); +} + +inline QString ktr( + const QString &key, + float64 value, + ::Kotato::Lang::Var var1 = ::Kotato::Lang::Var(), + ::Kotato::Lang::Var var2 = ::Kotato::Lang::Var(), + ::Kotato::Lang::Var var3 = ::Kotato::Lang::Var(), + ::Kotato::Lang::Var var4 = ::Kotato::Lang::Var()) { + return ::Kotato::Lang::Translate(key, value, var1, var2, var3, var4); +} + +inline TextWithEntities ktre( + const QString &key, + ::Kotato::Lang::EntVar var1 = ::Kotato::Lang::EntVar(), + ::Kotato::Lang::EntVar var2 = ::Kotato::Lang::EntVar(), + ::Kotato::Lang::EntVar var3 = ::Kotato::Lang::EntVar(), + ::Kotato::Lang::EntVar var4 = ::Kotato::Lang::EntVar()) { + return ::Kotato::Lang::TranslateWithEntities(key, var1, var2, var3, var4); +} + +inline TextWithEntities ktre( + const QString &key, + float64 value, + ::Kotato::Lang::EntVar var1 = ::Kotato::Lang::EntVar(), + ::Kotato::Lang::EntVar var2 = ::Kotato::Lang::EntVar(), + ::Kotato::Lang::EntVar var3 = ::Kotato::Lang::EntVar(), + ::Kotato::Lang::EntVar var4 = ::Kotato::Lang::EntVar()) { + return ::Kotato::Lang::TranslateWithEntities(key, value, var1, var2, var3, var4); +} + +inline rpl::producer rktr( + const QString &key, + ::Kotato::Lang::Var var1 = ::Kotato::Lang::Var(), + ::Kotato::Lang::Var var2 = ::Kotato::Lang::Var(), + ::Kotato::Lang::Var var3 = ::Kotato::Lang::Var(), + ::Kotato::Lang::Var var4 = ::Kotato::Lang::Var()) { + return rpl::single( + ::Kotato::Lang::Translate(key, var1, var2, var3, var4) + ) | rpl::then( + ::Kotato::Lang::Events() | rpl::map( + [=]{ return ::Kotato::Lang::Translate(key, var1, var2, var3, var4); }) + ); +} + +inline rpl::producer rktr( + const QString &key, + float64 value, + ::Kotato::Lang::Var var1 = ::Kotato::Lang::Var(), + ::Kotato::Lang::Var var2 = ::Kotato::Lang::Var(), + ::Kotato::Lang::Var var3 = ::Kotato::Lang::Var(), + ::Kotato::Lang::Var var4 = ::Kotato::Lang::Var()) { + return rpl::single( + ::Kotato::Lang::Translate(key, value, var1, var2, var3, var4) + ) | rpl::then( + ::Kotato::Lang::Events() | rpl::map( + [=]{ return ::Kotato::Lang::Translate(key, value, var1, var2, var3, var4); }) + ); +} + +inline rpl::producer rktre( + const QString &key, + ::Kotato::Lang::EntVar var1 = ::Kotato::Lang::EntVar(), + ::Kotato::Lang::EntVar var2 = ::Kotato::Lang::EntVar(), + ::Kotato::Lang::EntVar var3 = ::Kotato::Lang::EntVar(), + ::Kotato::Lang::EntVar var4 = ::Kotato::Lang::EntVar()) { + return rpl::single( + ::Kotato::Lang::TranslateWithEntities(key, var1, var2, var3, var4) + ) | rpl::then( + ::Kotato::Lang::Events() | rpl::map( + [=]{ return ::Kotato::Lang::TranslateWithEntities(key, var1, var2, var3, var4); }) + ); +} + +inline rpl::producer rktre( + const QString &key, + float64 value, + ::Kotato::Lang::EntVar var1 = ::Kotato::Lang::EntVar(), + ::Kotato::Lang::EntVar var2 = ::Kotato::Lang::EntVar(), + ::Kotato::Lang::EntVar var3 = ::Kotato::Lang::EntVar(), + ::Kotato::Lang::EntVar var4 = ::Kotato::Lang::EntVar()) { + return rpl::single( + ::Kotato::Lang::TranslateWithEntities(key, value, var1, var2, var3, var4) + ) | rpl::then( + ::Kotato::Lang::Events() | rpl::map( + [=]{ return ::Kotato::Lang::TranslateWithEntities(key, value, var1, var2, var3, var4); }) + ); +} diff --git a/Telegram/SourceFiles/lang/lang_cloud_manager.cpp b/Telegram/SourceFiles/lang/lang_cloud_manager.cpp index 8c98b96d1..c1d3c13fe 100644 --- a/Telegram/SourceFiles/lang/lang_cloud_manager.cpp +++ b/Telegram/SourceFiles/lang/lang_cloud_manager.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "lang/lang_cloud_manager.h" +#include "kotato/kotato_lang.h" #include "lang/lang_instance.h" #include "lang/lang_file_parser.h" #include "lang/lang_text_entity.h" @@ -447,6 +448,7 @@ void CloudManager::sendSwitchingToLanguageRequest() { const auto finalize = [=] { if (canApplyWithoutRestart(language.id)) { performSwitchAndAddToRecent(language); + Kotato::Lang::Load(Lang::GetInstance().baseId(), Lang::GetInstance().id()); } else { performSwitchAndRestart(language); } @@ -482,6 +484,7 @@ void CloudManager::switchToLanguage(const Language &data) { performSwitchToCustom(); } else if (canApplyWithoutRestart(data.id)) { performSwitchAndAddToRecent(data); + Kotato::Lang::Load(Lang::GetInstance().baseId(), Lang::GetInstance().id()); } else { QVector keys; keys.reserve(3); @@ -535,6 +538,7 @@ void CloudManager::performSwitchToCustom() { } if (canApplyWithoutRestart(u"#custom"_q)) { _langpack.switchToCustomFile(filePath); + Kotato::Lang::Load(Lang::GetInstance().baseId(), Lang::GetInstance().id()); } else { const auto values = loader.found(); const auto getValue = [&](ushort key) { diff --git a/Telegram/SourceFiles/lang/lang_tag.cpp b/Telegram/SourceFiles/lang/lang_tag.cpp index 7b1a10acb..513d5d90f 100644 --- a/Telegram/SourceFiles/lang/lang_tag.cpp +++ b/Telegram/SourceFiles/lang/lang_tag.cpp @@ -987,6 +987,35 @@ PluralResult Plural( return { shift, FormatDouble(value) }; } +ushort PluralShift(float64 value, bool isShortened) { + // To correctly select a shift for PluralType::Short + // we must first round the number. + const auto shortened = (isShortened) + ? FormatCountToShort(qRound(value)) + : ShortenedCount(); + + // Simplified. + const auto n = std::abs(shortened.number ? float64(shortened.number) : value); + const auto i = int(std::floor(n)); + const auto integer = (int(std::ceil(n)) == i); + const auto formatted = integer ? QString() : FormatDouble(n); + const auto dot = formatted.indexOf('.'); + const auto fraction = (dot >= 0) ? formatted.mid(dot + 1) : QString(); + const auto v = fraction.size(); + const auto w = v; + const auto f = NonZeroPartToInt(fraction); + const auto t = f; + + const auto useNonDefaultPlural = (ChoosePlural != ChoosePluralDefault); + return (useNonDefaultPlural ? ChoosePlural : ChoosePluralDefault)( + (integer ? i : -1), + i, + v, + w, + f, + t); +} + void UpdatePluralRules(const QString &languageId) { static auto kMap = GeneratePluralRulesMap(); auto parent = uint64(0); diff --git a/Telegram/SourceFiles/lang/lang_tag.h b/Telegram/SourceFiles/lang/lang_tag.h index 297ae1ff2..c252711ff 100644 --- a/Telegram/SourceFiles/lang/lang_tag.h +++ b/Telegram/SourceFiles/lang/lang_tag.h @@ -34,6 +34,7 @@ PluralResult Plural( ushort keyBase, float64 value, lngtag_count type); +ushort PluralShift(float64 value, bool isShortened = false); void UpdatePluralRules(const QString &languageId); template