init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,701 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "lang/lang_cloud_manager.h"
#include "lang/lang_instance.h"
#include "lang/lang_file_parser.h"
#include "lang/lang_text_entity.h"
#include "mtproto/mtp_instance.h"
#include "storage/localstorage.h"
#include "core/application.h"
#include "main/main_account.h"
#include "main/main_domain.h"
#include "ui/boxes/confirm_box.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/widgets/labels.h"
#include "ui/text/text_utilities.h"
#include "core/file_utilities.h"
#include "core/click_handler_types.h"
#include "boxes/abstract_box.h" // Ui::hideLayer().
#include "styles/style_layers.h"
namespace Lang {
namespace {
class ConfirmSwitchBox : public Ui::BoxContent {
public:
ConfirmSwitchBox(
QWidget*,
const MTPDlangPackLanguage &data,
Fn<void()> apply);
protected:
void prepare() override;
private:
QString _name;
int _percent = 0;
bool _official = false;
QString _editLink;
Fn<void()> _apply;
};
class NotReadyBox : public Ui::BoxContent {
public:
NotReadyBox(
QWidget*,
const MTPDlangPackLanguage &data);
protected:
void prepare() override;
private:
QString _name;
QString _editLink;
};
ConfirmSwitchBox::ConfirmSwitchBox(
QWidget*,
const MTPDlangPackLanguage &data,
Fn<void()> apply)
: _name(qs(data.vnative_name()))
, _percent(data.vtranslated_count().v * 100 / data.vstrings_count().v)
, _official(data.is_official())
, _editLink(qs(data.vtranslations_url()))
, _apply(std::move(apply)) {
}
void ConfirmSwitchBox::prepare() {
setTitle(tr::lng_language_switch_title());
auto text = (_official
? tr::lng_language_switch_about_official
: tr::lng_language_switch_about_unofficial)(
lt_lang_name,
rpl::single(tr::bold(_name)),
lt_percent,
rpl::single(tr::bold(QString::number(_percent))),
lt_link,
tr::lng_language_switch_link(tr::url(_editLink)),
tr::marked);
const auto content = Ui::CreateChild<Ui::PaddingWrap<Ui::FlatLabel>>(
this,
object_ptr<Ui::FlatLabel>(
this,
std::move(text),
st::boxLabel),
QMargins{ st::boxPadding.left(), 0, st::boxPadding.right(), 0 });
content->entity()->setLinksTrusted();
addButton(tr::lng_language_switch_apply(), [=] {
const auto apply = _apply;
closeBox();
apply();
});
addButton(tr::lng_cancel(), [=] { closeBox(); });
content->resizeToWidth(st::boxWideWidth);
content->heightValue(
) | rpl::on_next([=](int height) {
setDimensions(st::boxWideWidth, height);
}, lifetime());
}
NotReadyBox::NotReadyBox(
QWidget*,
const MTPDlangPackLanguage &data)
: _name(qs(data.vnative_name()))
, _editLink(qs(data.vtranslations_url())) {
}
void NotReadyBox::prepare() {
setTitle(tr::lng_language_not_ready_title());
auto text = tr::lng_language_not_ready_about(
lt_lang_name,
rpl::single(tr::marked(_name)),
lt_link,
tr::lng_language_not_ready_link(tr::url(_editLink)),
tr::marked);
const auto content = Ui::CreateChild<Ui::PaddingWrap<Ui::FlatLabel>>(
this,
object_ptr<Ui::FlatLabel>(
this,
std::move(text),
st::boxLabel),
QMargins{ st::boxPadding.left(), 0, st::boxPadding.right(), 0 });
content->entity()->setLinksTrusted();
addButton(tr::lng_box_ok(), [=] { closeBox(); });
content->resizeToWidth(st::boxWidth);
content->heightValue(
) | rpl::on_next([=](int height) {
setDimensions(st::boxWidth, height);
}, lifetime());
}
} // namespace
Language ParseLanguage(const MTPLangPackLanguage &data) {
return data.match([](const MTPDlangPackLanguage &data) {
return Language{
qs(data.vlang_code()),
qs(data.vplural_code()),
qs(data.vbase_lang_code().value_or_empty()),
qs(data.vname()),
qs(data.vnative_name())
};
});
}
CloudManager::CloudManager(Instance &langpack)
: _langpack(langpack) {
const auto mtpLifetime = _lifetime.make_state<rpl::lifetime>();
Core::App().domain().activeValue(
) | rpl::filter([=](Main::Account *account) {
return (account != nullptr);
}) | rpl::on_next_done([=](Main::Account *account) {
*mtpLifetime = account->mtpMainSessionValue(
) | rpl::on_next([=](not_null<MTP::Instance*> instance) {
_api.emplace(instance);
resendRequests();
});
}, [=] {
_api.reset();
}, _lifetime);
}
Pack CloudManager::packTypeFromId(const QString &id) const {
if (id == LanguageIdOrDefault(_langpack.id())) {
return Pack::Current;
} else if (id == _langpack.baseId()) {
return Pack::Base;
}
return Pack::None;
}
rpl::producer<> CloudManager::languageListChanged() const {
return _languageListChanged.events();
}
rpl::producer<> CloudManager::firstLanguageSuggestion() const {
return _firstLanguageSuggestion.events();
}
void CloudManager::requestLangPackDifference(const QString &langId) {
Expects(!langId.isEmpty());
if (langId == LanguageIdOrDefault(_langpack.id())) {
requestLangPackDifference(Pack::Current);
} else {
requestLangPackDifference(Pack::Base);
}
}
mtpRequestId &CloudManager::packRequestId(Pack pack) {
return (pack != Pack::Base)
? _langPackRequestId
: _langPackBaseRequestId;
}
mtpRequestId CloudManager::packRequestId(Pack pack) const {
return (pack != Pack::Base)
? _langPackRequestId
: _langPackBaseRequestId;
}
void CloudManager::requestLangPackDifference(Pack pack) {
if (!_api) {
return;
}
_api->request(base::take(packRequestId(pack))).cancel();
if (_langpack.isCustom()) {
return;
}
const auto version = _langpack.version(pack);
const auto code = _langpack.cloudLangCode(pack);
if (code.isEmpty()) {
return;
}
if (version > 0) {
packRequestId(pack) = _api->request(MTPlangpack_GetDifference(
MTP_string(CloudLangPackName()),
MTP_string(code),
MTP_int(version)
)).done([=](const MTPLangPackDifference &result) {
packRequestId(pack) = 0;
applyLangPackDifference(result);
}).fail([=] {
packRequestId(pack) = 0;
}).send();
} else {
packRequestId(pack) = _api->request(MTPlangpack_GetLangPack(
MTP_string(CloudLangPackName()),
MTP_string(code)
)).done([=](const MTPLangPackDifference &result) {
packRequestId(pack) = 0;
applyLangPackDifference(result);
}).fail([=] {
packRequestId(pack) = 0;
}).send();
}
}
void CloudManager::setSuggestedLanguage(const QString &langCode) {
if (Lang::LanguageIdOrDefault(langCode) != Lang::DefaultLanguageId()) {
_suggestedLanguage = langCode;
} else {
_suggestedLanguage = QString();
}
if (!_languageWasSuggested) {
_languageWasSuggested = true;
_firstLanguageSuggestion.fire({});
if (Core::App().offerLegacyLangPackSwitch()
&& _langpack.id().isEmpty()
&& !_suggestedLanguage.isEmpty()) {
_offerSwitchToId = _suggestedLanguage;
offerSwitchLangPack();
}
}
}
void CloudManager::setCurrentVersions(int version, int baseVersion) {
const auto check = [&](Pack pack, int version) {
if (version > _langpack.version(pack) && !packRequestId(pack)) {
requestLangPackDifference(pack);
}
};
check(Pack::Current, version);
check(Pack::Base, baseVersion);
}
void CloudManager::applyLangPackDifference(
const MTPLangPackDifference &difference) {
Expects(difference.type() == mtpc_langPackDifference);
if (_langpack.isCustom()) {
return;
}
const auto &langpack = difference.c_langPackDifference();
const auto langpackId = qs(langpack.vlang_code());
const auto pack = packTypeFromId(langpackId);
if (pack != Pack::None) {
applyLangPackData(pack, langpack);
if (_restartAfterSwitch) {
restartAfterSwitch();
}
} else {
LOG(("Lang Warning: "
"Ignoring update for '%1' because our language is '%2'").arg(
langpackId,
_langpack.id()));
}
}
void CloudManager::requestLanguageList() {
if (!_api) {
_languagesRequestId = -1;
return;
}
_api->request(base::take(_languagesRequestId)).cancel();
_languagesRequestId = _api->request(MTPlangpack_GetLanguages(
MTP_string(CloudLangPackName())
)).done([=](const MTPVector<MTPLangPackLanguage> &result) {
auto languages = Languages();
for (const auto &language : result.v) {
languages.push_back(ParseLanguage(language));
}
if (_languages != languages) {
_languages = languages;
_languageListChanged.fire({});
}
_languagesRequestId = 0;
}).fail([=] {
_languagesRequestId = 0;
}).send();
}
void CloudManager::offerSwitchLangPack() {
Expects(!_offerSwitchToId.isEmpty());
Expects(_offerSwitchToId != DefaultLanguageId());
if (!showOfferSwitchBox()) {
languageListChanged(
) | rpl::on_next([=] {
showOfferSwitchBox();
}, _lifetime);
requestLanguageList();
}
}
Language CloudManager::findOfferedLanguage() const {
for (const auto &language : _languages) {
if (language.id == _offerSwitchToId) {
return language;
}
}
return {};
}
bool CloudManager::showOfferSwitchBox() {
const auto language = findOfferedLanguage();
if (language.id.isEmpty()) {
return false;
}
const auto confirm = [=] {
Ui::hideLayer();
if (_offerSwitchToId.isEmpty()) {
return;
}
performSwitchAndRestart(language);
};
const auto cancel = [=] {
Ui::hideLayer();
changeIdAndReInitConnection(DefaultLanguage());
Local::writeLangPack();
};
Ui::show(
Ui::MakeConfirmBox({
.text = QString("Do you want to switch your language to ")
+ language.nativeName
+ QString("? You can always change your language in Settings."),
.confirmed = confirm,
.cancelled = cancel,
.confirmText = QString("Change"),
}),
Ui::LayerOption::KeepOther);
return true;
}
void CloudManager::applyLangPackData(
Pack pack,
const MTPDlangPackDifference &data) {
if (_langpack.version(pack) < data.vfrom_version().v) {
requestLangPackDifference(pack);
} else if (!data.vstrings().v.isEmpty()) {
_langpack.applyDifference(pack, data);
Local::writeLangPack();
} else if (_restartAfterSwitch) {
Local::writeLangPack();
} else {
LOG(("Lang Info: Up to date."));
}
}
bool CloudManager::canApplyWithoutRestart(const QString &id) const {
if (id == u"#TEST_X"_q || id == u"#TEST_0"_q) {
return true;
}
return Core::App().canApplyLangPackWithoutRestart();
}
void CloudManager::resetToDefault() {
performSwitch(DefaultLanguage());
}
void CloudManager::switchToLanguage(const QString &id) {
requestLanguageAndSwitch(id, false);
}
void CloudManager::switchWithWarning(const QString &id) {
requestLanguageAndSwitch(id, true);
}
void CloudManager::requestLanguageAndSwitch(
const QString &id,
bool warning) {
Expects(!id.isEmpty());
if (LanguageIdOrDefault(_langpack.id()) == id) {
Ui::show(Ui::MakeInformBox(tr::lng_language_already()));
return;
} else if (id == u"#custom"_q) {
performSwitchToCustom();
return;
}
_switchingToLanguageId = id;
_switchingToLanguageWarning = warning;
sendSwitchingToLanguageRequest();
}
void CloudManager::sendSwitchingToLanguageRequest() {
if (!_api) {
_switchingToLanguageRequest = -1;
return;
}
_api->request(_switchingToLanguageRequest).cancel();
_switchingToLanguageRequest = _api->request(MTPlangpack_GetLanguage(
MTP_string(Lang::CloudLangPackName()),
MTP_string(_switchingToLanguageId)
)).done([=](const MTPLangPackLanguage &result) {
_switchingToLanguageRequest = 0;
const auto language = Lang::ParseLanguage(result);
const auto finalize = [=] {
if (canApplyWithoutRestart(language.id)) {
performSwitchAndAddToRecent(language);
} else {
performSwitchAndRestart(language);
}
};
if (!_switchingToLanguageWarning) {
finalize();
return;
}
result.match([=](const MTPDlangPackLanguage &data) {
if (data.vstrings_count().v > 0) {
Ui::show(Box<ConfirmSwitchBox>(data, finalize));
} else {
Ui::show(Box<NotReadyBox>(data));
}
});
}).fail([=](const MTP::Error &error) {
_switchingToLanguageRequest = 0;
if (error.type() == "LANG_CODE_NOT_SUPPORTED") {
Ui::show(Ui::MakeInformBox(tr::lng_language_not_found()));
}
}).send();
}
void CloudManager::switchToLanguage(const Language &data) {
if (_langpack.id() == data.id && data.id != u"#custom"_q) {
return;
} else if (!_api) {
return;
}
_api->request(base::take(_getKeysForSwitchRequestId)).cancel();
if (data.id == u"#custom"_q) {
performSwitchToCustom();
} else if (canApplyWithoutRestart(data.id)) {
performSwitchAndAddToRecent(data);
} else {
QVector<MTPstring> keys;
keys.reserve(3);
keys.push_back(MTP_string("lng_sure_save_language"));
_getKeysForSwitchRequestId = _api->request(MTPlangpack_GetStrings(
MTP_string(Lang::CloudLangPackName()),
MTP_string(data.id),
MTP_vector<MTPstring>(std::move(keys))
)).done([=](const MTPVector<MTPLangPackString> &result) {
_getKeysForSwitchRequestId = 0;
const auto values = Instance::ParseStrings(result);
const auto getValue = [&](ushort key) {
auto it = values.find(key);
return (it == values.cend())
? GetOriginalValue(key)
: it->second;
};
const auto text = tr::lng_sure_save_language(tr::now)
+ "\n\n"
+ getValue(tr::lng_sure_save_language.base);
Ui::show(
Ui::MakeConfirmBox({
.text = text,
.confirmed = [=] { performSwitchAndRestart(data); },
.confirmText = tr::lng_box_ok(),
}),
Ui::LayerOption::KeepOther);
}).fail([=] {
_getKeysForSwitchRequestId = 0;
}).send();
}
}
void CloudManager::performSwitchToCustom() {
auto filter = u"Language files (*.strings)"_q;
auto title = u"Choose language .strings file"_q;
FileDialog::GetOpenPath(Core::App().getFileDialogParent(), title, filter, [=, weak = base::make_weak(this)](const FileDialog::OpenResult &result) {
if (!weak || result.paths.isEmpty()) {
return;
}
const auto filePath = result.paths.front();
auto loader = Lang::FileParser(
filePath,
{ tr::lng_sure_save_language.base });
if (loader.errors().isEmpty()) {
if (_api) {
_api->request(
base::take(_switchingToLanguageRequest)
).cancel();
}
if (canApplyWithoutRestart(u"#custom"_q)) {
_langpack.switchToCustomFile(filePath);
} else {
const auto values = loader.found();
const auto getValue = [&](ushort key) {
const auto it = values.find(key);
return (it == values.cend())
? GetOriginalValue(key)
: it.value();
};
const auto text = tr::lng_sure_save_language(tr::now)
+ "\n\n"
+ getValue(tr::lng_sure_save_language.base);
const auto change = [=] {
_langpack.switchToCustomFile(filePath);
Core::Restart();
};
Ui::show(
Ui::MakeConfirmBox({
.text = text,
.confirmed = change,
.confirmText = tr::lng_box_ok(),
}),
Ui::LayerOption::KeepOther);
}
} else {
Ui::show(
Ui::MakeInformBox(
"Custom lang failed :(\n\nError: " + loader.errors()),
Ui::LayerOption::KeepOther);
}
});
}
void CloudManager::switchToTestLanguage() {
const auto testLanguageId = (_langpack.id() == u"#TEST_X"_q)
? u"#TEST_0"_q
: u"#TEST_X"_q;
performSwitch({ testLanguageId });
}
void CloudManager::performSwitch(const Language &data) {
_restartAfterSwitch = false;
switchLangPackId(data);
requestLangPackDifference(Pack::Current);
requestLangPackDifference(Pack::Base);
}
void CloudManager::performSwitchAndAddToRecent(const Language &data) {
Local::pushRecentLanguage(data);
performSwitch(data);
}
void CloudManager::performSwitchAndRestart(const Language &data) {
performSwitchAndAddToRecent(data);
restartAfterSwitch();
}
void CloudManager::restartAfterSwitch() {
if (_langPackRequestId || _langPackBaseRequestId) {
_restartAfterSwitch = true;
} else {
Core::Restart();
}
}
void CloudManager::switchLangPackId(const Language &data) {
const auto currentId = _langpack.id();
const auto currentBaseId = _langpack.baseId();
const auto notChanged = (currentId == data.id
&& currentBaseId == data.baseId)
|| (currentId.isEmpty()
&& currentBaseId.isEmpty()
&& data.id == DefaultLanguageId());
if (!notChanged) {
changeIdAndReInitConnection(data);
}
}
void CloudManager::changeIdAndReInitConnection(const Language &data) {
_langpack.switchToId(data);
if (_api) {
const auto mtproto = &_api->instance();
mtproto->reInitConnection(mtproto->mainDcId());
}
}
void CloudManager::getValueForLang(
const QString &key,
const QString &langId,
Fn<void(const QString &)> callback) {
const auto requestKey = langId + ':' + key;
auto &request = _getValueForLangRequests[requestKey];
request.callback = std::move(callback);
if (!_api) {
request.requestId = -1;
return;
}
_api->request(base::take(request.requestId)).cancel();
request.requestId = _api->request(
MTPlangpack_GetStrings(
MTP_string(Lang::CloudLangPackName()),
MTP_string(langId),
MTP_vector<MTPstring>(1, MTP_string(key))
)).done([=](const MTPVector<MTPLangPackString> &result) {
const auto it = _getValueForLangRequests.find(requestKey);
if (it != _getValueForLangRequests.end()) {
const auto onstack = it->second.callback;
_getValueForLangRequests.erase(it);
const auto values = Instance::ParseStrings(result);
for (const auto &[k, v] : values) {
onstack(v);
return;
}
onstack(QString());
}
}).fail([=] {
const auto it = _getValueForLangRequests.find(requestKey);
if (it != _getValueForLangRequests.end()) {
const auto onstack = it->second.callback;
_getValueForLangRequests.erase(it);
onstack(QString());
}
}).send();
}
void CloudManager::resendPendingValueRequests() {
if (!_api) {
return;
}
for (const auto &[requestKey, request] : _getValueForLangRequests) {
if (request.requestId == -1) {
const auto colonPos = requestKey.indexOf(':');
if (colonPos > 0) {
getValueForLang(
requestKey.mid(colonPos + 1),
requestKey.left(colonPos),
request.callback);
}
}
}
}
void CloudManager::resendRequests() {
if (packRequestId(Pack::Base)) {
requestLangPackDifference(Pack::Base);
}
if (packRequestId(Pack::Current)) {
requestLangPackDifference(Pack::Current);
}
if (_languagesRequestId) {
requestLanguageList();
}
if (_switchingToLanguageRequest) {
sendSwitchingToLanguageRequest();
}
resendPendingValueRequests();
}
CloudManager &CurrentCloudManager() {
auto result = Core::App().langCloudManager();
Assert(result != nullptr);
return *result;
}
} // namespace Lang

View File

@@ -0,0 +1,114 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "mtproto/sender.h"
#include "base/weak_ptr.h"
namespace MTP {
class Instance;
} // namespace MTP
namespace Lang {
class Instance;
enum class Pack;
struct Language;
Language ParseLanguage(const MTPLangPackLanguage &data);
class CloudManager : public base::has_weak_ptr {
public:
explicit CloudManager(Instance &langpack);
using Languages = std::vector<Language>;
void requestLanguageList();
const Languages &languageList() const {
return _languages;
}
[[nodiscard]] rpl::producer<> languageListChanged() const;
[[nodiscard]] rpl::producer<> firstLanguageSuggestion() const;
void requestLangPackDifference(const QString &langId);
void applyLangPackDifference(const MTPLangPackDifference &difference);
void setCurrentVersions(int version, int baseVersion);
void resetToDefault();
void switchWithWarning(const QString &id);
void switchToLanguage(const QString &id);
void switchToLanguage(const Language &data);
void switchToTestLanguage();
void setSuggestedLanguage(const QString &langCode);
QString suggestedLanguage() const {
return _suggestedLanguage;
}
void getValueForLang(
const QString &key,
const QString &langId,
Fn<void(const QString &)> callback);
private:
mtpRequestId &packRequestId(Pack pack);
mtpRequestId packRequestId(Pack pack) const;
Pack packTypeFromId(const QString &id) const;
void requestLangPackDifference(Pack pack);
bool canApplyWithoutRestart(const QString &id) const;
void performSwitchToCustom();
void performSwitch(const Language &data);
void performSwitchAndAddToRecent(const Language &data);
void performSwitchAndRestart(const Language &data);
void restartAfterSwitch();
void offerSwitchLangPack();
bool showOfferSwitchBox();
Language findOfferedLanguage() const;
void requestLanguageAndSwitch(const QString &id, bool warning);
void applyLangPackData(Pack pack, const MTPDlangPackDifference &data);
void switchLangPackId(const Language &data);
void changeIdAndReInitConnection(const Language &data);
void sendSwitchingToLanguageRequest();
void resendPendingValueRequests();
void resendRequests();
std::optional<MTP::Sender> _api;
Instance &_langpack;
Languages _languages;
mtpRequestId _langPackRequestId = 0;
mtpRequestId _langPackBaseRequestId = 0;
mtpRequestId _languagesRequestId = 0;
QString _offerSwitchToId;
bool _restartAfterSwitch = false;
QString _suggestedLanguage;
bool _languageWasSuggested = false;
mtpRequestId _switchingToLanguageRequest = 0;
QString _switchingToLanguageId;
bool _switchingToLanguageWarning = false;
mtpRequestId _getKeysForSwitchRequestId = 0;
struct ValueRequest {
mtpRequestId requestId = 0;
Fn<void(const QString &)> callback;
};
base::flat_map<QString, ValueRequest> _getValueForLangRequests;
rpl::event_stream<> _languageListChanged;
rpl::event_stream<> _firstLanguageSuggestion;
rpl::lifetime _lifetime;
};
CloudManager &CurrentCloudManager();
} // namespace Lang

View File

@@ -0,0 +1,210 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "lang/lang_file_parser.h"
#include "base/parse_helper.h"
#include "base/debug_log.h"
#include <QtCore/QTextStream>
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
namespace Lang {
namespace {
constexpr auto kLangFileLimit = 1024 * 1024;
} // namespace
FileParser::FileParser(const QString &file, const std::set<ushort> &request)
: _content(base::parse::stripComments(ReadFile(file, file)))
, _request(request) {
parse();
}
FileParser::FileParser(const QByteArray &content, Fn<void(QLatin1String key, const QByteArray &value)> callback)
: _content(base::parse::stripComments(content))
, _callback(std::move(callback)) {
parse();
}
void FileParser::parse() {
if (_content.isEmpty()) {
error(u"Got empty lang file content"_q);
return;
}
auto text = _content.constData(), end = text + _content.size();
while (text != end) {
if (!readKeyValue(text, end)) {
break;
}
}
}
const QString &FileParser::errors() const {
if (_errors.isEmpty() && !_errorsList.isEmpty()) {
_errors = _errorsList.join('\n');
}
return _errors;
}
const QString &FileParser::warnings() const {
if (_warnings.isEmpty() && !_warningsList.isEmpty()) {
_warnings = _warningsList.join('\n');
}
return _warnings;
}
bool FileParser::readKeyValue(const char *&from, const char *end) {
using base::parse::skipWhitespaces;
if (!skipWhitespaces(from, end)) return false;
if (*from != '"') {
return error("Expected quote before key name!");
}
++from;
const char *nameStart = from;
while (from < end && ((*from >= 'a' && *from <= 'z') || (*from >= 'A' && *from <= 'Z') || *from == '_' || (*from >= '0' && *from <= '9') || *from == '#')) {
++from;
}
auto key = QLatin1String(nameStart, from - nameStart);
if (from == end || *from != '"') {
return error(u"Expected quote after key name '%1'!"_q.arg(key));
}
++from;
if (!skipWhitespaces(from, end)) {
return error(u"Unexpected end of file in key '%1'!"_q.arg(key));
}
if (*from != '=') {
return error(u"'=' expected in key '%1'!"_q.arg(key));
}
if (!skipWhitespaces(++from, end)) {
return error(u"Unexpected end of file in key '%1'!"_q.arg(key));
}
if (*from != '"') {
return error(u"Expected string after '=' in key '%1'!"_q.arg(key));
}
auto skipping = false;
auto keyIndex = kKeysCount;
if (!_callback) {
keyIndex = GetKeyIndex(key);
skipping = (_request.find(keyIndex) == _request.end());
}
auto value = QByteArray();
auto appendValue = [&value, skipping](auto&&... args) {
if (!skipping) {
value.append(std::forward<decltype(args)>(args)...);
}
};
const char *start = ++from;
while (from < end && *from != '"') {
if (*from == '\n') {
return error(u"Unexpected end of string in key '%1'!"_q.arg(key));
}
if (*from == '\\') {
if (from + 1 >= end) {
return error(u"Unexpected end of file in key '%1'!"_q.arg(key));
}
if (*(from + 1) == '"' || *(from + 1) == '\\') {
if (from > start) appendValue(start, from - start);
start = ++from;
} else if (*(from + 1) == 'n') {
if (from > start) appendValue(start, from - start);
appendValue('\n');
start = (++from) + 1;
}
}
++from;
}
if (from >= end) {
return error(u"Unexpected end of file in key '%1'!"_q.arg(key));
}
if (from > start) {
appendValue(start, from - start);
}
if (!skipWhitespaces(++from, end)) {
return error(u"Unexpected end of file in key '%1'!"_q.arg(key));
}
if (*from != ';') {
return error(u"';' expected after \"value\" in key '%1'!"_q.arg(key));
}
skipWhitespaces(++from, end);
if (_callback) {
_callback(key, value);
} else if (!skipping) {
_result.insert(keyIndex, QString::fromUtf8(value));
}
return true;
}
QByteArray FileParser::ReadFile(const QString &absolutePath, const QString &relativePath) {
QFile file(QFileInfo::exists(relativePath) ? relativePath : absolutePath);
if (!file.open(QIODevice::ReadOnly)) {
LOG(("Lang Error: Could not open file at '%1' ('%2')")
.arg(relativePath, absolutePath));
return QByteArray();
}
if (file.size() > kLangFileLimit) {
LOG(("Lang Error: File is too big: %1").arg(file.size()));
return QByteArray();
}
constexpr auto kCodecMagicSize = 3;
auto codecMagic = file.read(kCodecMagicSize);
if (codecMagic.size() < kCodecMagicSize) {
LOG(("Lang Error: Found bad file at '%1' ('%2')").arg(relativePath, absolutePath));
return QByteArray();
}
file.seek(0);
QByteArray data;
auto readUtf16Stream = [relativePath, absolutePath](auto &&stream) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
stream.setEncoding(QStringConverter::Utf16);
#else // Qt >= 6.0.0
stream.setCodec("UTF-16");
#endif // Qt < 6.0.0
auto string = stream.readAll();
if (stream.status() != QTextStream::Ok) {
LOG(("Lang Error: Could not read UTF-16 data from '%1' ('%2')").arg(relativePath, absolutePath));
return QByteArray();
}
if (string.isEmpty()) {
LOG(("Lang Error: Empty UTF-16 content in '%1' ('%2')").arg(relativePath, absolutePath));
return QByteArray();
}
return string.toUtf8();
};
if ((codecMagic.at(0) == '\xFF' && codecMagic.at(1) == '\xFE') || (codecMagic.at(0) == '\xFE' && codecMagic.at(1) == '\xFF') || (codecMagic.at(1) == 0)) {
return readUtf16Stream(QTextStream(&file));
} else if (codecMagic.at(0) == 0) {
auto utf16WithBOM = "\xFE\xFF" + file.readAll();
return readUtf16Stream(QTextStream(utf16WithBOM));
}
data = file.readAll();
if (codecMagic.at(0) == '\xEF' && codecMagic.at(1) == '\xBB' && codecMagic.at(2) == '\xBF') {
data = data.mid(3); // skip UTF-8 BOM
}
if (data.isEmpty()) {
LOG(("Lang Error: Empty UTF-8 content in '%1' ('%2')").arg(relativePath, absolutePath));
return QByteArray();
}
return data;
}
} // namespace Lang

View File

@@ -0,0 +1,56 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "lang/lang_keys.h"
#include <set>
#include <QtCore/QMap>
namespace Lang {
class FileParser {
public:
using Result = QMap<ushort, QString>;
FileParser(const QString &file, const std::set<ushort> &request);
FileParser(const QByteArray &content, Fn<void(QLatin1String key, const QByteArray &value)> callback);
static QByteArray ReadFile(const QString &absolutePath, const QString &relativePath);
const QString &errors() const;
const QString &warnings() const;
Result found() const {
return _result;
}
private:
void parse();
bool error(const QString &text) {
_errorsList.push_back(text);
return false;
}
void warning(const QString &text) {
_warningsList.push_back(text);
}
bool readKeyValue(const char *&from, const char *end);
mutable QStringList _errorsList, _warningsList;
mutable QString _errors, _warnings;
const QByteArray _content;
const std::set<ushort> _request;
const Fn<void(QLatin1String key, const QByteArray &value)> _callback;
Result _result;
};
} // namespace Lang

View File

@@ -0,0 +1,74 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Lang {
namespace Hard {
inline QString FavedSetTitle() {
return u"Favorite stickers"_q;
}
inline QString CallErrorIncompatible() {
return u"{user}'s app is using an incompatible protocol. They need to update their app before you can call them."_q;
}
inline QString ServerError() {
return u"Internal server error."_q;
}
inline QString ClearPathFailed() {
return u"Clear failed :("_q;
}
inline QString ProxyConfigError() {
return u"The proxy you are using is not configured correctly and will be disabled. Please find another one."_q;
}
inline QString NoAuthorizationBot() {
return u"Could not get authorization bot."_q;
}
inline QString SecureSaveError() {
return u"Error saving value."_q;
}
inline QString SecureAcceptError() {
return u"Error accepting form."_q;
}
inline QString PassportCorrupted() {
return u"It seems your Telegram Passport data was corrupted.\n\nYou can reset your Telegram Passport and restart this authorization."_q;
}
inline QString PassportCorruptedChange() {
return u"It seems your Telegram Passport data was corrupted.\n\nYou can reset your Telegram Passport and change your cloud password."_q;
}
inline QString PassportCorruptedReset() {
return u"Reset"_q;
}
inline QString PassportCorruptedResetSure() {
return u"Are you sure you want to reset your Telegram Passport data?"_q;
}
inline QString UnknownSecureScanError() {
return u"Unknown scan read error."_q;
}
inline QString EmailConfirmationExpired() {
return u"This email confirmation has expired. Please setup two-step verification once again."_q;
}
inline QString AutostartEnableError() {
return u"Could not register for autostart."_q;
}
} // namespace Hard
} // namespace Lang

View File

@@ -0,0 +1,822 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "lang/lang_instance.h"
#include "core/application.h"
#include "storage/serialize_common.h"
#include "storage/localstorage.h"
#include "ui/boxes/confirm_box.h"
#include "lang/lang_file_parser.h"
#include "lang/lang_tag.h" // kTextCommandLangTag.
#include "base/platform/base_platform_info.h"
#include "base/qthelp_regex.h"
namespace Lang {
namespace {
const auto kSerializeVersionTag = u"#new"_q;
constexpr auto kSerializeVersion = 1;
constexpr auto kCloudLangPackName = "tdesktop"_cs;
constexpr auto kCustomLanguage = "#custom"_cs;
constexpr auto kLangValuesLimit = 20000;
std::vector<QString> PrepareDefaultValues() {
auto result = std::vector<QString>();
result.reserve(kKeysCount);
for (auto i = 0; i != kKeysCount; ++i) {
result.emplace_back(GetOriginalValue(ushort(i)));
}
return result;
}
class ValueParser {
public:
ValueParser(
const QByteArray &key,
ushort keyIndex,
const QByteArray &value);
QString takeResult() {
Expects(!_failed);
return std::move(_result);
}
bool parse();
private:
void appendToResult(const char *nextBegin);
bool logError(const QString &text);
bool readTag();
const QByteArray &_key;
ushort _keyIndex = kKeysCount;
QLatin1String _currentTag;
ushort _currentTagIndex = 0;
QString _currentTagReplacer;
bool _failed = true;
const char *_begin = nullptr;
const char *_ch = nullptr;
const char *_end = nullptr;
QString _result;
OrderedSet<ushort> _tagsUsed;
};
ValueParser::ValueParser(
const QByteArray &key,
ushort keyIndex,
const QByteArray &value)
: _key(key)
, _keyIndex(keyIndex)
, _currentTag("")
, _begin(value.constData())
, _ch(_begin)
, _end(_begin + value.size()) {
}
void ValueParser::appendToResult(const char *nextBegin) {
if (_ch > _begin) _result.append(QString::fromUtf8(_begin, _ch - _begin));
_begin = nextBegin;
}
bool ValueParser::logError(const QString &text) {
_failed = true;
auto loggedKey = (_currentTag.size() > 0) ? (_key + QString(':') + _currentTag) : QString(_key);
LOG(("Lang Error: %1 (key '%2')").arg(text, loggedKey));
return false;
}
bool ValueParser::readTag() {
auto tagStart = _ch;
auto isTagChar = [](QChar ch) {
if (ch >= 'a' && ch <= 'z') {
return true;
} else if (ch >= 'A' && ch <= 'Z') {
return true;
} else if (ch >= '0' && ch <= '9') {
return true;
}
return (ch == '_');
};
while (_ch != _end && isTagChar(*_ch)) {
++_ch;
}
if (_ch == tagStart) {
return logError("Expected tag name");
}
_currentTag = QLatin1String(tagStart, _ch - tagStart);
if (_ch == _end || *_ch != '}') {
return logError("Expected '}' after tag name");
}
_currentTagIndex = GetTagIndex(_currentTag);
if (_currentTagIndex == kTagsCount) {
return logError("Unknown tag");
}
if (!IsTagReplaced(_keyIndex, _currentTagIndex)) {
return logError("Unexpected tag");
}
if (_tagsUsed.contains(_currentTagIndex)) {
return logError("Repeated tag");
}
_tagsUsed.insert(_currentTagIndex);
if (_currentTagReplacer.isEmpty()) {
_currentTagReplacer = QString(4, QChar(kTextCommand));
_currentTagReplacer[1] = QChar(kTextCommandLangTag);
}
_currentTagReplacer[2] = QChar(0x0020 + _currentTagIndex);
return true;
}
bool ValueParser::parse() {
_failed = false;
_result.reserve(_end - _begin);
for (; _ch != _end; ++_ch) {
if (*_ch == '{') {
appendToResult(_ch);
++_ch;
if (!readTag()) {
return false;
}
_result.append(_currentTagReplacer);
_begin = _ch + 1;
_currentTag = QLatin1String("");
}
}
appendToResult(_end);
return true;
}
QString PrepareTestValue(const QString &current, QChar filler) {
auto size = current.size();
auto result = QString(size + 1, filler);
auto inCommand = false;
for (auto i = 0; i != size; ++i) {
const auto ch = current[i];
const auto newInCommand = (ch.unicode() == kTextCommand)
? (!inCommand)
: inCommand;
if (inCommand || newInCommand || ch.isSpace()) {
result[i + 1] = ch;
}
inCommand = newInCommand;
}
return result;
}
QString PluralCodeForCustom(
const QString &absolutePath,
const QString &relativePath) {
const auto path = !absolutePath.isEmpty()
? absolutePath
: relativePath;
const auto name = QFileInfo(path).fileName();
if (const auto match = qthelp::regex_match(
"_([a-z]{2,3}_[A-Z]{2,3}|\\-[a-z]{2,3})?\\.",
name)) {
return match->captured(1);
}
return DefaultLanguageId();
}
template <typename Save>
void ParseKeyValue(
const QByteArray &key,
const QByteArray &value,
Save &&save) {
const auto index = GetKeyIndex(QLatin1String(key));
if (index != kKeysCount) {
ValueParser parser(key, index, value);
if (parser.parse()) {
save(index, parser.takeResult());
}
} else if (!key.startsWith("cloud_")) {
DEBUG_LOG(("Lang Warning: Unknown key '%1'"
).arg(QString::fromLatin1(key)));
}
}
} // namespace
QString CloudLangPackName() {
return kCloudLangPackName.utf16();
}
QString CustomLanguageId() {
return kCustomLanguage.utf16();
}
Language DefaultLanguage() {
return Language{
u"en"_q,
QString(),
QString(),
u"English"_q,
u"English"_q,
};
}
struct Instance::PrivateTag {
};
Instance::Instance()
: _values(PrepareDefaultValues())
, _nonDefaultSet(kKeysCount, 0) {
}
Instance::Instance(not_null<Instance*> derived, const PrivateTag &)
: _derived(derived)
, _nonDefaultSet(kKeysCount, 0) {
}
void Instance::switchToId(const Language &data) {
reset(data);
if (_id == u"#TEST_X"_q || _id == u"#TEST_0"_q) {
for (auto &value : _values) {
value = PrepareTestValue(value, _id[5]);
}
if (!_derived) {
_updated.fire({});
}
}
updatePluralRules();
}
void Instance::setBaseId(const QString &baseId, const QString &pluralId) {
if (baseId.isEmpty()) {
_base = nullptr;
} else {
if (!_base) {
_base = std::make_unique<Instance>(this, PrivateTag{});
}
_base->switchToId({ baseId, pluralId });
}
}
void Instance::switchToCustomFile(const QString &filePath) {
if (loadFromCustomFile(filePath)) {
Local::writeLangPack();
_updated.fire({});
}
}
void Instance::reset(const Language &data) {
const auto computedPluralId = !data.pluralId.isEmpty()
? data.pluralId
: !data.baseId.isEmpty()
? data.baseId
: data.id;
setBaseId(data.baseId, computedPluralId);
_id = LanguageIdOrDefault(data.id);
_pluralId = computedPluralId;
_name = data.name;
_nativeName = data.nativeName;
_customFilePathAbsolute = QString();
_customFilePathRelative = QString();
_customFileContent = QByteArray();
_version = 0;
_nonDefaultValues.clear();
for (auto i = 0, count = int(_values.size()); i != count; ++i) {
_values[i] = GetOriginalValue(ushort(i));
}
ranges::fill(_nonDefaultSet, 0);
updateChoosingStickerReplacement();
_idChanges.fire_copy(_id);
}
QString Instance::systemLangCode() const {
if (_systemLanguage.isEmpty()) {
_systemLanguage = Platform::SystemLanguage();
if (_systemLanguage.isEmpty()) {
auto uiLanguages = QLocale::system().uiLanguages();
if (!uiLanguages.isEmpty()) {
_systemLanguage = uiLanguages.front();
}
if (_systemLanguage.isEmpty()) {
_systemLanguage = DefaultLanguageId();
}
}
}
return _systemLanguage;
}
QString Instance::cloudLangCode(Pack pack) const {
return (isCustom() || id().isEmpty())
? DefaultLanguageId()
: id(pack);
}
QString Instance::id() const {
return id(Pack::Current);
}
rpl::producer<QString> Instance::idChanges() const {
return _idChanges.events();
}
QString Instance::baseId() const {
return id(Pack::Base);
}
QString Instance::name() const {
return _name.isEmpty()
? getValue(tr::lng_language_name.base)
: _name;
}
QString Instance::nativeName() const {
return _nativeName.isEmpty()
? getValue(tr::lng_language_name.base)
: _nativeName;
}
QString Instance::id(Pack pack) const {
return (pack != Pack::Base)
? _id
: _base
? _base->id(Pack::Current)
: QString();
}
bool Instance::isCustom() const {
return (_id == CustomLanguageId())
|| (_id == u"#TEST_X"_q)
|| (_id == u"#TEST_0"_q);
}
int Instance::version(Pack pack) const {
return (pack != Pack::Base)
? _version
: _base
? _base->version(Pack::Current)
: 0;
}
QString Instance::langPackName() const {
return isCustom() ? QString() : CloudLangPackName();
}
QByteArray Instance::serialize() const {
auto size = Serialize::stringSize(kSerializeVersionTag)
+ sizeof(qint32) // serializeVersion
+ Serialize::stringSize(_id)
+ Serialize::stringSize(_pluralId)
+ Serialize::stringSize(_name)
+ Serialize::stringSize(_nativeName)
+ sizeof(qint32) // version
+ Serialize::stringSize(_customFilePathAbsolute)
+ Serialize::stringSize(_customFilePathRelative)
+ Serialize::bytearraySize(_customFileContent)
+ sizeof(qint32); // _nonDefaultValues.size()
for (auto &nonDefault : _nonDefaultValues) {
size += Serialize::bytearraySize(nonDefault.first)
+ Serialize::bytearraySize(nonDefault.second);
}
const auto base = _base ? _base->serialize() : QByteArray();
size += Serialize::bytearraySize(base);
auto result = QByteArray();
result.reserve(size);
{
QDataStream stream(&result, QIODevice::WriteOnly);
stream.setVersion(QDataStream::Qt_5_1);
stream
<< kSerializeVersionTag
<< qint32(kSerializeVersion)
<< _id
<< _pluralId
<< _name
<< _nativeName
<< qint32(_version)
<< _customFilePathAbsolute
<< _customFilePathRelative
<< _customFileContent
<< qint32(_nonDefaultValues.size());
for (const auto &nonDefault : _nonDefaultValues) {
stream << nonDefault.first << nonDefault.second;
}
stream << base;
}
return result;
}
void Instance::fillFromSerialized(
const QByteArray &data,
int dataAppVersion) {
QDataStream stream(data);
stream.setVersion(QDataStream::Qt_5_1);
qint32 serializeVersion = 0;
QString serializeVersionTag;
QString id, pluralId, name, nativeName;
qint32 version = 0;
QString customFilePathAbsolute, customFilePathRelative;
QByteArray customFileContent;
qint32 nonDefaultValuesCount = 0;
stream >> serializeVersionTag;
const auto legacyFormat = (serializeVersionTag != kSerializeVersionTag);
if (legacyFormat) {
id = serializeVersionTag;
stream
>> version
>> customFilePathAbsolute
>> customFilePathRelative
>> customFileContent
>> nonDefaultValuesCount;
} else {
stream >> serializeVersion;
if (serializeVersion == kSerializeVersion) {
stream
>> id
>> pluralId
>> name
>> nativeName
>> version
>> customFilePathAbsolute
>> customFilePathRelative
>> customFileContent
>> nonDefaultValuesCount;
} else {
LOG(("Lang Error: Unsupported serialize version."));
return;
}
}
if (stream.status() != QDataStream::Ok) {
LOG(("Lang Error: Could not read data from serialized langpack."));
return;
}
if (nonDefaultValuesCount > kLangValuesLimit) {
LOG(("Lang Error: Values count limit exceeded: %1"
).arg(nonDefaultValuesCount));
return;
}
if (!customFilePathAbsolute.isEmpty()) {
id = CustomLanguageId();
auto currentCustomFileContent = Lang::FileParser::ReadFile(
customFilePathAbsolute,
customFilePathRelative);
if (!currentCustomFileContent.isEmpty()
&& currentCustomFileContent != customFileContent) {
fillFromCustomContent(
customFilePathAbsolute,
customFilePathRelative,
currentCustomFileContent);
Local::writeLangPack();
return;
}
}
std::vector<QByteArray> nonDefaultStrings;
nonDefaultStrings.reserve(2 * nonDefaultValuesCount);
for (auto i = 0; i != nonDefaultValuesCount; ++i) {
QByteArray key, value;
stream >> key >> value;
if (stream.status() != QDataStream::Ok) {
LOG(("Lang Error: "
"Could not read data from serialized langpack."));
return;
}
nonDefaultStrings.push_back(key);
nonDefaultStrings.push_back(value);
}
_base = nullptr;
QByteArray base;
if (legacyFormat) {
if (!stream.atEnd()) {
stream >> pluralId;
} else {
pluralId = id;
}
if (!stream.atEnd()) {
stream >> base;
if (base.isEmpty()) {
stream.setStatus(QDataStream::ReadCorruptData);
}
}
if (stream.status() != QDataStream::Ok) {
LOG(("Lang Error: "
"Could not read data from serialized langpack."));
return;
}
} else {
stream >> base;
}
if (!base.isEmpty()) {
_base = std::make_unique<Instance>(this, PrivateTag{});
_base->fillFromSerialized(base, dataAppVersion);
}
_id = id;
_pluralId = (id == CustomLanguageId())
? PluralCodeForCustom(
customFilePathAbsolute,
customFilePathRelative)
: pluralId;
_name = name;
_nativeName = nativeName;
_version = version;
_customFilePathAbsolute = customFilePathAbsolute;
_customFilePathRelative = customFilePathRelative;
_customFileContent = customFileContent;
LOG(("Lang Info: Loaded cached, keys: %1").arg(nonDefaultValuesCount));
for (auto i = 0, count = nonDefaultValuesCount * 2; i != count; i += 2) {
applyValue(nonDefaultStrings[i], nonDefaultStrings[i + 1]);
}
updatePluralRules();
updateChoosingStickerReplacement();
_idChanges.fire_copy(_id);
}
void Instance::loadFromContent(const QByteArray &content) {
Lang::FileParser loader(content, [this](QLatin1String key, const QByteArray &value) {
applyValue(QByteArray(key.data(), key.size()), value);
});
if (!loader.errors().isEmpty()) {
LOG(("Lang load errors: %1").arg(loader.errors()));
} else if (!loader.warnings().isEmpty()) {
LOG(("Lang load warnings: %1").arg(loader.warnings()));
}
}
void Instance::fillFromCustomContent(
const QString &absolutePath,
const QString &relativePath,
const QByteArray &content) {
setBaseId(QString(), QString());
_id = CustomLanguageId();
_pluralId = PluralCodeForCustom(absolutePath, relativePath);
_name = _nativeName = QString();
loadFromCustomContent(absolutePath, relativePath, content);
updateChoosingStickerReplacement();
_idChanges.fire_copy(_id);
}
void Instance::loadFromCustomContent(
const QString &absolutePath,
const QString &relativePath,
const QByteArray &content) {
_version = 0;
_customFilePathAbsolute = absolutePath;
_customFilePathRelative = relativePath;
_customFileContent = content;
loadFromContent(_customFileContent);
}
bool Instance::loadFromCustomFile(const QString &filePath) {
auto absolutePath = QFileInfo(filePath).absoluteFilePath();
auto relativePath = QDir().relativeFilePath(filePath);
auto content = Lang::FileParser::ReadFile(absolutePath, relativePath);
if (!content.isEmpty()) {
reset({
CustomLanguageId(),
PluralCodeForCustom(absolutePath, relativePath) });
loadFromCustomContent(absolutePath, relativePath, content);
updatePluralRules();
return true;
}
return false;
}
void Instance::updateChoosingStickerReplacement() {
// A language changing in the runtime is not supported.
const auto replacement = kChoosingStickerReplacement.utf8();
const auto phrase = tr::lng_send_action_choose_sticker(tr::now);
const auto first = phrase.indexOf(replacement);
const auto support = (first != -1);
const auto phraseNamed = tr::lng_user_action_choose_sticker(
tr::now,
lt_user,
QString());
const auto firstNamed = phraseNamed.indexOf(replacement);
const auto supportNamed = (firstNamed != -1);
_choosingStickerReplacement.support = (supportNamed && support);
_choosingStickerReplacement.rightIndex = phrase.size() - first;
_choosingStickerReplacement.rightIndexNamed = phraseNamed.size()
- firstNamed;
}
bool Instance::supportChoosingStickerReplacement() const {
return _choosingStickerReplacement.support;
}
int Instance::rightIndexChoosingStickerReplacement(bool named) const {
return named
? _choosingStickerReplacement.rightIndexNamed
: _choosingStickerReplacement.rightIndex;
}
// SetCallback takes two QByteArrays: key, value.
// It is called for all key-value pairs in string.
// ResetCallback takes one QByteArray: key.
template <typename SetCallback, typename ResetCallback>
void HandleString(
const MTPLangPackString &string,
SetCallback setCallback,
ResetCallback resetCallback) {
string.match([&](const MTPDlangPackString &data) {
setCallback(qba(data.vkey()), qba(data.vvalue()));
}, [&](const MTPDlangPackStringPluralized &data) {
const auto key = qba(data.vkey());
setCallback(key + "#zero", data.vzero_value().value_or_empty());
setCallback(key + "#one", data.vone_value().value_or_empty());
setCallback(key + "#two", data.vtwo_value().value_or_empty());
setCallback(key + "#few", data.vfew_value().value_or_empty());
setCallback(key + "#many", data.vmany_value().value_or_empty());
setCallback(key + "#other", qba(data.vother_value()));
}, [&](const MTPDlangPackStringDeleted &data) {
auto key = qba(data.vkey());
resetCallback(key);
const auto postfixes = {
"#zero",
"#one",
"#two",
"#few",
"#many",
"#other"
};
for (const auto plural : postfixes) {
resetCallback(key + plural);
}
});
}
void Instance::applyDifference(
Pack pack,
const MTPDlangPackDifference &difference) {
switch (pack) {
case Pack::Current:
applyDifferenceToMe(difference);
break;
case Pack::Base:
Assert(_base != nullptr);
_base->applyDifference(Pack::Current, difference);
break;
default:
Unexpected("Pack in Instance::applyDifference.");
}
}
void Instance::applyDifferenceToMe(
const MTPDlangPackDifference &difference) {
Expects(LanguageIdOrDefault(_id) == qs(difference.vlang_code()));
Expects(difference.vfrom_version().v <= _version);
_version = difference.vversion().v;
for (const auto &string : difference.vstrings().v) {
HandleString(string, [&](auto &&key, auto &&value) {
applyValue(key, value);
}, [&](auto &&key) {
resetValue(key);
});
}
if (!_derived) {
_updated.fire({});
} else {
_derived->_updated.fire({});
}
}
std::map<ushort, QString> Instance::ParseStrings(
const MTPVector<MTPLangPackString> &strings) {
auto result = std::map<ushort, QString>();
for (const auto &string : strings.v) {
HandleString(string, [&](auto &&key, auto &&value) {
ParseKeyValue(key, value, [&](ushort key, QString &&value) {
result[key] = std::move(value);
});
}, [&](auto &&key) {
auto keyIndex = GetKeyIndex(QLatin1String(key));
if (keyIndex != kKeysCount) {
result.erase(keyIndex);
}
});
}
return result;
}
QString Instance::getNonDefaultValue(const QByteArray &key) const {
const auto i = _nonDefaultValues.find(key);
return (i != end(_nonDefaultValues))
? QString::fromUtf8(i->second)
: _base
? _base->getNonDefaultValue(key)
: QString();
}
void Instance::applyValue(const QByteArray &key, const QByteArray &value) {
_nonDefaultValues[key] = value;
ParseKeyValue(key, value, [&](ushort key, QString &&value) {
_nonDefaultSet[key] = 1;
if (!_derived) {
_values[key] = std::move(value);
} else if (!_derived->_nonDefaultSet[key]) {
_derived->_values[key] = std::move(value);
}
if (key == tr::lng_send_action_choose_sticker.base
|| key == tr::lng_user_action_choose_sticker.base) {
if (!_derived) {
updateChoosingStickerReplacement();
} else {
_derived->updateChoosingStickerReplacement();
}
}
});
}
void Instance::updatePluralRules() {
if (_pluralId.isEmpty()) {
_pluralId = isCustom()
? PluralCodeForCustom(
_customFilePathAbsolute,
_customFilePathRelative)
: LanguageIdOrDefault(_id);
}
UpdatePluralRules(_pluralId);
}
void Instance::resetValue(const QByteArray &key) {
_nonDefaultValues.erase(key);
const auto keyIndex = GetKeyIndex(QLatin1String(key));
if (keyIndex != kKeysCount) {
_nonDefaultSet[keyIndex] = 0;
if (!_derived) {
const auto base = _base
? _base->getNonDefaultValue(key)
: QString();
_values[keyIndex] = !base.isEmpty()
? base
: GetOriginalValue(keyIndex);
} else if (!_derived->_nonDefaultSet[keyIndex]) {
_derived->_values[keyIndex] = GetOriginalValue(keyIndex);
}
if (keyIndex == tr::lng_send_action_choose_sticker.base
|| keyIndex == tr::lng_user_action_choose_sticker.base) {
if (!_derived) {
updateChoosingStickerReplacement();
} else {
_derived->updateChoosingStickerReplacement();
}
}
}
}
Instance &GetInstance() {
return Core::App().langpack();
}
QString Id() {
return GetInstance().id();
}
rpl::producer<> Updated() {
return GetInstance().updated();
}
QString GetNonDefaultValue(const QByteArray &key) {
return GetInstance().getNonDefaultValue(key);
}
namespace details {
QString Current(ushort key) {
return GetInstance().getValue(key);
}
rpl::producer<QString> Value(ushort key) {
return rpl::single(
Current(key)
) | then(
Updated() | rpl::map([=] { return Current(key); })
);
}
bool IsNonDefaultPlural(ushort keyBase) {
return GetInstance().isNonDefaultPlural(keyBase);
}
} // namespace details
} // namespace Lang

View File

@@ -0,0 +1,161 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "lang_auto.h"
#include "base/const_string.h"
#include "base/weak_ptr.h"
namespace Lang {
inline constexpr auto kChoosingStickerReplacement = "oo"_cs;
struct Language {
QString id;
QString pluralId;
QString baseId;
QString name;
QString nativeName;
};
inline bool operator==(const Language &a, const Language &b) {
return (a.id == b.id) && (a.name == b.name);
}
inline bool operator!=(const Language &a, const Language &b) {
return !(a == b);
}
QString CloudLangPackName();
QString CustomLanguageId();
Language DefaultLanguage();
class Instance;
Instance &GetInstance();
enum class Pack {
None,
Current,
Base,
};
class Instance {
struct PrivateTag;
public:
Instance();
Instance(not_null<Instance*> derived, const PrivateTag &);
void switchToId(const Language &language);
void switchToCustomFile(const QString &filePath);
Instance(const Instance &other) = delete;
Instance &operator=(const Instance &other) = delete;
Instance(Instance &&other) = default;
Instance &operator=(Instance &&other) = default;
QString systemLangCode() const;
QString langPackName() const;
QString cloudLangCode(Pack pack) const;
QString id() const;
rpl::producer<QString> idChanges() const;
QString baseId() const;
QString name() const;
QString nativeName() const;
QString id(Pack pack) const;
bool isCustom() const;
int version(Pack pack) const;
QByteArray serialize() const;
void fillFromSerialized(const QByteArray &data, int dataAppVersion);
bool supportChoosingStickerReplacement() const;
int rightIndexChoosingStickerReplacement(bool named) const;
void applyDifference(
Pack pack,
const MTPDlangPackDifference &difference);
static std::map<ushort, QString> ParseStrings(
const MTPVector<MTPLangPackString> &strings);
[[nodiscard]] rpl::producer<> updated() const {
return _updated.events();
}
QString getValue(ushort key) const {
Expects(key < _values.size());
return _values[key];
}
QString getNonDefaultValue(const QByteArray &key) const;
bool isNonDefaultPlural(ushort key) const {
Expects(key + 5 < _nonDefaultSet.size());
return _nonDefaultSet[key]
|| _nonDefaultSet[key + 1]
|| _nonDefaultSet[key + 2]
|| _nonDefaultSet[key + 3]
|| _nonDefaultSet[key + 4]
|| _nonDefaultSet[key + 5]
|| (_base && _base->isNonDefaultPlural(key));
}
private:
void setBaseId(const QString &baseId, const QString &pluralId);
void applyDifferenceToMe(const MTPDlangPackDifference &difference);
void applyValue(const QByteArray &key, const QByteArray &value);
void resetValue(const QByteArray &key);
void reset(const Language &language);
void fillFromCustomContent(
const QString &absolutePath,
const QString &relativePath,
const QByteArray &content);
bool loadFromCustomFile(const QString &filePath);
void loadFromContent(const QByteArray &content);
void loadFromCustomContent(
const QString &absolutePath,
const QString &relativePath,
const QByteArray &content);
void updatePluralRules();
void updateChoosingStickerReplacement();
Instance *_derived = nullptr;
QString _id, _pluralId;
rpl::event_stream<QString> _idChanges;
QString _name, _nativeName;
QString _customFilePathAbsolute;
QString _customFilePathRelative;
QByteArray _customFileContent;
int _version = 0;
rpl::event_stream<> _updated;
struct {
bool support = false;
int rightIndex = 0;
int rightIndexNamed = 0;
} _choosingStickerReplacement;
mutable QString _systemLanguage;
std::vector<QString> _values;
std::vector<uchar> _nonDefaultSet;
std::map<QByteArray, QByteArray> _nonDefaultValues;
std::unique_ptr<Instance> _base;
};
namespace details {
QString Current(ushort key);
rpl::producer<QString> Value(ushort key);
} // namespace details
} // namespace Lang

View File

@@ -0,0 +1,250 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "lang/lang_keys.h"
#include "base/const_string.h"
#include "lang/lang_file_parser.h"
#include "ui/integration.h"
#include <QtCore/QLocale>
namespace {
constexpr auto kDefaultLanguage = "en"_cs;
template <typename WithYear, typename WithoutYear>
inline QString langDateMaybeWithYear(
QDate date,
WithYear withYear,
WithoutYear withoutYear) {
const auto month = date.month();
if (month <= 0 || month > 12) {
return u"MONTH_ERR"_q;
};
const auto year = date.year();
const auto current = QDate::currentDate();
const auto currentYear = current.year();
const auto currentMonth = current.month();
if (year != currentYear) {
const auto yearIsMuchGreater = [](int year, int otherYear) {
return (year > otherYear + 1);
};
const auto monthIsMuchGreater = [](
int year,
int month,
int otherYear,
int otherMonth) {
return (year == otherYear + 1) && (month + 12 > otherMonth + 3);
};
if (false
|| yearIsMuchGreater(year, currentYear)
|| yearIsMuchGreater(currentYear, year)
|| monthIsMuchGreater(year, month, currentYear, currentMonth)
|| monthIsMuchGreater(currentYear, currentMonth, year, month)) {
return withYear(month, year);
}
}
return withoutYear(month, year);
}
using namespace Lang;
} // namespace
bool langFirstNameGoesSecond() {
const auto kFirstName = QChar(0x0001);
const auto kLastName = QChar(0x0002);
const auto fullname = tr::lng_full_name(
tr::now,
lt_first_name,
QString(1, kFirstName),
lt_last_name,
QString(1, kLastName));
return fullname.indexOf(kLastName) < fullname.indexOf(kFirstName);
}
QString langDayOfMonth(const QDate &date) {
auto day = date.day();
return langDateMaybeWithYear(date, [&](int month, int year) {
return tr::lng_month_day_year(
tr::now,
lt_month,
MonthSmall(month)(tr::now),
lt_day,
QString::number(day),
lt_year,
QString::number(year));
}, [day](int month, int year) {
return tr::lng_month_day(
tr::now,
lt_month,
MonthSmall(month)(tr::now),
lt_day,
QString::number(day));
});
}
QString langDayOfMonthFull(const QDate &date) {
auto day = date.day();
return langDateMaybeWithYear(date, [day](int month, int year) {
return tr::lng_month_day_year(
tr::now,
lt_month,
MonthDay(month)(tr::now),
lt_day,
QString::number(day),
lt_year,
QString::number(year));
}, [day](int month, int year) {
return tr::lng_month_day(
tr::now,
lt_month,
MonthDay(month)(tr::now),
lt_day,
QString::number(day));
});
}
QString langMonthOfYear(int month, int year) {
return (month > 0 && month <= 12)
? tr::lng_month_year(
tr::now,
lt_month,
MonthSmall(month)(tr::now),
lt_year,
QString::number(year))
: u"MONTH_ERR"_q;
}
QString langMonth(const QDate &date) {
return langDateMaybeWithYear(date, [](int month, int year) {
return langMonthOfYear(month, year);
}, [](int month, int year) {
return MonthSmall(month)(tr::now);
});
}
QString langMonthOfYearFull(int month, int year) {
return (month > 0 && month <= 12)
? tr::lng_month_year(
tr::now,
lt_month,
Month(month)(tr::now),
lt_year,
QString::number(year))
: u"MONTH_ERR"_q;
}
QString langMonthFull(const QDate &date) {
return langDateMaybeWithYear(date, [](int month, int year) {
return langMonthOfYearFull(month, year);
}, [](int month, int year) {
return Month(month)(tr::now);
});
}
QString langDayOfWeek(int index) {
return (index > 0 && index <= 7) ? Weekday(index)(tr::now) : u"DAY_ERR"_q;
}
QString langDateTime(const QDateTime &date) {
return tr::lng_mediaview_date_time(
tr::now,
lt_date,
langDayOfMonth(date.date()),
lt_time,
QLocale().toString(date.time(), QLocale::ShortFormat));
}
QString langDateTimeFull(const QDateTime &date) {
return tr::lng_mediaview_date_time(
tr::now,
lt_date,
langDayOfMonthFull(date.date()),
lt_time,
QLocale().toString(date.time(), QLocale::ShortFormat));
}
namespace Lang {
QString DefaultLanguageId() {
return kDefaultLanguage.utf16();
}
QString LanguageIdOrDefault(const QString &id) {
return !id.isEmpty() ? id : DefaultLanguageId();
}
tr::phrase<> Month(int index) {
switch (index) {
case 1: return tr::lng_month1;
case 2: return tr::lng_month2;
case 3: return tr::lng_month3;
case 4: return tr::lng_month4;
case 5: return tr::lng_month5;
case 6: return tr::lng_month6;
case 7: return tr::lng_month7;
case 8: return tr::lng_month8;
case 9: return tr::lng_month9;
case 10: return tr::lng_month10;
case 11: return tr::lng_month11;
case 12: return tr::lng_month12;
}
Unexpected("Index in MonthSmall.");
}
tr::phrase<> MonthSmall(int index) {
switch (index) {
case 1: return tr::lng_month1_small;
case 2: return tr::lng_month2_small;
case 3: return tr::lng_month3_small;
case 4: return tr::lng_month4_small;
case 5: return tr::lng_month5_small;
case 6: return tr::lng_month6_small;
case 7: return tr::lng_month7_small;
case 8: return tr::lng_month8_small;
case 9: return tr::lng_month9_small;
case 10: return tr::lng_month10_small;
case 11: return tr::lng_month11_small;
case 12: return tr::lng_month12_small;
}
Unexpected("Index in MonthSmall.");
}
tr::phrase<> MonthDay(int index) {
switch (index) {
case 1: return tr::lng_month_day1;
case 2: return tr::lng_month_day2;
case 3: return tr::lng_month_day3;
case 4: return tr::lng_month_day4;
case 5: return tr::lng_month_day5;
case 6: return tr::lng_month_day6;
case 7: return tr::lng_month_day7;
case 8: return tr::lng_month_day8;
case 9: return tr::lng_month_day9;
case 10: return tr::lng_month_day10;
case 11: return tr::lng_month_day11;
case 12: return tr::lng_month_day12;
}
Unexpected("Index in MonthDay.");
}
tr::phrase<> Weekday(int index) {
switch (index) {
case 1: return tr::lng_weekday1;
case 2: return tr::lng_weekday2;
case 3: return tr::lng_weekday3;
case 4: return tr::lng_weekday4;
case 5: return tr::lng_weekday5;
case 6: return tr::lng_weekday6;
case 7: return tr::lng_weekday7;
}
Unexpected("Index in Weekday.");
}
} // namespace Lang

View File

@@ -0,0 +1,45 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "lang_auto.h"
#include "lang/lang_hardcoded.h"
#include "lang/lang_text_entity.h"
#include <QDateTime>
[[nodiscard]] QString langDayOfMonth(const QDate &date);
[[nodiscard]] QString langDayOfMonthFull(const QDate &date);
[[nodiscard]] QString langMonthOfYear(int month, int year);
[[nodiscard]] QString langMonth(const QDate &date);
[[nodiscard]] QString langMonthOfYearFull(int month, int year);
[[nodiscard]] QString langMonthFull(const QDate &date);
[[nodiscard]] QString langDayOfWeek(int index);
[[nodiscard]] inline QString langDayOfWeek(const QDate &date) {
return langDayOfWeek(date.dayOfWeek());
}
[[nodiscard]] QString langDateTime(const QDateTime &date);
[[nodiscard]] QString langDateTimeFull(const QDateTime &date);
[[nodiscard]] bool langFirstNameGoesSecond();
namespace Lang {
[[nodiscard]] QString Id();
[[nodiscard]] rpl::producer<> Updated();
[[nodiscard]] QString GetNonDefaultValue(const QByteArray &key);
[[nodiscard]] QString DefaultLanguageId();
[[nodiscard]] QString LanguageIdOrDefault(const QString &id);
[[nodiscard]] tr::phrase<> Month(int index);
[[nodiscard]] tr::phrase<> MonthSmall(int index);
[[nodiscard]] tr::phrase<> MonthDay(int index);
[[nodiscard]] tr::phrase<> Weekday(int index);
} // namespace Lang

View File

@@ -0,0 +1,30 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "lang/lang_numbers_animation.h"
#include "lang/lang_tag.h"
namespace Lang {
Ui::StringWithNumbers ReplaceTag<Ui::StringWithNumbers>::Call(
Ui::StringWithNumbers &&original,
ushort tag,
const Ui::StringWithNumbers &replacement) {
original.offset = FindTagReplacementPosition(original.text, tag);
if (original.offset < 0) {
return std::move(original);
}
original.text = ReplaceTag<QString>::Call(
std::move(original.text),
tag,
replacement.text);
original.length = replacement.text.size();
return std::move(original);
}
} // namespace Lang

View File

@@ -0,0 +1,35 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/effects/numbers_animation.h"
namespace Lang {
template <typename ResultString>
struct StartReplacements;
template <>
struct StartReplacements<Ui::StringWithNumbers> {
static inline Ui::StringWithNumbers Call(QString &&langString) {
return { std::move(langString) };
}
};
template <typename ResultString>
struct ReplaceTag;
template <>
struct ReplaceTag<Ui::StringWithNumbers> {
static Ui::StringWithNumbers Call(
Ui::StringWithNumbers &&original,
ushort tag,
const Ui::StringWithNumbers &replacement);
};
} // namespace Lang

View File

@@ -0,0 +1,14 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include <QtCore/QString>
#include <QtCore/QDateTime>
#include <rpl/rpl.h>
#include "base/basic_types.h"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,76 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
class CreditsAmount;
enum lngtag_count : int;
namespace Lang {
inline constexpr auto kTextCommand = 0x10;
inline constexpr auto kTextCommandLangTag = 0x20;
constexpr auto kTagReplacementSize = 4;
[[nodiscard]] int FindTagReplacementPosition(
const QString &original,
ushort tag);
struct ShortenedCount {
int64 number = 0;
QString string;
bool shortened = false;
};
[[nodiscard]] ShortenedCount FormatCountToShort(
int64 number,
bool onlyK = false);
[[nodiscard]] QString FormatCountDecimal(int64 number);
[[nodiscard]] QString FormatExactCountDecimal(float64 number);
[[nodiscard]] ShortenedCount FormatCreditsAmountToShort(
CreditsAmount amount);
[[nodiscard]] QString FormatCreditsAmountDecimal(CreditsAmount amount);
[[nodiscard]] QString FormatCreditsAmountRounded(CreditsAmount amount);
struct PluralResult {
int keyShift = 0;
QString replacement;
};
inline constexpr auto kPluralKeyBaseForCloudValue = ushort(-1);
PluralResult Plural(
ushort keyBase,
float64 value,
lngtag_count type);
void UpdatePluralRules(const QString &languageId);
template <typename ResultString>
struct StartReplacements;
template <>
struct StartReplacements<QString> {
static inline QString Call(QString &&langString) {
return std::move(langString);
}
};
template <typename ResultString>
struct ReplaceTag;
template <>
struct ReplaceTag<QString> {
static inline QString Call(QString &&original, ushort tag, const QString &replacement) {
auto replacementPosition = FindTagReplacementPosition(original, tag);
if (replacementPosition < 0) {
return std::move(original);
}
return Replace(std::move(original), replacement, replacementPosition);
}
static QString Replace(QString &&original, const QString &replacement, int start);
};
} // namespace Lang

View File

@@ -0,0 +1,75 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "lang/lang_text_entity.h"
#include "lang/lang_tag.h"
namespace Lang {
TextWithEntities ReplaceTag<TextWithEntities>::Call(TextWithEntities &&original, ushort tag, const TextWithEntities &replacement) {
auto replacementPosition = FindTagReplacementPosition(original.text, tag);
if (replacementPosition < 0) {
return std::move(original);
}
return Replace(std::move(original), replacement, replacementPosition);
}
TextWithEntities ReplaceTag<TextWithEntities>::Replace(TextWithEntities &&original, const TextWithEntities &replacement, int start) {
auto result = TextWithEntities();
result.text = ReplaceTag<QString>::Replace(std::move(original.text), replacement.text, start);
auto originalEntitiesCount = original.entities.size();
auto replacementEntitiesCount = replacement.entities.size();
if (originalEntitiesCount != 0 || replacementEntitiesCount != 0) {
result.entities.reserve(originalEntitiesCount + replacementEntitiesCount);
auto replacementEnd = start + int(replacement.text.size());
auto replacementEntity = replacement.entities.cbegin();
auto addReplacementEntitiesUntil = [&](int untilPosition) {
while (replacementEntity != replacement.entities.cend()) {
auto newOffset = start + replacementEntity->offset();
if (newOffset >= untilPosition) {
return;
}
auto newEnd = newOffset + replacementEntity->length();
newOffset = std::clamp(newOffset, start, replacementEnd);
newEnd = std::clamp(newEnd, start, replacementEnd);
if (auto newLength = newEnd - newOffset) {
result.entities.push_back({ replacementEntity->type(), newOffset, newLength, replacementEntity->data() });
}
++replacementEntity;
}
};
for (const auto &entity : std::as_const(original.entities)) {
// Transform the entity by the replacement.
auto offset = entity.offset();
auto end = offset + entity.length();
if (offset > start) {
offset = offset + replacement.text.size() - kTagReplacementSize;
}
if (end > start) {
end = end + replacement.text.size() - kTagReplacementSize;
}
offset = std::clamp(offset, 0, int(result.text.size()));
end = std::clamp(end, 0, int(result.text.size()));
// Add all replacement entities that start before the current original entity.
addReplacementEntitiesUntil(offset);
// Add a modified original entity.
if (auto length = end - offset) {
result.entities.push_back({ entity.type(), offset, length, entity.data() });
}
}
// Add the remaining replacement entities.
addReplacementEntitiesUntil(result.text.size());
}
return result;
}
} // namespace Lang

View File

@@ -0,0 +1,225 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/text/text_entity.h"
#include "ui/text/text_utilities.h"
namespace Lang {
template <typename ResultString>
struct StartReplacements;
template <>
struct StartReplacements<TextWithEntities> {
static inline TextWithEntities Call(QString &&langString) {
return { std::move(langString), EntitiesInText() };
}
};
template <typename ResultString>
struct ReplaceTag;
template <>
struct ReplaceTag<TextWithEntities> {
static TextWithEntities Call(TextWithEntities &&original, ushort tag, const TextWithEntities &replacement);
static TextWithEntities Replace(TextWithEntities &&original, const TextWithEntities &replacement, int start);
};
} // namespace Lang
namespace tr {
namespace details {
struct UpperProjection {
[[nodiscard]] QString operator()(const QString &value) const {
return value.toUpper();
}
[[nodiscard]] QString operator()(QString &&value) const {
return std::move(value).toUpper();
}
[[nodiscard]] TextWithEntities operator()(
const TextWithEntities &value) const {
return { value.text.toUpper(), value.entities };
}
[[nodiscard]] TextWithEntities operator()(
TextWithEntities &&value) const {
return {
std::move(value.text).toUpper(),
std::move(value.entities) };
}
};
struct MarkedProjection {
[[nodiscard]] TextWithEntities operator()() const {
return {};
}
[[nodiscard]] TextWithEntities operator()(const QString &value) const {
return TextWithEntities{ value };
}
[[nodiscard]] TextWithEntities operator()(QString &&value) const {
return TextWithEntities{ std::move(value) };
}
[[nodiscard]] TextWithEntities operator()(
const TextWithEntities &value) const {
return value;
}
[[nodiscard]] TextWithEntities operator()(
TextWithEntities &&value) const {
return std::move(value);
}
};
struct RichProjection {
[[nodiscard]] TextWithEntities operator()(const QString &value) const {
return Ui::Text::RichLangValue(value);
}
};
struct BoldProjection {
[[nodiscard]] TextWithEntities operator()(const QString &value) const {
return Ui::Text::Bold(value);
}
[[nodiscard]] TextWithEntities operator()(
const TextWithEntities &value) const {
return Ui::Text::Wrapped(value, EntityType::Bold);
}
[[nodiscard]] TextWithEntities operator()(
TextWithEntities &&value) const {
return Ui::Text::Wrapped(std::move(value), EntityType::Bold);
}
};
struct SemiboldProjection {
[[nodiscard]] TextWithEntities operator()(const QString &value) const {
return Ui::Text::Semibold(value);
}
[[nodiscard]] TextWithEntities operator()(
const TextWithEntities &value) const {
return Ui::Text::Wrapped(value, EntityType::Semibold);
}
[[nodiscard]] TextWithEntities operator()(
TextWithEntities &&value) const {
return Ui::Text::Wrapped(std::move(value), EntityType::Semibold);
}
};
struct ItalicProjection {
[[nodiscard]] TextWithEntities operator()(const QString &value) const {
return Ui::Text::Italic(value);
}
[[nodiscard]] TextWithEntities operator()(
const TextWithEntities &value) const {
return Ui::Text::Wrapped(value, EntityType::Italic);
}
[[nodiscard]] TextWithEntities operator()(
TextWithEntities &&value) const {
return Ui::Text::Wrapped(std::move(value), EntityType::Italic);
}
};
struct UnderlineProjection {
[[nodiscard]] TextWithEntities operator()(const QString &value) const {
return Ui::Text::Underline(value);
}
[[nodiscard]] TextWithEntities operator()(
const TextWithEntities &value) const {
return Ui::Text::Wrapped(value, EntityType::Underline);
}
[[nodiscard]] TextWithEntities operator()(
TextWithEntities &&value) const {
return Ui::Text::Wrapped(std::move(value), EntityType::Underline);
}
};
struct StrikeOutProjection {
[[nodiscard]] TextWithEntities operator()(const QString &value) const {
return Ui::Text::StrikeOut(value);
}
[[nodiscard]] TextWithEntities operator()(
const TextWithEntities &value) const {
return Ui::Text::Wrapped(value, EntityType::StrikeOut);
}
[[nodiscard]] TextWithEntities operator()(
TextWithEntities &&value) const {
return Ui::Text::Wrapped(std::move(value), EntityType::StrikeOut);
}
};
struct LinkProjection {
[[nodiscard]] TextWithEntities operator()(const QString &value) const {
return Ui::Text::Link(value);
}
[[nodiscard]] TextWithEntities operator()(
const QString &value,
const QString &url) const {
return Ui::Text::Link(value, url);
}
[[nodiscard]] TextWithEntities operator()(
const QString &value,
int index) const {
return Ui::Text::Link(value, index);
}
[[nodiscard]] TextWithEntities operator()(
const TextWithEntities &value) const {
return Ui::Text::Link(value);
}
[[nodiscard]] TextWithEntities operator()(
const TextWithEntities &value,
const QString &url) const {
return Ui::Text::Link(value, url);
}
[[nodiscard]] TextWithEntities operator()(
const TextWithEntities &value,
int index) const {
return Ui::Text::Link(value, index);
}
[[nodiscard]] TextWithEntities operator()(
TextWithEntities &&value) const {
return Ui::Text::Link(std::move(value));
}
[[nodiscard]] TextWithEntities operator()(
TextWithEntities &&value,
const QString &url) const {
return Ui::Text::Link(std::move(value), url);
}
[[nodiscard]] TextWithEntities operator()(
TextWithEntities &&value,
int index) const {
return Ui::Text::Link(std::move(value), index);
}
};
struct UrlProjection {
[[nodiscard]] auto operator()(const QString &url) const {
return [url](auto &&text) {
return Ui::Text::Link(std::forward<decltype(text)>(text), url);
};
}
[[nodiscard]] auto operator()(int index) const {
return [index](auto &&text) {
return Ui::Text::Link(std::forward<decltype(text)>(text), index);
};
}
};
} // namespace details
inline constexpr details::UpperProjection upper{};
inline constexpr details::MarkedProjection marked{};
inline constexpr details::RichProjection rich{};
inline constexpr details::BoldProjection bold{};
inline constexpr details::SemiboldProjection semibold{};
inline constexpr details::ItalicProjection italic{};
inline constexpr details::UnderlineProjection underline{};
inline constexpr details::StrikeOutProjection strikeout{};
inline constexpr details::LinkProjection link{};
inline constexpr details::UrlProjection url{};
} // namespace tr

View File

@@ -0,0 +1,43 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "lang/lang_translator.h"
#include "lang/lang_keys.h"
#include "base/platform/base_platform_info.h"
namespace Lang {
QString Translator::translate(const char *context, const char *sourceText, const char *disambiguation, int n) const {
if (u"QMenuBar"_q == context) {
if (u"Services"_q == sourceText) return tr::lng_mac_menu_services(tr::now);
if (u"Hide %1"_q == sourceText) return tr::lng_mac_menu_hide_telegram(tr::now, lt_telegram, u"%1"_q);
if (u"Hide Others"_q == sourceText) return tr::lng_mac_menu_hide_others(tr::now);
if (u"Show All"_q == sourceText) return tr::lng_mac_menu_show_all(tr::now);
if (u"Preferences..."_q == sourceText) return tr::lng_mac_menu_preferences(tr::now);
if (u"Quit %1"_q == sourceText) return tr::lng_mac_menu_quit_telegram(tr::now, lt_telegram, u"%1"_q);
if (u"About %1"_q == sourceText) return tr::lng_mac_menu_about_telegram(tr::now, lt_telegram, u"%1"_q);
return QString();
}
if (u"QWidgetTextControl"_q == context || u"QLineEdit"_q == context) {
if (u"&Undo"_q == sourceText) return Platform::IsWindows() ? tr::lng_wnd_menu_undo(tr::now) : Platform::IsMac() ? tr::lng_mac_menu_undo(tr::now) : tr::lng_linux_menu_undo(tr::now);
if (u"&Redo"_q == sourceText) return Platform::IsWindows() ? tr::lng_wnd_menu_redo(tr::now) : Platform::IsMac() ? tr::lng_mac_menu_redo(tr::now) : tr::lng_linux_menu_redo(tr::now);
if (u"Cu&t"_q == sourceText) return tr::lng_mac_menu_cut(tr::now);
if (u"&Copy"_q == sourceText) return tr::lng_mac_menu_copy(tr::now);
if (u"&Paste"_q == sourceText) return tr::lng_mac_menu_paste(tr::now);
if (u"Delete"_q == sourceText) return tr::lng_mac_menu_delete(tr::now);
if (u"Select All"_q == sourceText) return tr::lng_mac_menu_select_all(tr::now);
return QString();
}
if (u"QUnicodeControlCharacterMenu"_q == context) {
if (u"Insert Unicode control character"_q == sourceText) return tr::lng_menu_insert_unicode(tr::now);
return QString();
}
return QString();
}
} // namespace Lang

View File

@@ -0,0 +1,20 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include <QtCore/QTranslator>
namespace Lang {
class Translator : public QTranslator {
public:
QString translate(const char *context, const char *sourceText, const char *disambiguation = 0, int n = -1) const override;
};
} // namespace Lang

View File

@@ -0,0 +1,175 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "lang/lang_tag.h"
enum lngtag_count : int;
namespace Lang {
namespace details {
inline constexpr auto kPluralCount = 6;
template <typename Tag>
inline constexpr ushort TagValue();
template <typename P>
using S = std::decay_t<decltype(std::declval<P>()(QString()))>;
[[nodiscard]] QString Current(ushort key);
[[nodiscard]] rpl::producer<QString> Value(ushort key);
[[nodiscard]] bool IsNonDefaultPlural(ushort keyBase);
template <int Index, typename Type, typename Tuple>
[[nodiscard]] Type ReplaceUnwrapTuple(Type accumulated, const Tuple &tuple) {
return accumulated;
}
template <
int Index,
typename Type,
typename Tuple,
typename Tag,
typename ...Tags>
[[nodiscard]] Type ReplaceUnwrapTuple(
Type accumulated,
const Tuple &tuple,
Tag tag,
Tags ...tags) {
return ReplaceUnwrapTuple<Index + 1>(
ReplaceTag<Type>::Call(
std::move(accumulated),
tag,
std::get<Index>(tuple)),
tuple,
tags...);
}
template <typename ...Tags>
struct ReplaceUnwrap;
template <>
struct ReplaceUnwrap<> {
template <typename Type>
[[nodiscard]] static Type Call(Type accumulated) {
return accumulated;
}
};
template <typename Tag, typename ...Tags>
struct ReplaceUnwrap<Tag, Tags...> {
template <typename Type, typename Value, typename ...Values>
[[nodiscard]] static Type Call(
Type accumulated,
const Value &value,
const Values &...values) {
return ReplaceUnwrap<Tags...>::Call(
ReplaceTag<Type>::Call(
std::move(accumulated),
TagValue<Tag>(),
value),
values...);
}
};
template <typename ...Tags>
struct Producer {
template <typename P, typename ...Values>
[[nodiscard]] static rpl::producer<S<P>> Combine(ushort base, P p, Values &...values) {
return rpl::combine(
Value(base),
std::move(values)...
) | rpl::map([p = std::move(p)](auto tuple) {
return ReplaceUnwrapTuple<1>(p(std::get<0>(tuple)), tuple, TagValue<Tags>()...);
});
}
template <typename P, typename ...Values>
[[nodiscard]] static S<P> Current(ushort base, P p, const Values &...values) {
return ReplaceUnwrap<Tags...>::Call(
p(Lang::details::Current(base)),
values...);
}
};
template <>
struct Producer<> {
template <typename P>
[[nodiscard]] static rpl::producer<S<P>> Combine(ushort base, P p) {
return Value(base) | rpl::map(std::move(p));
}
template <typename P>
[[nodiscard]] static S<P> Current(ushort base, P p) {
return p(Lang::details::Current(base));
}
};
template <typename ...Tags>
struct Producer<lngtag_count, Tags...> {
template <typename P, typename ...Values>
[[nodiscard]] static rpl::producer<S<P>> Combine(
ushort base,
P p,
lngtag_count type,
rpl::producer<float64> &count,
Values &...values) {
return rpl::combine(
Value(base),
Value(base + 1),
Value(base + 2),
Value(base + 3),
Value(base + 4),
Value(base + 5),
std::move(count),
std::move(values)...
) | rpl::map([base, type, p = std::move(p)](auto tuple) {
auto plural = Plural(base, std::get<6>(tuple), type);
const auto select = [&] {
switch (plural.keyShift) {
case 0: return std::get<0>(tuple);
case 1: return std::get<1>(tuple);
case 2: return std::get<2>(tuple);
case 3: return std::get<3>(tuple);
case 4: return std::get<4>(tuple);
case 5: return std::get<5>(tuple);
}
Unexpected("Lang shift value in Plural result.");
};
return ReplaceUnwrapTuple<7>(
ReplaceTag<S<P>>::Call(
p(select()),
TagValue<lngtag_count>(),
StartReplacements<S<P>>::Call(
std::move(plural.replacement))),
tuple,
TagValue<Tags>()...);
});
}
template <typename P, typename ...Values>
[[nodiscard]] static S<P> Current(
ushort base,
P p,
lngtag_count type,
float64 count,
const Values &...values) {
auto plural = Plural(base, count, type);
return ReplaceUnwrap<Tags...>::Call(
ReplaceTag<S<P>>::Call(
p(Lang::details::Current(base + plural.keyShift)),
TagValue<lngtag_count>(),
StartReplacements<S<P>>::Call(
std::move(plural.replacement))),
values...);
}
};
} // namespace details
} // namespace Lang