diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index a1ad8e4912..b5b4c7436a 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1239,6 +1239,8 @@ PRIVATE lang/translate_mtproto_provider.h lang/translate_provider.cpp lang/translate_provider.h + lang/translate_url_provider.cpp + lang/translate_url_provider.h layout/layout_document_generic_preview.cpp layout/layout_document_generic_preview.h layout/layout_item_base.cpp diff --git a/Telegram/SourceFiles/lang/translate_provider.cpp b/Telegram/SourceFiles/lang/translate_provider.cpp index 15b79c5b99..454c54aad9 100644 --- a/Telegram/SourceFiles/lang/translate_provider.cpp +++ b/Telegram/SourceFiles/lang/translate_provider.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "lang/translate_provider.h" +#include "base/options.h" #include "core/application.h" #include "core/core_settings.h" #include "data/data_msg_id.h" @@ -14,12 +15,29 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "history/history_item.h" #include "lang/translate_mtproto_provider.h" +#include "lang/translate_url_provider.h" #include "platform/platform_translate_provider.h" +namespace { + +base::options::option OptionTranslateUrlTemplate({ + .id = "translate-url-template", + .name = "Translate URL template", + .description = "Template URL for custom translation provider." + " Supports %q text, %f source language and %t target language.", +}); + +} // namespace + namespace Ui { std::unique_ptr CreateTranslateProvider( not_null session) { + const auto urlTemplate = OptionTranslateUrlTemplate.value(); + if (!urlTemplate.isEmpty() + && urlTemplate.contains(u"%q"_q)) { + return CreateUrlTranslateProvider(urlTemplate); + } if (Core::App().settings().usePlatformTranslation() && Platform::IsTranslateProviderAvailable()) { return Platform::CreateTranslateProvider(); diff --git a/Telegram/SourceFiles/lang/translate_url_provider.cpp b/Telegram/SourceFiles/lang/translate_url_provider.cpp new file mode 100644 index 0000000000..7d4c137caa --- /dev/null +++ b/Telegram/SourceFiles/lang/translate_url_provider.cpp @@ -0,0 +1,247 @@ +/* +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/translate_url_provider.h" + +#include "spellcheck/platform/platform_language.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Ui { +namespace { + +[[nodiscard]] bool SkipJsonKey(const QString &key) { + return (key.compare(u"code"_q, Qt::CaseInsensitive) == 0); +} + +[[nodiscard]] QString DetectFromLanguage(const QString &text) { +#ifndef TDESKTOP_DISABLE_SPELLCHECK + const auto result = Platform::Language::Recognize(text); + return result.known() ? result.twoLetterCode() : u"auto"_q; +#else // TDESKTOP_DISABLE_SPELLCHECK + return u"auto"_q; +#endif // !TDESKTOP_DISABLE_SPELLCHECK +} + +[[nodiscard]] QString JsonValueToText(const QJsonValue &v) { + switch (v.type()) { + case QJsonValue::Null: return u"null"_q; + case QJsonValue::Bool: return v.toBool() + ? u"true"_q + : u"false"_q; + case QJsonValue::Double: return QString::number(v.toDouble(), 'g', 15); + case QJsonValue::String: return v.toString().trimmed(); + case QJsonValue::Array: return QString::fromUtf8( + QJsonDocument(v.toArray()).toJson(QJsonDocument::Compact)); + case QJsonValue::Object: return QString::fromUtf8( + QJsonDocument(v.toObject()).toJson(QJsonDocument::Compact)); + case QJsonValue::Undefined: return QString(); + } + return QString(); +} + +[[nodiscard]] std::optional ParseSegmentedArrayResponse( + const QJsonDocument &parsed) { + if (!parsed.isArray()) { + return std::nullopt; + } + const auto root = parsed.array(); + if (root.isEmpty() || !root[0].isArray()) { + return std::nullopt; + } + const auto segments = root[0].toArray(); + auto translated = QString(); + for (const auto &segmentValue : segments) { + if (!segmentValue.isArray()) { + return std::nullopt; + } + const auto segment = segmentValue.toArray(); + if (segment.isEmpty()) { + return std::nullopt; + } + if (!segment[0].isString()) { + return std::nullopt; + } + translated += segment[0].toString(); + } + if (translated.trimmed().isEmpty()) { + return std::nullopt; + } + return translated; +} + +struct JsonLine { + QString name; + QString value; + int length = 0; +}; + +void CollectJsonLines( + const QString &name, + const QJsonValue &value, + std::vector &lines) { + if (value.isObject()) { + const auto object = value.toObject(); + for (auto i = object.constBegin(); i != object.constEnd(); ++i) { + if (SkipJsonKey(i.key())) { + continue; + } + CollectJsonLines( + name.isEmpty() ? i.key() : (name + '.' + i.key()), + i.value(), + lines); + } + return; + } + if (value.isArray()) { + const auto array = value.toArray(); + for (auto i = 0; i != array.size(); ++i) { + CollectJsonLines( + u"%1[%2]"_q.arg(name).arg(i), + array.at(i), + lines); + } + return; + } + const auto text = JsonValueToText(value); + if (text.isEmpty()) { + return; + } + lines.push_back(JsonLine{ + .name = name, + .value = text, + .length = int(text.size()), + }); +} + +[[nodiscard]] std::optional FormatJsonResponse( + const QByteArray &body) { + auto error = QJsonParseError(); + const auto parsed = QJsonDocument::fromJson(body, &error); + if (error.error != QJsonParseError::NoError) { + return std::nullopt; + } + if (const auto parsedArray = ParseSegmentedArrayResponse(parsed)) { + return parsedArray; + } + auto lines = std::vector(); + if (parsed.isObject()) { + const auto object = parsed.object(); + for (auto i = object.constBegin(); i != object.constEnd(); ++i) { + if (SkipJsonKey(i.key())) { + continue; + } + CollectJsonLines(i.key(), i.value(), lines); + } + } else if (parsed.isArray()) { + const auto array = parsed.array(); + for (auto i = 0; i != array.size(); ++i) { + CollectJsonLines(u"[%1]"_q.arg(i), array.at(i), lines); + } + } + if (lines.empty()) { + return QString::fromUtf8(body); + } + ranges::sort(lines, [](const JsonLine &a, const JsonLine &b) { + return (a.length != b.length) + ? (a.length > b.length) + : (a.name < b.name); + }); + auto result = QString(); + result.reserve(lines.size() * 16); + for (auto i = 0; i != int(lines.size()); ++i) { + const auto &line = lines[i]; + if (!line.name.isEmpty()) { + result += line.name; + result += '\n'; + } + result += line.value; + if (i + 1 != int(lines.size())) { + result += "\n\n"; + } + } + return result; +} + +class UrlTranslateProvider final : public TranslateProvider { +public: + explicit UrlTranslateProvider(QString urlTemplate) + : _urlTemplate(std::move(urlTemplate)) { + } + + [[nodiscard]] bool supportsMessageId() const override { + return false; + } + + void request( + TranslateProviderRequest request, + LanguageId to, + Fn done) override { + if (request.text.text.isEmpty()) { + done(TranslateProviderResult{ + .error = TranslateProviderError::Unknown, + }); + return; + } + auto url = _urlTemplate; + const auto from = DetectFromLanguage(request.text.text); + const auto toCode = to.twoLetterCode(); + url.replace( + u"%q"_q, + QString::fromLatin1( + QUrl::toPercentEncoding(request.text.text.toHtmlEscaped()))); + url.replace( + u"%f"_q, + QString::fromLatin1(QUrl::toPercentEncoding(from))); + url.replace( + u"%t"_q, + QString::fromLatin1(QUrl::toPercentEncoding(toCode))); + const auto requestUrl = QUrl(url); + if (!requestUrl.isValid()) { + done(TranslateProviderResult{ + .error = TranslateProviderError::Unknown, + }); + return; + } + auto networkRequest = QNetworkRequest(requestUrl); + const auto reply = _network.get(networkRequest); + QObject::connect(reply, &QNetworkReply::finished, [=] { + auto result = TranslateProviderResult(); + if (reply->error() != QNetworkReply::NoError) { + result.error = TranslateProviderError::Unknown; + } else { + const auto body = reply->readAll(); + const auto formatted = FormatJsonResponse(body).value_or( + QString::fromUtf8(body)); + result.text = TextWithEntities{ formatted }; + } + done(std::move(result)); + reply->deleteLater(); + }); + } + +private: + const QString _urlTemplate; + QNetworkAccessManager _network; + +}; + +} // namespace + +std::unique_ptr CreateUrlTranslateProvider( + QString urlTemplate) { + return std::make_unique(std::move(urlTemplate)); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/lang/translate_url_provider.h b/Telegram/SourceFiles/lang/translate_url_provider.h new file mode 100644 index 0000000000..8bb1efbb03 --- /dev/null +++ b/Telegram/SourceFiles/lang/translate_url_provider.h @@ -0,0 +1,17 @@ +/* +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 "translate_provider.h" + +namespace Ui { + +[[nodiscard]] std::unique_ptr CreateUrlTranslateProvider( + QString urlTemplate); + +} // namespace Ui