File indexing completed on 2025-04-27 03:58:38

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2018-02-22
0007  * Description : A text translator using web-services - Bing methods.
0008  *
0009  * SPDX-FileCopyrightText: 2018-2022 by Hennadii Chernyshchyk <genaloner at gmail dot com>
0010  * SPDX-FileCopyrightText: 2021-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0011  *
0012  * SPDX-License-Identifier: GPL-2.0-or-later
0013  *
0014  * ============================================================ */
0015 
0016 #include "donlinetranslator_p.h"
0017 
0018 namespace Digikam
0019 {
0020 
0021 void DOnlineTranslator::slotRequestBingCredentials()
0022 {
0023     const QUrl url(QStringLiteral("https://www.bing.com/translator"));
0024     d->currentReply = d->networkManager->get(QNetworkRequest(url));
0025 }
0026 
0027 void DOnlineTranslator::slotParseBingCredentials()
0028 {
0029     if ((quintptr)d->currentReply.data() != sender()->property("QNetworkReply").value<quintptr>())
0030     {
0031         return;
0032     }
0033 
0034     d->currentReply->deleteLater();
0035 
0036     if (d->currentReply->error() != QNetworkReply::NoError)
0037     {
0038         resetData(NetworkError, d->currentReply->errorString());
0039         return;
0040     }
0041 
0042     const QByteArray webSiteData            = d->currentReply->readAll();
0043     const QByteArray credentialsBeginString = "var params_RichTranslateHelper = [";
0044     const int credentialsBeginPos           = webSiteData.indexOf(credentialsBeginString);
0045 
0046     if (credentialsBeginPos == -1)
0047     {
0048         resetData(ParsingError, i18n("Error: Unable to find Bing credentials in web version."));
0049         return;
0050     }
0051 
0052     const int keyBeginPos = credentialsBeginPos + credentialsBeginString.size();
0053     const int keyEndPos   = webSiteData.indexOf(',', keyBeginPos);
0054 
0055     if (keyEndPos == -1)
0056     {
0057         resetData(ParsingError, i18n("Error: Unable to extract Bing key from web version."));
0058         return;
0059     }
0060 
0061     Private::s_bingKey               = webSiteData.mid(keyBeginPos, keyEndPos - keyBeginPos);
0062     const int tokenBeginPos = keyEndPos + 2; // Skip two symbols instead of one because the value is enclosed in quotes
0063     const int tokenEndPos   = webSiteData.indexOf('"', tokenBeginPos);
0064 
0065     if (tokenEndPos == -1)
0066     {
0067         resetData(ParsingError, i18n("Error: Unable to extract Bing token from web version."));
0068         return;
0069     }
0070 
0071     Private::s_bingToken          = webSiteData.mid(tokenBeginPos, tokenEndPos - tokenBeginPos);
0072     const int igBeginPos = webSiteData.indexOf("IG");
0073     const int igEndPos   = webSiteData.indexOf('"', igBeginPos + 2);
0074 
0075     if (igEndPos == -1)
0076     {
0077         resetData(ParsingError, i18n("Error: Unable to extract additional Bing information from web version."));
0078         return;
0079     }
0080 
0081     Private::s_bingIg              = QString::fromUtf8(webSiteData.mid(igBeginPos, igEndPos - igBeginPos));
0082     const int iidBeginPos = webSiteData.indexOf("data-iid");
0083     const int iidEndPos   = webSiteData.indexOf('"', iidBeginPos + 2);
0084 
0085     if (iidEndPos == -1)
0086     {
0087         resetData(ParsingError, i18n("Error: Unable to extract additional Bing information from web version."));
0088         return;
0089     }
0090 
0091     Private::s_bingIid = QString::fromUtf8(webSiteData.mid(iidBeginPos, iidEndPos - iidBeginPos));
0092 }
0093 
0094 void DOnlineTranslator::slotRequestBingTranslate()
0095 {
0096     const QString sourceText = sender()->property(Private::s_textProperty).toString();
0097 
0098     // Generate POST data
0099 
0100     const QByteArray postData = "&text="     + QUrl::toPercentEncoding(sourceText)
0101                               + "&fromLang=" + languageApiCode(Bing, d->sourceLang).toUtf8()
0102                               + "&to="       + languageApiCode(Bing, d->translationLang).toUtf8()
0103                               + "&token="    + Private::s_bingToken
0104                               + "&key="      + Private::s_bingKey;
0105 
0106     QUrl url(QStringLiteral("https://www.bing.com/ttranslatev3"));
0107     url.setQuery(QStringLiteral("IG=%1&IID=%2").arg(Private::s_bingIg, Private::s_bingIid));
0108 
0109     // Setup request
0110 
0111     QNetworkRequest request;
0112     request.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/x-www-form-urlencoded"));
0113     request.setHeader(QNetworkRequest::UserAgentHeader, QString::fromUtf8("%1/%2").arg(QCoreApplication::applicationName()).arg(QCoreApplication::applicationVersion()));
0114     request.setUrl(url);
0115 
0116     // Make reply
0117 
0118     d->currentReply = d->networkManager->post(request, postData);
0119 }
0120 
0121 void DOnlineTranslator::slotParseBingTranslate()
0122 {
0123     d->currentReply->deleteLater();
0124 
0125     // Check for errors
0126 
0127     if (d->currentReply->error() != QNetworkReply::NoError)
0128     {
0129         resetData(NetworkError, d->currentReply->errorString());
0130         return;
0131     }
0132 
0133     // Parse translation data
0134 
0135     const QJsonDocument jsonResponse = QJsonDocument::fromJson(d->currentReply->readAll());
0136     const QJsonObject responseObject = jsonResponse.array().first().toObject();
0137 
0138     if (d->sourceLang == Auto)
0139     {
0140         const QString langCode = responseObject.value(QStringLiteral("detectedLanguage")).toObject().value(QStringLiteral("language")).toString();
0141         d->sourceLang           = language(Bing, langCode);
0142 
0143         if (d->sourceLang == NoLanguage)
0144         {
0145             resetData(ParsingError, i18n("Error: Unable to parse autodetected language"));
0146             return;
0147         }
0148 
0149         if (d->onlyDetectLanguage)
0150             return;
0151     }
0152 
0153     const QJsonObject translationsObject = responseObject.value(QStringLiteral("translations")).toArray().first().toObject();
0154     d->translation                       += translationsObject.value(QStringLiteral("text")).toString();
0155     d->translationTranslit               += translationsObject.value(QStringLiteral("transliteration")).toObject().value(QStringLiteral("text")).toString();
0156 }
0157 
0158 void DOnlineTranslator::slotRequestBingDictionary()
0159 {
0160     // Check if language is supported (need to check here because language may be autodetected)
0161 
0162     if (!isSupportDictionary(Bing, d->sourceLang, d->translationLang) && !d->source.contains(QLatin1Char(' ')))
0163     {
0164         auto* state = qobject_cast<QState *>(sender());
0165         state->addTransition(new QFinalState(state->parentState()));
0166         return;
0167     }
0168 
0169     // Generate POST data
0170 
0171     const QByteArray postData = "&text=" + QUrl::toPercentEncoding(sender()->property(Private::s_textProperty).toString())
0172                               + "&from=" + languageApiCode(Bing, d->sourceLang).toUtf8()
0173                               + "&to="   + languageApiCode(Bing, d->translationLang).toUtf8();
0174 
0175     QNetworkRequest request;
0176     request.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/x-www-form-urlencoded"));
0177     request.setUrl(QUrl(QStringLiteral("https://www.bing.com/tlookupv3")));
0178 
0179     d->currentReply = d->networkManager->post(request, postData);
0180 }
0181 
0182 void DOnlineTranslator::slotParseBingDictionary()
0183 {
0184     if ((quintptr)d->currentReply.data() != sender()->property("QNetworkReply").value<quintptr>())
0185     {
0186         return;
0187     }
0188 
0189     d->currentReply->deleteLater();
0190 
0191     // Check for errors
0192 
0193     if (d->currentReply->error() != QNetworkReply::NoError)
0194     {
0195         resetData(NetworkError, d->currentReply->errorString());
0196         return;
0197     }
0198 
0199     const QJsonDocument jsonResponse = QJsonDocument::fromJson(d->currentReply->readAll());
0200     const QJsonObject responseObject = jsonResponse.array().first().toObject();
0201 
0202     for (const QJsonValueRef dictionaryData : responseObject.value(QStringLiteral("translations")).toArray())
0203     {
0204         const QJsonObject dictionaryObject = dictionaryData.toObject();
0205         const QString typeOfSpeech         = dictionaryObject.value(QStringLiteral("posTag")).toString().toLower();
0206         const QString word                 = dictionaryObject.value(QStringLiteral("displayTarget")).toString().toLower();
0207         const QJsonArray translationsArray = dictionaryObject.value(QStringLiteral("backTranslations")).toArray();
0208         QStringList translations;
0209         translations.reserve(translationsArray.size());
0210 
0211         for (const QJsonValue &wordTranslation : translationsArray)
0212         {
0213             translations.append(wordTranslation.toObject().value(QStringLiteral("displayText")).toString());
0214         }
0215 
0216         d->translationOptions[typeOfSpeech].append({word, {}, translations});
0217     }
0218 }
0219 
0220 void DOnlineTranslator::buildBingStateMachine()
0221 {
0222     // States
0223 
0224     auto* credentialsState = new QState(d->stateMachine); // Generate credentials from web version first to access API
0225     auto* translationState = new QState(d->stateMachine);
0226     auto* dictionaryState  = new QState(d->stateMachine);
0227     auto* finalState       = new QFinalState(d->stateMachine);
0228     d->stateMachine->setInitialState(credentialsState);
0229 
0230     // Transitions
0231 
0232     credentialsState->addTransition(credentialsState, &QState::finished, translationState);
0233     translationState->addTransition(translationState, &QState::finished, dictionaryState);
0234     dictionaryState->addTransition(dictionaryState, &QState::finished, finalState);
0235 
0236     // Setup credentials state
0237 
0238     if (Private::s_bingKey.isEmpty() || Private::s_bingToken.isEmpty())
0239     {
0240         buildNetworkRequestState(credentialsState,
0241                                  &DOnlineTranslator::slotRequestBingCredentials,
0242                                  &DOnlineTranslator::slotParseBingCredentials);
0243     }
0244     else
0245     {
0246         credentialsState->setInitialState(new QFinalState(credentialsState));
0247     }
0248 
0249     // Setup translation state
0250 
0251     buildSplitNetworkRequest(translationState,
0252                              &DOnlineTranslator::slotRequestBingTranslate,
0253                              &DOnlineTranslator::slotParseBingTranslate,
0254                              d->source,
0255                              Private::s_bingTranslateLimit);
0256 
0257     // Setup dictionary state
0258 
0259     if (d->translationOptionsEnabled && !isContainsSpace(d->source))
0260     {
0261         buildNetworkRequestState(dictionaryState,
0262                                  &DOnlineTranslator::slotRequestBingDictionary,
0263                                  &DOnlineTranslator::slotParseBingDictionary,
0264                                  d->source);
0265     }
0266     else
0267     {
0268         dictionaryState->setInitialState(new QFinalState(dictionaryState));
0269     }
0270 }
0271 
0272 void DOnlineTranslator::buildBingDetectStateMachine()
0273 {
0274     // States
0275 
0276     auto* detectState = new QState(d->stateMachine);
0277     auto* finalState  = new QFinalState(d->stateMachine);
0278     d->stateMachine->setInitialState(detectState);
0279 
0280     detectState->addTransition(detectState, &QState::finished, finalState);
0281 
0282     // Setup translation state
0283 
0284     const QString text = d->source.left(getSplitIndex(d->source, Private::s_bingTranslateLimit));
0285 
0286     buildNetworkRequestState(detectState,
0287                              &DOnlineTranslator::slotRequestBingTranslate,
0288                              &DOnlineTranslator::slotParseBingTranslate,
0289                              text);
0290 }
0291 
0292 } // namespace Digikam