diff --git a/Telegram/Resources/langs/rewrites/ru.json b/Telegram/Resources/langs/rewrites/ru.json
new file mode 100644
index 000000000..91471f245
--- /dev/null
+++ b/Telegram/Resources/langs/rewrites/ru.json
@@ -0,0 +1,92 @@
+// This file is used to override translation in Kotatogram.
+// Please note that changes here won't be saved, because
+// this file is bundled into application.
+// Suggest any new translations or edits for existing here:
+// https://github.com/kotatogram/kotatogram-desktop/issues
+
+[
+ {
+ "key": "ktg_intro_about",
+ "value": "Добро пожаловать в неофициальный мессенджер\nна основе Telegram Desktop."
+ },
+ {
+ "key": "ktg_about_text1",
+ "value": "Экспериментальный неофициальный форк {tdesktop_link}."
+ },
+ {
+ "key": "ktg_about_text3",
+ "value": "Посетите {channel_link} или {faq_link} (англ.), чтобы узнать больше."
+ },
+ {
+ "key": "ktg_about_text3_channel",
+ "value": "канал Kotatogram"
+ },
+ {
+ "key": "ktg_copy_btn_callback",
+ "value": "Копировать callback-данные"
+ },
+ {
+ "key": "ktg_profile_copy_id",
+ "value": "Копировать ID"
+ },
+ {
+ "key": "ktg_profile_bot_id",
+ "value": "ID бота"
+ },
+ {
+ "key": "ktg_profile_user_id",
+ "value": "ID пользователя"
+ },
+ {
+ "key": "ktg_profile_group_id",
+ "value": "ID группы"
+ },
+ {
+ "key": "ktg_profile_supergroup_id",
+ "value": "ID супергруппы"
+ },
+ {
+ "key": "ktg_profile_channel_id",
+ "value": "ID канала"
+ },
+ {
+ "key": "ktg_pinned_message_show",
+ "value": "Показать закреплённое сообщение"
+ },
+ {
+ "key": "ktg_pinned_message_hide",
+ "value": "Скрыть закреплённое сообщение"
+ },
+ {
+ "key": "ktg_restricted_send_message_until",
+ "value": "Администраторы группы запретили вам писать в ней до {date}, {time}."
+ },
+ {
+ "key": "ktg_restricted_send_media_until",
+ "value": "Администраторы группы запретили вам публиковать здесь фото и видео до {date}, {time}."
+ },
+ {
+ "key": "ktg_restricted_send_stickers_until",
+ "value": "Администраторы группы запретили вам публиковать здесь стикеры до {date}, {time}."
+ },
+ {
+ "key": "ktg_restricted_send_gifs_until",
+ "value": "Администраторы группы запретили вам отправлять в нее файлы GIF до {date}, {time}."
+ },
+ {
+ "key": "ktg_restricted_send_inline_until",
+ "value": "Администраторы группы запретили вам отправлять сообщения с помощью ботов до {date}, {time}."
+ },
+ {
+ "key": "ktg_restricted_send_polls_until",
+ "value": "Администраторы группы запретили Вам публиковать в ней опросы до {date}, {time}."
+ },
+ {
+ "key": "ktg_settings_show_json_settings",
+ "value": "Показать файл настроек"
+ },
+ {
+ "key": "ktg_settings_restart",
+ "value": "Перезапустить Kotatogram"
+ }
+]
\ No newline at end of file
diff --git a/Telegram/Resources/qrc/telegram/telegram.qrc b/Telegram/Resources/qrc/telegram/telegram.qrc
index 25ab0b68a..f197964ad 100644
--- a/Telegram/Resources/qrc/telegram/telegram.qrc
+++ b/Telegram/Resources/qrc/telegram/telegram.qrc
@@ -58,4 +58,7 @@
../../default_shortcuts-custom.json
../../default_kotato-settings-custom.json
+
+ ../../langs/rewrites/ru.json
+
diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp
index e3d24bfa5..a03246834 100644
--- a/Telegram/SourceFiles/core/application.cpp
+++ b/Telegram/SourceFiles/core/application.cpp
@@ -183,6 +183,8 @@ void Application::run() {
refreshGlobalProxy(); // Depends on Global::started().
startLocalStorage();
+ Lang::Current().fillDefaultJson();
+ Lang::Current().fillFromJson();
ValidateScale();
if (Local::oldSettingsVersion() < AppVersion) {
diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp
index 2435a1631..8cac56980 100644
--- a/Telegram/SourceFiles/data/data_channel.cpp
+++ b/Telegram/SourceFiles/data/data_channel.cpp
@@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "data/data_folder.h"
#include "data/data_location.h"
+#include "lang/lang_keys.h"
#include "base/unixtime.h"
#include "history/history.h"
#include "observer_peer.h"
diff --git a/Telegram/SourceFiles/lang/lang_instance.cpp b/Telegram/SourceFiles/lang/lang_instance.cpp
index f6177f0fd..e8767866c 100644
--- a/Telegram/SourceFiles/lang/lang_instance.cpp
+++ b/Telegram/SourceFiles/lang/lang_instance.cpp
@@ -14,6 +14,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/confirm_box.h"
#include "lang/lang_file_parser.h"
#include "base/qthelp_regex.h"
+#include "base/parse_helper.h"
+
+#include
+#include
+#include
+#include
namespace Lang {
namespace {
@@ -552,6 +558,100 @@ void Instance::fillFromSerialized(
_idChanges.fire_copy(_id);
}
+QString Instance::jsonLangDir() {
+ return cWorkingDir() + "tdata/ktg_lang/";
+}
+
+void Instance::fillDefaultJson() {
+ if (!QDir().exists(jsonLangDir())) QDir().mkpath(jsonLangDir());
+
+ const auto path = jsonLangDir() + "ru.json";
+ const auto pathRaw = jsonLangDir() + "ru-raw.json";
+ auto input = QFile(":/ktg_lang/ru.json");
+ auto output = QFile(path);
+ auto outputRaw = QFile(pathRaw);
+ if (input.open(QIODevice::ReadOnly)) {
+ auto inputData = input.readAll();
+ if (output.open(QIODevice::WriteOnly)) {
+ output.write(inputData);
+ output.close();
+ }
+
+ if (outputRaw.open(QIODevice::WriteOnly)) {
+ outputRaw.write(inputData);
+ outputRaw.close();
+ }
+ input.close();
+ }
+}
+
+void Instance::fillFromJson() {
+ if (id() != baseId()) {
+ const auto langBaseDir = jsonLangDir() + (qsl("%1.json").arg(baseId()));
+ loadFromJson(langBaseDir);
+ }
+
+ const auto langCustomDir = jsonLangDir() + (qsl("%1.json").arg(id()));
+ loadFromJson(langCustomDir);
+
+ _idChanges.fire_copy(_id);
+}
+
+void Instance::loadFromJson(const QString &filename) {
+ QFile file(filename);
+ if (!file.exists()) {
+ return;
+ }
+ if (!file.open(QIODevice::ReadOnly)) {
+ LOG(("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(("Lang Info: file %1 has failed to parse. Error: %2"
+ ).arg(filename
+ ).arg(error.errorString()));
+ return;
+ } else if (!document.isArray()) {
+ LOG(("Lang Info: file %1 has failed to parse. Error: array expected"
+ ).arg(filename));
+ return;
+ }
+ const auto langKeys = document.array();
+ auto limit = kLangValuesLimit;
+ for (auto i = langKeys.constBegin(), e = langKeys.constEnd(); i != e; ++i) {
+ if (!(*i).isObject()) {
+ LOG(("Lang Info: Bad entry in file %1, object expected"
+ ).arg(filename));
+ continue;
+ }
+ const auto entry = (*i).toObject();
+ const auto key = entry.constFind(qsl("key"));
+ const auto value = entry.constFind(qsl("value"));
+ if (key == entry.constEnd()
+ || value == entry.constEnd()
+ || !(*key).isString()
+ || !(*value).isString()) {
+ LOG(("Lang Info: Bad entry in file %1, %2 expected"
+ ).arg(filename).arg("{\"key\": \"...\", \"value\": \"...\" }"
+ ));
+ } else {
+ const auto name = QByteArray().append((*key).toString());
+ const auto translation = QByteArray().append((*value).toString());
+
+ applyValue(name, translation);
+ }
+ if (!--limit) {
+ break;
+ }
+ }
+}
+
void Instance::loadFromContent(const QByteArray &content) {
Lang::FileParser loader(content, [this](QLatin1String key, const QByteArray &value) {
applyValue(QByteArray(key.data(), key.size()), value);
diff --git a/Telegram/SourceFiles/lang/lang_instance.h b/Telegram/SourceFiles/lang/lang_instance.h
index 10c5c69b4..50617e8da 100644
--- a/Telegram/SourceFiles/lang/lang_instance.h
+++ b/Telegram/SourceFiles/lang/lang_instance.h
@@ -73,6 +73,9 @@ public:
QByteArray serialize() const;
void fillFromSerialized(const QByteArray &data, int dataAppVersion);
+ QString jsonLangDir();
+ void fillDefaultJson();
+ void fillFromJson();
void applyDifference(
Pack pack,
@@ -112,6 +115,7 @@ private:
const QString &absolutePath,
const QString &relativePath,
const QByteArray &content);
+ void loadFromJson(const QString &filename);
bool loadFromCustomFile(const QString &filePath);
void loadFromContent(const QByteArray &content);
void loadFromCustomContent(