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
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:
701
Telegram/SourceFiles/lang/lang_cloud_manager.cpp
Normal file
701
Telegram/SourceFiles/lang/lang_cloud_manager.cpp
Normal 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
|
||||
114
Telegram/SourceFiles/lang/lang_cloud_manager.h
Normal file
114
Telegram/SourceFiles/lang/lang_cloud_manager.h
Normal 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
|
||||
210
Telegram/SourceFiles/lang/lang_file_parser.cpp
Normal file
210
Telegram/SourceFiles/lang/lang_file_parser.cpp
Normal 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
|
||||
56
Telegram/SourceFiles/lang/lang_file_parser.h
Normal file
56
Telegram/SourceFiles/lang/lang_file_parser.h
Normal 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
|
||||
74
Telegram/SourceFiles/lang/lang_hardcoded.h
Normal file
74
Telegram/SourceFiles/lang/lang_hardcoded.h
Normal 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
|
||||
822
Telegram/SourceFiles/lang/lang_instance.cpp
Normal file
822
Telegram/SourceFiles/lang/lang_instance.cpp
Normal 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 ¤t, 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
|
||||
161
Telegram/SourceFiles/lang/lang_instance.h
Normal file
161
Telegram/SourceFiles/lang/lang_instance.h
Normal 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
|
||||
250
Telegram/SourceFiles/lang/lang_keys.cpp
Normal file
250
Telegram/SourceFiles/lang/lang_keys.cpp
Normal 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
|
||||
45
Telegram/SourceFiles/lang/lang_keys.h
Normal file
45
Telegram/SourceFiles/lang/lang_keys.h
Normal 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
|
||||
30
Telegram/SourceFiles/lang/lang_numbers_animation.cpp
Normal file
30
Telegram/SourceFiles/lang/lang_numbers_animation.cpp
Normal 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
|
||||
35
Telegram/SourceFiles/lang/lang_numbers_animation.h
Normal file
35
Telegram/SourceFiles/lang/lang_numbers_animation.h
Normal 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
|
||||
14
Telegram/SourceFiles/lang/lang_pch.h
Normal file
14
Telegram/SourceFiles/lang/lang_pch.h
Normal 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"
|
||||
1057
Telegram/SourceFiles/lang/lang_tag.cpp
Normal file
1057
Telegram/SourceFiles/lang/lang_tag.cpp
Normal file
File diff suppressed because it is too large
Load Diff
76
Telegram/SourceFiles/lang/lang_tag.h
Normal file
76
Telegram/SourceFiles/lang/lang_tag.h
Normal 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
|
||||
75
Telegram/SourceFiles/lang/lang_text_entity.cpp
Normal file
75
Telegram/SourceFiles/lang/lang_text_entity.cpp
Normal 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
|
||||
225
Telegram/SourceFiles/lang/lang_text_entity.h
Normal file
225
Telegram/SourceFiles/lang/lang_text_entity.h
Normal 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
|
||||
43
Telegram/SourceFiles/lang/lang_translator.cpp
Normal file
43
Telegram/SourceFiles/lang/lang_translator.cpp
Normal 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
|
||||
20
Telegram/SourceFiles/lang/lang_translator.h
Normal file
20
Telegram/SourceFiles/lang/lang_translator.h
Normal 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
|
||||
175
Telegram/SourceFiles/lang/lang_values.h
Normal file
175
Telegram/SourceFiles/lang/lang_values.h
Normal 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
|
||||
Reference in New Issue
Block a user