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.
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 DOnlineTranslator::DOnlineTranslator(QObject* const parent)
0022     : QObject(parent),
0023       d      (new Private(this))
0024 {
0025     connect(d->stateMachine, &QStateMachine::finished,
0026             this, &DOnlineTranslator::signalFinished);
0027 
0028     connect(d->stateMachine, &QStateMachine::stopped,
0029             this, &DOnlineTranslator::signalFinished);
0030 }
0031 
0032 DOnlineTranslator::~DOnlineTranslator()
0033 {
0034     delete d;
0035 }
0036 
0037 void DOnlineTranslator::translate(const QString& text,
0038                                   Engine engine,
0039                                   Language translationLang,
0040                                   Language sourceLang,
0041                                   Language uiLang)
0042 {
0043     abort();
0044     resetData();
0045 
0046     d->onlyDetectLanguage = false;
0047     d->source             = text;
0048     d->sourceLang         = sourceLang;
0049     d->translationLang    = (translationLang == Auto) ? language(QLocale()) : translationLang;
0050     d->uiLang             = (uiLang == Auto)          ? language(QLocale()) : uiLang;
0051 
0052     // Check if the selected languages are supported by the engine
0053  
0054    if (!isSupportTranslation(engine, d->sourceLang))
0055     {
0056         resetData(ParametersError,
0057                   i18n("Selected source language %1 is not supported for %2",
0058                        languageName(d->sourceLang),
0059                        QString::fromUtf8(QMetaEnum::fromType<Engine>().valueToKey(engine))));
0060 
0061         Q_EMIT signalFinished();
0062 
0063         return;
0064     }
0065 
0066     if (!isSupportTranslation(engine, d->translationLang))
0067     {
0068         resetData(ParametersError,
0069                   i18n("Selected translation language %1 is not supported for %2",
0070                        languageName(d->translationLang),
0071                        QString::fromUtf8(QMetaEnum::fromType<Engine>().valueToKey(engine))));
0072 
0073         Q_EMIT signalFinished();
0074 
0075         return;
0076     }
0077 
0078     if (!isSupportTranslation(engine, d->uiLang))
0079     {
0080         resetData(ParametersError,
0081                   i18n("Selected ui language %1 is not supported for %2",
0082                        languageName(d->uiLang),
0083                        QString::fromUtf8(QMetaEnum::fromType<Engine>().valueToKey(engine))));
0084 
0085         Q_EMIT signalFinished();
0086 
0087         return;
0088     }
0089 
0090     switch (engine)
0091     {
0092         case Google:
0093         {
0094             buildGoogleStateMachine();
0095             break;
0096         }
0097 
0098         case Yandex:
0099         {
0100             buildYandexStateMachine();
0101             break;
0102         }
0103 
0104         case Bing:
0105         {
0106             buildBingStateMachine();
0107             break;
0108         }
0109 
0110         case LibreTranslate:
0111         {
0112             if (d->libreUrl.isEmpty())
0113             {
0114                 resetData(ParametersError,
0115                         i18n("%1 URL can't be empty.",
0116                              QString::fromUtf8(QMetaEnum::fromType<Engine>().valueToKey(engine))));
0117 
0118                 Q_EMIT signalFinished();
0119 
0120                 return;
0121             }
0122 
0123             buildLibreStateMachine();
0124             break;
0125         }
0126 
0127         case Lingva:
0128         {
0129             if (d->lingvaUrl.isEmpty())
0130             {
0131                 resetData(ParametersError,
0132                         i18n("%1 URL can't be empty.",
0133                              QString::fromUtf8(QMetaEnum::fromType<Engine>().valueToKey(engine))));
0134 
0135                 Q_EMIT signalFinished();
0136 
0137                 return;
0138             }
0139 
0140             buildLingvaStateMachine();
0141             break;
0142         }
0143     }
0144 
0145     d->stateMachine->start();
0146 }
0147 
0148 QString DOnlineTranslator::engineName(Engine engine)
0149 {
0150     switch (engine)
0151     {
0152         case Yandex:
0153         {
0154             return QLatin1String("Yandex");
0155         }
0156 
0157         case Bing:
0158         {
0159             return QLatin1String("Bing");
0160         }
0161 
0162         case LibreTranslate:
0163         {
0164             return QLatin1String("Libre Translate");
0165         }
0166 
0167         case Lingva:
0168         {
0169             return QLatin1String("Lingva");
0170         }
0171 
0172         default:
0173         {
0174             return QLatin1String("Google");
0175         }
0176     }
0177 }
0178 
0179 void DOnlineTranslator::detectLanguage(const QString& text, Engine engine)
0180 {
0181     abort();
0182     resetData();
0183 
0184     d->onlyDetectLanguage = true;
0185     d->source             = text;
0186     d->sourceLang         = Auto;
0187     d->translationLang    = English;
0188     d->uiLang             = language(QLocale());
0189 
0190     switch (engine)
0191     {
0192         case Google:
0193         {
0194             buildGoogleDetectStateMachine();
0195             break;
0196         }
0197 
0198         case Yandex:
0199         {
0200             buildYandexDetectStateMachine();
0201             break;
0202         }
0203 
0204         case Bing:
0205         {
0206             buildBingDetectStateMachine();
0207             break;
0208         }
0209 
0210         case LibreTranslate:
0211         {
0212             if (d->libreUrl.isEmpty())
0213             {
0214                 resetData(ParametersError,
0215                         i18n("%1 URL can't be empty.",
0216                              QString::fromUtf8(QMetaEnum::fromType<Engine>().valueToKey(engine))));
0217 
0218                 Q_EMIT signalFinished();
0219 
0220                 return;
0221             }
0222 
0223             buildLibreDetectStateMachine();
0224             break;
0225         }
0226 
0227         case Lingva:
0228         {
0229             if (d->lingvaUrl.isEmpty())
0230             {
0231                 resetData(ParametersError,
0232                         i18n("%1 URL can't be empty.",
0233                              QString::fromUtf8(QMetaEnum::fromType<Engine>().valueToKey(engine))));
0234 
0235                 Q_EMIT signalFinished();
0236 
0237                 return;
0238             }
0239 
0240             buildLingvaDetectStateMachine();
0241             break;
0242         }
0243     }
0244 
0245     d->stateMachine->start();
0246 }
0247 
0248 void DOnlineTranslator::abort()
0249 {
0250     if (d->currentReply != nullptr)
0251     {
0252         d->currentReply->abort();
0253     }
0254 }
0255 
0256 bool DOnlineTranslator::isRunning() const
0257 {
0258     return d->stateMachine->isRunning();
0259 }
0260 
0261 void DOnlineTranslator::slotSkipGarbageText()
0262 {
0263     d->translation.append(sender()->property(Private::s_textProperty).toString());
0264 }
0265 
0266 void DOnlineTranslator::buildSplitNetworkRequest(QState* const parent,
0267                                                  void (DOnlineTranslator::*requestMethod)(),
0268                                                  void (DOnlineTranslator::*parseMethod)(),
0269                                                  const QString& text,
0270                                                  int textLimit)
0271 {
0272     QString unsendedText       = text;
0273     auto* nextTranslationState = new QState(parent);
0274     parent->setInitialState(nextTranslationState);
0275 
0276     while (!unsendedText.isEmpty())
0277     {
0278         auto* currentTranslationState = nextTranslationState;
0279         nextTranslationState          = new QState(parent);
0280 
0281         // Do not translate the part if it looks like garbage
0282 
0283         const int splitIndex          = getSplitIndex(unsendedText, textLimit);
0284 
0285         if (splitIndex == -1)
0286         {
0287             currentTranslationState->setProperty(Private::s_textProperty, unsendedText.left(textLimit));
0288             currentTranslationState->addTransition(nextTranslationState);
0289 
0290             connect(currentTranslationState, &QState::entered,
0291                     this, &DOnlineTranslator::slotSkipGarbageText);
0292 
0293             // Remove the parsed part from the next parsing
0294 
0295             unsendedText = unsendedText.mid(textLimit);
0296         }
0297         else
0298         {
0299             buildNetworkRequestState(currentTranslationState, requestMethod, parseMethod, unsendedText.left(splitIndex));
0300             currentTranslationState->addTransition(currentTranslationState, &QState::finished, nextTranslationState);
0301 
0302             // Remove the parsed part from the next parsing
0303 
0304             unsendedText = unsendedText.mid(splitIndex);
0305         }
0306     }
0307 
0308     nextTranslationState->addTransition(new QFinalState(parent));
0309 }
0310 
0311 void DOnlineTranslator::buildNetworkRequestState(QState* const parent,
0312                                                  void (DOnlineTranslator::*requestMethod)(),
0313                                                  void (DOnlineTranslator::*parseMethod)(),
0314                                                  const QString& text)
0315 {
0316     // Network substates
0317 
0318     auto* requestingState = new QState(parent);
0319     auto* parsingState    = new QState(parent);
0320 
0321     parent->setInitialState(requestingState);
0322 
0323     connect(d->networkManager, &QNetworkAccessManager::finished,
0324             parsingState, [parsingState](QNetworkReply* reply)
0325         {
0326             parsingState->setProperty("QNetworkReply", (quintptr)reply);
0327         }
0328     );
0329 
0330     // Substates transitions
0331 
0332     requestingState->addTransition(d->networkManager, &QNetworkAccessManager::finished, parsingState);
0333     parsingState->addTransition(new QFinalState(parent));
0334 
0335     // Setup requesting state
0336 
0337     requestingState->setProperty(Private::s_textProperty, text);
0338 
0339     connect(requestingState, &QState::entered,
0340             this, requestMethod);
0341 
0342     // Setup parsing state
0343 
0344     connect(parsingState, &QState::entered,
0345             this, parseMethod);
0346 }
0347 
0348 void DOnlineTranslator::resetData(TranslationError error, const QString& errorString)
0349 {
0350     d->error       = error;
0351     d->errorString = errorString;
0352     d->translation.clear();
0353     d->translationTranslit.clear();
0354     d->sourceTranslit.clear();
0355     d->sourceTranscription.clear();
0356     d->translationOptions.clear();
0357 
0358     d->stateMachine->stop();
0359 
0360     for (QAbstractState* state : d->stateMachine->findChildren<QAbstractState*>())
0361     {
0362         if (!d->stateMachine->configuration().contains(state))
0363         {
0364             state->deleteLater();
0365         }
0366     }
0367 }
0368 
0369 QString DOnlineTranslator::languageApiCode(Engine engine, Language lang)
0370 {
0371     if (!isSupportTranslation(engine, lang))
0372     {
0373         return QString();
0374     }
0375 
0376     switch (engine)
0377     {
0378         case Google:
0379         {
0380             return DOnlineTranslator::Private::s_googleLanguageCodes.value(lang, DOnlineTranslator::Private::s_genericLanguageCodes.value(lang));
0381         }
0382 
0383         case Yandex:
0384         {
0385             return DOnlineTranslator::Private::s_yandexLanguageCodes.value(lang, DOnlineTranslator::Private::s_genericLanguageCodes.value(lang));
0386         }
0387 
0388         case Bing:
0389         {
0390             return DOnlineTranslator::Private::s_bingLanguageCodes.value(lang, DOnlineTranslator::Private::s_genericLanguageCodes.value(lang));
0391         }
0392 
0393         case LibreTranslate:
0394         {
0395             return DOnlineTranslator::Private::s_genericLanguageCodes.value(lang);
0396         }
0397 
0398         case Lingva:
0399         {
0400             return DOnlineTranslator::Private::s_lingvaLanguageCodes.value(lang, DOnlineTranslator::Private::s_genericLanguageCodes.value(lang));
0401         }
0402     }
0403 
0404     Q_UNREACHABLE();
0405 }
0406 
0407 DOnlineTranslator::Language DOnlineTranslator::language(Engine engine, const QString& langCode)
0408 {
0409     // Engine exceptions
0410 
0411     switch (engine)
0412     {
0413         case Google:
0414         {
0415             return DOnlineTranslator::Private::s_googleLanguageCodes.key(langCode, DOnlineTranslator::Private::s_genericLanguageCodes.key(langCode, NoLanguage));
0416         }
0417 
0418         case Yandex:
0419         {
0420             return DOnlineTranslator::Private::s_yandexLanguageCodes.key(langCode, DOnlineTranslator::Private::s_genericLanguageCodes.key(langCode, NoLanguage));
0421         }
0422 
0423         case Bing:
0424         {
0425             return DOnlineTranslator::Private::s_bingLanguageCodes.key(langCode, DOnlineTranslator::Private::s_genericLanguageCodes.key(langCode, NoLanguage));
0426         }
0427 
0428         case LibreTranslate:
0429         {
0430             return DOnlineTranslator::Private::s_genericLanguageCodes.key(langCode, NoLanguage);
0431         }
0432 
0433         case Lingva:
0434         {
0435             return DOnlineTranslator::Private::s_lingvaLanguageCodes.key(langCode, DOnlineTranslator::Private::s_genericLanguageCodes.key(langCode, NoLanguage));
0436         }
0437     }
0438 
0439     Q_UNREACHABLE();
0440 }
0441 
0442 int DOnlineTranslator::getSplitIndex(const QString& untranslatedText, int limit)
0443 {
0444     if (untranslatedText.size() < limit)
0445     {
0446         return limit;
0447     }
0448 
0449     int splitIndex = untranslatedText.lastIndexOf(QLatin1String(". "), limit - 1);
0450 
0451     if (splitIndex != -1)
0452     {
0453         return splitIndex + 1;
0454     }
0455 
0456     splitIndex = untranslatedText.lastIndexOf(QLatin1Char(' '), limit - 1);
0457 
0458     if (splitIndex != -1)
0459     {
0460         return splitIndex + 1;
0461     }
0462 
0463     splitIndex = untranslatedText.lastIndexOf(QLatin1Char('\n'), limit - 1);
0464 
0465     if (splitIndex != -1)
0466     {
0467         return splitIndex + 1;
0468     }
0469 
0470     // Non-breaking space
0471 
0472     splitIndex = untranslatedText.lastIndexOf(QChar(0x00a0), limit - 1);
0473 
0474     if (splitIndex != -1)
0475     {
0476         return splitIndex + 1;
0477     }
0478 
0479     // If the text has not passed any check and is most likely garbage
0480 
0481     return limit;
0482 }
0483 
0484 bool DOnlineTranslator::isContainsSpace(const QString& text)
0485 {
0486     return std::any_of(text.cbegin(), text.cend(), [](QChar symbol)
0487         {
0488             return symbol.isSpace();
0489         }
0490     );
0491 }
0492 
0493 void DOnlineTranslator::addSpaceBetweenParts(QString& text)
0494 {
0495     if (text.isEmpty())
0496     {
0497         return;
0498     }
0499 
0500 #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
0501 
0502     if (!text.back().isSpace())
0503     {
0504 
0505 #else
0506 
0507     if (!text.at(text.size() - 1).isSpace())
0508     {
0509 
0510 #endif
0511 
0512         text.append(QLatin1Char(' '));
0513     }
0514 }
0515 
0516 } // namespace Digikam
0517 
0518 #include "moc_donlinetranslator.cpp"