File indexing completed on 2024-04-21 03:54:22

0001 /*  This file is part of the KDE libraries
0002     SPDX-FileCopyrightText: 2006, 2013 Chusslove Illich <caslav.ilic@gmx.net>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 // We don't want i18n to be expanded to i18nd here
0008 #undef TRANSLATION_DOMAIN
0009 
0010 #include <cstdlib>
0011 
0012 #include <QByteArray>
0013 #include <QCoreApplication>
0014 #include <QDir>
0015 #include <QFile>
0016 #include <QFileInfo>
0017 #include <QHash>
0018 #include <QLibrary>
0019 #include <QList>
0020 #include <QMutexLocker>
0021 #include <QPluginLoader>
0022 #include <QRecursiveMutex>
0023 #include <QStandardPaths>
0024 #include <QStringList>
0025 
0026 #include <common_helpers_p.h>
0027 #include <kcatalog_p.h>
0028 #include <klocalizedstring.h>
0029 #include <ktranscript_p.h>
0030 #include <kuitsetup_p.h>
0031 
0032 #include "ki18n_logging.h"
0033 
0034 // Truncate string, for output of long messages.
0035 static QString shortenMessage(const QString &str)
0036 {
0037     const int maxlen = 20;
0038     if (str.length() <= maxlen) {
0039         return str;
0040     } else {
0041         return QStringView(str).left(maxlen) + QLatin1String("...");
0042     }
0043 }
0044 
0045 static void splitLocale(const QString &aLocale, QString &language, QString &country, QString &modifier, QString &charset)
0046 {
0047     QString locale = aLocale;
0048 
0049     language.clear();
0050     country.clear();
0051     modifier.clear();
0052     charset.clear();
0053 
0054     // In case there are several concatenated locale specifications,
0055     // truncate all but first.
0056     int f = locale.indexOf(QLatin1Char(':'));
0057     if (f >= 0) {
0058         locale.truncate(f);
0059     }
0060 
0061     // now decompose into [language[_territory][.codeset][@modifier]]
0062     f = locale.indexOf(QLatin1Char('@'));
0063     if (f >= 0) {
0064         modifier = locale.mid(f + 1);
0065         locale.truncate(f);
0066     }
0067 
0068     f = locale.indexOf(QLatin1Char('.'));
0069     if (f >= 0) {
0070         charset = locale.mid(f + 1);
0071         locale.truncate(f);
0072     }
0073 
0074     f = locale.indexOf(QLatin1Char('_'));
0075     if (f >= 0) {
0076         country = locale.mid(f + 1);
0077         locale.truncate(f);
0078     }
0079 
0080     language = locale;
0081 }
0082 
0083 static void appendLocaleString(QStringList &languages, const QString &value)
0084 {
0085     // Process the value to create possible combinations.
0086     QString language;
0087     QString country;
0088     QString modifier;
0089     QString charset;
0090     splitLocale(value, language, country, modifier, charset);
0091 
0092     if (language.isEmpty()) {
0093         return;
0094     }
0095 
0096     if (!country.isEmpty() && !modifier.isEmpty()) {
0097         languages += language + QLatin1Char('_') + country + QLatin1Char('@') + modifier;
0098     }
0099     // NOTE: Priority is unclear in case both the country and
0100     // the modifier are present. Should really language@modifier be of
0101     // higher priority than language_country?
0102     // In at least one case (Serbian language), it is better this way.
0103     if (!modifier.isEmpty()) {
0104         languages += language + QLatin1Char('@') + modifier;
0105     }
0106     if (!country.isEmpty()) {
0107         languages += language + QLatin1Char('_') + country;
0108     }
0109     languages += language;
0110 }
0111 
0112 static void appendLanguagesFromVariable(QStringList &languages, const char *envar, bool isList = false)
0113 {
0114     QByteArray qenvar(qgetenv(envar));
0115     if (!qenvar.isEmpty()) {
0116         QString value = QFile::decodeName(qenvar);
0117         if (isList) {
0118             const auto listLanguages = value.split(QLatin1Char(':'), Qt::SkipEmptyParts);
0119             for (const QString &v : listLanguages) {
0120                 appendLocaleString(languages, v);
0121             }
0122         } else {
0123             appendLocaleString(languages, value);
0124         }
0125     }
0126 }
0127 
0128 #if !defined(Q_OS_UNIX) || defined(Q_OS_ANDROID)
0129 static void appendLanguagesFromQLocale(QStringList &languages, const QLocale &locale)
0130 {
0131     const QStringList uiLangs = locale.uiLanguages();
0132     for (QString value : uiLangs) { // no const ref because of replace() below
0133         appendLocaleString(languages, value.replace(QLatin1Char('-'), QLatin1Char('_')));
0134     }
0135 }
0136 #endif
0137 
0138 // Extract the first country code from a list of language_COUNTRY strings.
0139 // Country code is converted to all lower case letters.
0140 static QString extractCountry(const QStringList &languages)
0141 {
0142     QString country;
0143     for (const QString &language : languages) {
0144         int pos1 = language.indexOf(QLatin1Char('_'));
0145         if (pos1 >= 0) {
0146             ++pos1;
0147             int pos2 = pos1;
0148             while (pos2 < language.length() && language[pos2].isLetter()) {
0149                 ++pos2;
0150             }
0151             country = language.mid(pos1, pos2 - pos1);
0152             break;
0153         }
0154     }
0155     country = country.toLower();
0156     return country;
0157 }
0158 
0159 typedef qulonglong pluraln;
0160 typedef qlonglong intn;
0161 typedef qulonglong uintn;
0162 typedef double realn;
0163 
0164 class KLocalizedStringPrivate
0165 {
0166     friend class KLocalizedString;
0167 
0168     QByteArray domain;
0169     QStringList languages;
0170     Kuit::VisualFormat format;
0171     QByteArray context;
0172     QByteArray text;
0173     QByteArray plural;
0174     QStringList arguments;
0175     QList<QVariant> values;
0176     QHash<int, KLocalizedString> klsArguments;
0177     QHash<int, int> klsArgumentFieldWidths;
0178     QHash<int, QChar> klsArgumentFillChars;
0179     bool numberSet;
0180     pluraln number;
0181     int numberOrdinal;
0182     QHash<QString, QString> dynamicContext;
0183     bool markupAware;
0184     bool relaxedSubs;
0185 
0186     KLocalizedStringPrivate()
0187         : format()
0188         , numberSet(false)
0189         , markupAware(false)
0190         , relaxedSubs(false)
0191     {
0192     }
0193 
0194     static void translateRaw(const QByteArray &domain,
0195                              const QStringList &languages,
0196                              const QByteArray &msgctxt,
0197                              const QByteArray &msgid,
0198                              const QByteArray &msgid_plural,
0199                              qulonglong n,
0200                              QString &language,
0201                              QString &translation);
0202 
0203     QString toString(const QByteArray &domain, const QStringList &languages, Kuit::VisualFormat format, bool isArgument = false) const;
0204     QString substituteSimple(const QString &translation, const QStringList &arguments, QChar plchar = QLatin1Char('%'), bool isPartial = false) const;
0205     QString formatMarkup(const QByteArray &domain, const QString &language, const QString &context, const QString &text, Kuit::VisualFormat format) const;
0206     QString substituteTranscript(const QString &scriptedTranslation,
0207                                  const QString &language,
0208                                  const QString &country,
0209                                  const QString &ordinaryTranslation,
0210                                  const QStringList &arguments,
0211                                  const QList<QVariant> &values,
0212                                  bool &fallback) const;
0213     int resolveInterpolation(const QString &scriptedTranslation,
0214                              int pos,
0215                              const QString &language,
0216                              const QString &country,
0217                              const QString &ordinaryTranslation,
0218                              const QStringList &arguments,
0219                              const QList<QVariant> &values,
0220                              QString &result,
0221                              bool &fallback) const;
0222     QVariant segmentToValue(const QString &segment) const;
0223     QString postTranscript(const QString &pcall,
0224                            const QString &language,
0225                            const QString &country,
0226                            const QString &finalTranslation,
0227                            const QStringList &arguments,
0228                            const QList<QVariant> &values) const;
0229 
0230     static const KCatalog &getCatalog(const QByteArray &domain, const QString &language);
0231     static void locateScriptingModule(const QByteArray &domain, const QString &language);
0232 
0233     static void loadTranscript();
0234 
0235     void checkNumber(pluraln a)
0236     {
0237         if (!plural.isEmpty() && !numberSet) {
0238             number = a;
0239             numberSet = true;
0240             numberOrdinal = arguments.size();
0241         }
0242     }
0243 };
0244 
0245 typedef QHash<QString, KCatalog *> KCatalogPtrHash;
0246 
0247 class KLocalizedStringPrivateStatics
0248 {
0249 public:
0250     QHash<QByteArray, KCatalogPtrHash> catalogs;
0251     QStringList languages;
0252 
0253     QByteArray ourDomain;
0254     QByteArray applicationDomain;
0255     const QString codeLanguage;
0256     QStringList localeLanguages;
0257 
0258     const QString theFence;
0259     const QString startInterp;
0260     const QString endInterp;
0261     const QChar scriptPlchar;
0262     const QChar scriptVachar;
0263 
0264     const QString scriptDir;
0265     QHash<QString, QList<QByteArray>> scriptModules;
0266     QList<QStringList> scriptModulesToLoad;
0267 
0268     bool loadTranscriptCalled;
0269     KTranscript *ktrs;
0270 
0271     QHash<QString, KuitFormatter *> formatters;
0272 
0273     QList<QByteArray> qtDomains;
0274     QList<int> qtDomainInsertCount;
0275 
0276     QRecursiveMutex klspMutex;
0277 
0278     KLocalizedStringPrivateStatics();
0279     ~KLocalizedStringPrivateStatics();
0280 
0281     void initializeLocaleLanguages();
0282 };
0283 
0284 KLocalizedStringPrivateStatics::KLocalizedStringPrivateStatics()
0285     : catalogs()
0286     , languages()
0287 
0288     , ourDomain(QByteArrayLiteral("ki18n6"))
0289     , applicationDomain()
0290     , codeLanguage(QStringLiteral("en_US"))
0291     , localeLanguages()
0292 
0293     , theFence(QStringLiteral("|/|"))
0294     , startInterp(QStringLiteral("$["))
0295     , endInterp(QStringLiteral("]"))
0296     , scriptPlchar(QLatin1Char('%'))
0297     , scriptVachar(QLatin1Char('^'))
0298 
0299     , scriptDir(QStringLiteral("LC_SCRIPTS"))
0300     , scriptModules()
0301     , scriptModulesToLoad()
0302 
0303     , loadTranscriptCalled(false)
0304     , ktrs(nullptr)
0305 
0306     , formatters()
0307 
0308     , qtDomains()
0309     , qtDomainInsertCount()
0310 {
0311     initializeLocaleLanguages();
0312     languages = localeLanguages;
0313 }
0314 
0315 KLocalizedStringPrivateStatics::~KLocalizedStringPrivateStatics()
0316 {
0317     for (const KCatalogPtrHash &languageCatalogs : std::as_const(catalogs)) {
0318         qDeleteAll(languageCatalogs);
0319     }
0320     // ktrs is handled by QLibrary.
0321     // delete ktrs;
0322     qDeleteAll(formatters);
0323 }
0324 
0325 Q_GLOBAL_STATIC(KLocalizedStringPrivateStatics, staticsKLSP)
0326 
0327 void KLocalizedStringPrivateStatics::initializeLocaleLanguages()
0328 {
0329     QMutexLocker lock(&klspMutex);
0330 
0331     // Collect languages by same order of priority as for gettext(3).
0332     // LANGUAGE contains list of language codes, not locale string.
0333     appendLanguagesFromVariable(localeLanguages, "LANGUAGE", true);
0334     appendLanguagesFromVariable(localeLanguages, "LC_ALL");
0335     appendLanguagesFromVariable(localeLanguages, "LC_MESSAGES");
0336     appendLanguagesFromVariable(localeLanguages, "LANG");
0337 #if !defined(Q_OS_UNIX) || defined(Q_OS_ANDROID)
0338     // For non UNIX platforms the environment variables might not
0339     // suffice so we add system locale UI languages, too.
0340     appendLanguagesFromQLocale(localeLanguages, QLocale::system());
0341 #endif
0342 }
0343 
0344 KLocalizedString::KLocalizedString()
0345     : d(new KLocalizedStringPrivate)
0346 {
0347 }
0348 
0349 KLocalizedString::KLocalizedString(const char *domain, const char *context, const char *text, const char *plural, bool markupAware)
0350     : d(new KLocalizedStringPrivate)
0351 {
0352     d->domain = domain;
0353     d->languages.clear();
0354     d->format = Kuit::UndefinedFormat;
0355     d->context = context;
0356     d->text = text;
0357     d->plural = plural;
0358     d->numberSet = false;
0359     d->number = 0;
0360     d->numberOrdinal = 0;
0361     d->markupAware = markupAware;
0362     d->relaxedSubs = false;
0363 }
0364 
0365 KLocalizedString::KLocalizedString(const KLocalizedString &rhs)
0366     : d(new KLocalizedStringPrivate(*rhs.d))
0367 {
0368 }
0369 
0370 KLocalizedString &KLocalizedString::operator=(const KLocalizedString &rhs)
0371 {
0372     if (&rhs != this) {
0373         *d = *rhs.d;
0374     }
0375     return *this;
0376 }
0377 
0378 KLocalizedString::~KLocalizedString() = default;
0379 
0380 bool KLocalizedString::isEmpty() const
0381 {
0382     return d->text.isEmpty();
0383 }
0384 
0385 void KLocalizedStringPrivate::translateRaw(const QByteArray &domain,
0386                                            const QStringList &languages,
0387                                            const QByteArray &msgctxt,
0388                                            const QByteArray &msgid,
0389                                            const QByteArray &msgid_plural,
0390                                            qulonglong n,
0391                                            QString &language,
0392                                            QString &msgstr)
0393 {
0394     KLocalizedStringPrivateStatics *s = staticsKLSP();
0395 
0396     // Empty msgid would result in returning the catalog header,
0397     // which is never intended, so warn and return empty translation.
0398     if (msgid.isNull() || msgid.isEmpty()) {
0399         qCWarning(KI18N) << "KLocalizedString: "
0400                             "Trying to look up translation of \"\", fix the code.";
0401         language.clear();
0402         msgstr.clear();
0403         return;
0404     }
0405     // Gettext semantics allows empty context, but it is pointless, so warn.
0406     if (!msgctxt.isNull() && msgctxt.isEmpty()) {
0407         qCWarning(KI18N) << "KLocalizedString: "
0408                             "Using \"\" as context, fix the code.";
0409     }
0410     // Gettext semantics allows empty plural, but it is pointless, so warn.
0411     if (!msgid_plural.isNull() && msgid_plural.isEmpty()) {
0412         qCWarning(KI18N) << "KLocalizedString: "
0413                             "Using \"\" as plural text, fix the code.";
0414     }
0415 
0416     // Set translation to text in code language, in case no translation found.
0417     msgstr = msgid_plural.isNull() || n == 1 ? QString::fromUtf8(msgid) : QString::fromUtf8(msgid_plural);
0418     language = s->codeLanguage;
0419 
0420     if (domain.isEmpty()) {
0421         qCWarning(KI18N) << "KLocalizedString: Domain is not set for this string, translation will not work. Please see https://api.kde.org/frameworks/ki18n/html/prg_guide.html msgid:" << msgid << "msgid_plural:" << msgid_plural
0422                          << "msgctxt:" << msgctxt;
0423         return;
0424     }
0425 
0426     // Languages are ordered from highest to lowest priority.
0427     for (const QString &testLanguage : languages) {
0428         // If code language reached, no catalog lookup is needed.
0429         if (testLanguage == s->codeLanguage) {
0430             return;
0431         }
0432         const KCatalog &catalog = getCatalog(domain, testLanguage);
0433         QString testMsgstr;
0434         if (!msgctxt.isNull() && !msgid_plural.isNull()) {
0435             testMsgstr = catalog.translate(msgctxt, msgid, msgid_plural, n);
0436         } else if (!msgid_plural.isNull()) {
0437             testMsgstr = catalog.translate(msgid, msgid_plural, n);
0438         } else if (!msgctxt.isNull()) {
0439             testMsgstr = catalog.translate(msgctxt, msgid);
0440         } else {
0441             testMsgstr = catalog.translate(msgid);
0442         }
0443         if (!testMsgstr.isEmpty()) {
0444             // Translation found.
0445             language = testLanguage;
0446             msgstr = testMsgstr;
0447             return;
0448         }
0449     }
0450 }
0451 
0452 QString KLocalizedString::toString() const
0453 {
0454     return d->toString(d->domain, d->languages, d->format);
0455 }
0456 
0457 QString KLocalizedString::toString(const char *domain) const
0458 {
0459     return d->toString(domain, d->languages, d->format);
0460 }
0461 
0462 QString KLocalizedString::toString(const QStringList &languages) const
0463 {
0464     return d->toString(d->domain, languages, d->format);
0465 }
0466 
0467 QString KLocalizedString::toString(Kuit::VisualFormat format) const
0468 {
0469     return d->toString(d->domain, d->languages, format);
0470 }
0471 
0472 QString KLocalizedStringPrivate::toString(const QByteArray &domain, const QStringList &languages, Kuit::VisualFormat format, bool isArgument) const
0473 {
0474     KLocalizedStringPrivateStatics *s = staticsKLSP();
0475 
0476     QMutexLocker lock(&s->klspMutex);
0477 
0478     // Assure the message has been supplied.
0479     if (text.isEmpty()) {
0480         qCWarning(KI18N) << "Trying to convert empty KLocalizedString to QString.";
0481 #ifndef NDEBUG
0482         return QStringLiteral("(I18N_EMPTY_MESSAGE)");
0483 #else
0484         return QString();
0485 #endif
0486     }
0487 
0488     // Check whether plural argument has been supplied, if message has plural.
0489     if (!plural.isEmpty() && !numberSet) {
0490         qCWarning(KI18N) << "Plural argument to message" << shortenMessage(QString::fromUtf8(text)) << "not supplied before conversion.";
0491     }
0492 
0493     // Resolve inputs.
0494     QByteArray resolvedDomain = domain;
0495     if (resolvedDomain.isEmpty()) {
0496         resolvedDomain = s->applicationDomain;
0497     }
0498     QStringList resolvedLanguages = languages;
0499     if (resolvedLanguages.isEmpty()) {
0500         resolvedLanguages = s->languages;
0501     }
0502     Kuit::VisualFormat resolvedFormat = format;
0503 
0504     // Get raw translation.
0505     QString language;
0506     QString rawTranslation;
0507     translateRaw(resolvedDomain, resolvedLanguages, context, text, plural, number, language, rawTranslation);
0508     QString country = extractCountry(resolvedLanguages);
0509 
0510     // Set ordinary translation and possibly scripted translation.
0511     QString translation;
0512     QString scriptedTranslation;
0513     int fencePos = rawTranslation.indexOf(s->theFence);
0514     if (fencePos > 0) {
0515         // Script fence has been found, strip the scripted from the
0516         // ordinary translation.
0517         translation = rawTranslation.left(fencePos);
0518 
0519         // Scripted translation.
0520         scriptedTranslation = rawTranslation.mid(fencePos + s->theFence.length());
0521 
0522         // Try to initialize Transcript if not initialized and script not empty.
0523         // FIXME: And also if Transcript not disabled: where to configure this?
0524         if (!s->loadTranscriptCalled && !scriptedTranslation.isEmpty()) {
0525             loadTranscript();
0526 
0527             // Definitions from this library's scripting module
0528             // must be available to all other modules.
0529             // So force creation of this library's catalog here,
0530             // to make sure the scripting module is loaded.
0531             getCatalog(s->ourDomain, language);
0532         }
0533     } else if (fencePos < 0) {
0534         // No script fence, use translation as is.
0535         translation = rawTranslation;
0536     } else { // fencePos == 0
0537         // The msgstr starts with the script fence, no ordinary translation.
0538         // This is not allowed, consider message not translated.
0539         qCWarning(KI18N) << "Scripted message" << shortenMessage(translation) << "without ordinary translation, discarded.";
0540         translation = plural.isEmpty() || number == 1 ? QString::fromUtf8(text) : QString::fromUtf8(plural);
0541     }
0542 
0543     // Resolve substituted KLocalizedString arguments.
0544     QStringList resolvedArguments;
0545     QList<QVariant> resolvedValues;
0546     for (int i = 0; i < arguments.size(); i++) {
0547         auto lsIt = klsArguments.constFind(i);
0548         if (lsIt != klsArguments.constEnd()) {
0549             const KLocalizedString &kls = *lsIt;
0550             int fieldWidth = klsArgumentFieldWidths.value(i);
0551             QChar fillChar = klsArgumentFillChars.value(i);
0552             // Override argument's languages and format, but not domain.
0553             bool isArgumentSub = true;
0554             QString resdArg = kls.d->toString(kls.d->domain, resolvedLanguages, resolvedFormat, isArgumentSub);
0555             resolvedValues.append(resdArg);
0556             if (markupAware && !kls.d->markupAware) {
0557                 resdArg = Kuit::escape(resdArg);
0558             }
0559             resdArg = QStringLiteral("%1").arg(resdArg, fieldWidth, fillChar);
0560             resolvedArguments.append(resdArg);
0561         } else {
0562             QString resdArg = arguments[i];
0563             if (markupAware) {
0564                 resdArg = Kuit::escape(resdArg);
0565             }
0566             resolvedArguments.append(resdArg);
0567             resolvedValues.append(values[i]);
0568         }
0569     }
0570 
0571     // Substitute placeholders in ordinary translation.
0572     QString finalTranslation = substituteSimple(translation, resolvedArguments);
0573     if (markupAware && !isArgument) {
0574         // Resolve markup in ordinary translation.
0575         finalTranslation = formatMarkup(resolvedDomain, language, QString::fromUtf8(context), finalTranslation, resolvedFormat);
0576     }
0577 
0578     // If there is also a scripted translation.
0579     if (!scriptedTranslation.isEmpty()) {
0580         // Evaluate scripted translation.
0581         bool fallback = false;
0582         scriptedTranslation = substituteTranscript(scriptedTranslation, language, country, finalTranslation, resolvedArguments, resolvedValues, fallback);
0583 
0584         // If any translation produced and no fallback requested.
0585         if (!scriptedTranslation.isEmpty() && !fallback) {
0586             if (markupAware && !isArgument) {
0587                 // Resolve markup in scripted translation.
0588                 scriptedTranslation = formatMarkup(resolvedDomain, language, QString::fromUtf8(context), scriptedTranslation, resolvedFormat);
0589             }
0590             finalTranslation = scriptedTranslation;
0591         }
0592     }
0593 
0594     // Execute any scripted post calls; they cannot modify the final result,
0595     // but are used to set states.
0596     if (s->ktrs != nullptr) {
0597         const QStringList pcalls = s->ktrs->postCalls(language);
0598         for (const QString &pcall : pcalls) {
0599             postTranscript(pcall, language, country, finalTranslation, resolvedArguments, resolvedValues);
0600         }
0601     }
0602 
0603     return finalTranslation;
0604 }
0605 
0606 QString KLocalizedStringPrivate::substituteSimple(const QString &translation, const QStringList &arguments, QChar plchar, bool isPartial) const
0607 {
0608 #ifdef NDEBUG
0609     Q_UNUSED(isPartial);
0610 #endif
0611 
0612     QStringList tsegs; // text segments per placeholder occurrence
0613     QList<int> plords; // ordinal numbers per placeholder occurrence
0614 #ifndef NDEBUG
0615     QList<int> ords; // indicates which placeholders are present
0616 #endif
0617     int slen = translation.length();
0618     int spos = 0;
0619     int tpos = translation.indexOf(plchar);
0620     while (tpos >= 0) {
0621         int ctpos = tpos;
0622 
0623         ++tpos;
0624         if (tpos == slen) {
0625             break;
0626         }
0627 
0628         if (translation[tpos].digitValue() > 0) {
0629             // NOTE: %0 is not considered a placeholder.
0630             // Get the placeholder ordinal.
0631             int plord = 0;
0632             while (tpos < slen && translation[tpos].digitValue() >= 0) {
0633                 plord = 10 * plord + translation[tpos].digitValue();
0634                 ++tpos;
0635             }
0636             --plord; // ordinals are zero based
0637 
0638 #ifndef NDEBUG
0639             // Perhaps enlarge storage for indicators.
0640             // Note that QList<int> will initialize new elements to 0,
0641             // as they are supposed to be.
0642             if (plord >= ords.size()) {
0643                 ords.resize(plord + 1);
0644             }
0645 
0646             // Indicate that placeholder with computed ordinal is present.
0647             ords[plord] = 1;
0648 #endif
0649 
0650             // Store text segment prior to placeholder and placeholder number.
0651             tsegs.append(translation.mid(spos, ctpos - spos));
0652             plords.append(plord);
0653 
0654             // Position of next text segment.
0655             spos = tpos;
0656         }
0657 
0658         tpos = translation.indexOf(plchar, tpos);
0659     }
0660     // Store last text segment.
0661     tsegs.append(translation.mid(spos));
0662 
0663 #ifndef NDEBUG
0664     // Perhaps enlarge storage for plural-number ordinal.
0665     if (!plural.isEmpty() && numberOrdinal >= ords.size()) {
0666         ords.resize(numberOrdinal + 1);
0667     }
0668 
0669     // Message might have plural but without plural placeholder, which is an
0670     // allowed state. To ease further logic, indicate that plural placeholder
0671     // is present anyway if message has plural.
0672     if (!plural.isEmpty()) {
0673         ords[numberOrdinal] = 1;
0674     }
0675 #endif
0676 
0677     // Assemble the final string from text segments and arguments.
0678     QString finalTranslation;
0679     for (int i = 0; i < plords.size(); i++) {
0680         finalTranslation.append(tsegs.at(i));
0681         if (plords.at(i) >= arguments.size()) { // too little arguments
0682             // put back the placeholder
0683             finalTranslation.append(QLatin1Char('%') + QString::number(plords.at(i) + 1));
0684 #ifndef NDEBUG
0685             if (!isPartial) {
0686                 // spoof the message
0687                 finalTranslation.append(QStringLiteral("(I18N_ARGUMENT_MISSING)"));
0688             }
0689 #endif
0690         } else { // just fine
0691             finalTranslation.append(arguments.at(plords.at(i)));
0692         }
0693     }
0694     finalTranslation.append(tsegs.last());
0695 
0696 #ifndef NDEBUG
0697     if (!isPartial && !relaxedSubs) {
0698         // Check that there are no gaps in numbering sequence of placeholders.
0699         bool gaps = false;
0700         for (int i = 0; i < ords.size(); i++) {
0701             if (!ords.at(i)) {
0702                 gaps = true;
0703                 qCWarning(KI18N).nospace() << "Placeholder %" << QString::number(i + 1) << " skipped in message " << shortenMessage(translation);
0704             }
0705         }
0706         // If no gaps, check for mismatch between the number of
0707         // unique placeholders and actually supplied arguments.
0708         if (!gaps && ords.size() != arguments.size()) {
0709             qCWarning(KI18N) << arguments.size() << "instead of" << ords.size() << "arguments to message" << shortenMessage(translation)
0710                              << "supplied before conversion";
0711         }
0712 
0713         // Some spoofs.
0714         if (gaps) {
0715             finalTranslation.append(QStringLiteral("(I18N_GAPS_IN_PLACEHOLDER_SEQUENCE)"));
0716         }
0717         if (ords.size() < arguments.size()) {
0718             finalTranslation.append(QStringLiteral("(I18N_EXCESS_ARGUMENTS_SUPPLIED)"));
0719         }
0720     }
0721     if (!isPartial) {
0722         if (!plural.isEmpty() && !numberSet) {
0723             finalTranslation.append(QStringLiteral("(I18N_PLURAL_ARGUMENT_MISSING)"));
0724         }
0725     }
0726 #endif
0727 
0728     return finalTranslation;
0729 }
0730 
0731 QString KLocalizedStringPrivate::formatMarkup(const QByteArray &domain,
0732                                               const QString &language,
0733                                               const QString &context,
0734                                               const QString &text,
0735                                               Kuit::VisualFormat format) const
0736 {
0737     KLocalizedStringPrivateStatics *s = staticsKLSP();
0738 
0739     QHash<QString, KuitFormatter *>::iterator formatter = s->formatters.find(language);
0740     if (formatter == s->formatters.end()) {
0741         formatter = s->formatters.insert(language, new KuitFormatter(language));
0742     }
0743     return (*formatter)->format(domain, context, text, format);
0744 }
0745 
0746 QString KLocalizedStringPrivate::substituteTranscript(const QString &scriptedTranslation,
0747                                                       const QString &language,
0748                                                       const QString &country,
0749                                                       const QString &ordinaryTranslation,
0750                                                       const QStringList &arguments,
0751                                                       const QList<QVariant> &values,
0752                                                       bool &fallback) const
0753 {
0754     KLocalizedStringPrivateStatics *s = staticsKLSP();
0755 
0756     if (s->ktrs == nullptr) {
0757         // Scripting engine not available.
0758         return QString();
0759     }
0760 
0761     // Iterate by interpolations.
0762     QString finalTranslation;
0763     fallback = false;
0764     int ppos = 0;
0765     int tpos = scriptedTranslation.indexOf(s->startInterp);
0766     while (tpos >= 0) {
0767         // Resolve substitutions in preceding text.
0768         QString ptext = substituteSimple(scriptedTranslation.mid(ppos, tpos - ppos), arguments, s->scriptPlchar, true);
0769         finalTranslation.append(ptext);
0770 
0771         // Resolve interpolation.
0772         QString result;
0773         bool fallbackLocal;
0774         tpos = resolveInterpolation(scriptedTranslation, tpos, language, country, ordinaryTranslation, arguments, values, result, fallbackLocal);
0775 
0776         // If there was a problem in parsing the interpolation, cannot proceed
0777         // (debug info already reported while parsing).
0778         if (tpos < 0) {
0779             return QString();
0780         }
0781         // If fallback has been explicitly requested, indicate global fallback
0782         // but proceed with evaluations (other interpolations may set states).
0783         if (fallbackLocal) {
0784             fallback = true;
0785         }
0786 
0787         // Add evaluated interpolation to the text.
0788         finalTranslation.append(result);
0789 
0790         // On to next interpolation.
0791         ppos = tpos;
0792         tpos = scriptedTranslation.indexOf(s->startInterp, tpos);
0793     }
0794     // Last text segment.
0795     finalTranslation.append(substituteSimple(scriptedTranslation.mid(ppos), arguments, s->scriptPlchar, true));
0796 
0797     // Return empty string if fallback was requested.
0798     return fallback ? QString() : finalTranslation;
0799 }
0800 
0801 int KLocalizedStringPrivate::resolveInterpolation(const QString &scriptedTranslation,
0802                                                   int pos,
0803                                                   const QString &language,
0804                                                   const QString &country,
0805                                                   const QString &ordinaryTranslation,
0806                                                   const QStringList &arguments,
0807                                                   const QList<QVariant> &values,
0808                                                   QString &result,
0809                                                   bool &fallback) const
0810 {
0811     // pos is the position of opening character sequence.
0812     // Returns the position of first character after closing sequence,
0813     // or -1 in case of parsing error.
0814     // result is set to result of Transcript evaluation.
0815     // fallback is set to true if Transcript evaluation requested so.
0816 
0817     KLocalizedStringPrivateStatics *s = staticsKLSP();
0818 
0819     result.clear();
0820     fallback = false;
0821 
0822     // Split interpolation into arguments.
0823     QList<QVariant> iargs;
0824     const int slen = scriptedTranslation.length();
0825     const int islen = s->startInterp.length();
0826     const int ielen = s->endInterp.length();
0827     int tpos = pos + s->startInterp.length();
0828     while (1) {
0829         // Skip whitespace.
0830         while (tpos < slen && scriptedTranslation[tpos].isSpace()) {
0831             ++tpos;
0832         }
0833         if (tpos == slen) {
0834             qCWarning(KI18N) << "Unclosed interpolation" << scriptedTranslation.mid(pos, tpos - pos) << "in message" << shortenMessage(scriptedTranslation);
0835             return -1;
0836         }
0837         if (QStringView(scriptedTranslation).mid(tpos, ielen) == s->endInterp) {
0838             break; // no more arguments
0839         }
0840 
0841         // Parse argument: may be concatenated from free and quoted text,
0842         // and sub-interpolations.
0843         // Free and quoted segments may contain placeholders, substitute them;
0844         // recurse into sub-interpolations.
0845         // Free segments may be value references, parse and record for
0846         // consideration at the end.
0847         // Mind backslash escapes throughout.
0848         QStringList segs;
0849         QVariant vref;
0850         while (!scriptedTranslation[tpos].isSpace() && scriptedTranslation.mid(tpos, ielen) != s->endInterp) {
0851             if (scriptedTranslation[tpos] == QLatin1Char('\'')) { // quoted segment
0852                 QString seg;
0853                 ++tpos; // skip opening quote
0854                 // Find closing quote.
0855                 while (tpos < slen && scriptedTranslation[tpos] != QLatin1Char('\'')) {
0856                     if (scriptedTranslation[tpos] == QLatin1Char('\\')) {
0857                         ++tpos; // escape next character
0858                     }
0859                     seg.append(scriptedTranslation[tpos]);
0860                     ++tpos;
0861                 }
0862                 if (tpos == slen) {
0863                     qCWarning(KI18N) << "Unclosed quote in interpolation" << scriptedTranslation.mid(pos, tpos - pos) << "in message"
0864                                      << shortenMessage(scriptedTranslation);
0865                     return -1;
0866                 }
0867 
0868                 // Append to list of segments, resolving placeholders.
0869                 segs.append(substituteSimple(seg, arguments, s->scriptPlchar, true));
0870 
0871                 ++tpos; // skip closing quote
0872             } else if (scriptedTranslation.mid(tpos, islen) == s->startInterp) { // sub-interpolation
0873                 QString resultLocal;
0874                 bool fallbackLocal;
0875                 tpos = resolveInterpolation(scriptedTranslation, tpos, language, country, ordinaryTranslation, arguments, values, resultLocal, fallbackLocal);
0876                 if (tpos < 0) { // unrecoverable problem in sub-interpolation
0877                     // Error reported in the subcall.
0878                     return tpos;
0879                 }
0880                 if (fallbackLocal) { // sub-interpolation requested fallback
0881                     fallback = true;
0882                 }
0883                 segs.append(resultLocal);
0884             } else { // free segment
0885                 QString seg;
0886                 // Find whitespace, quote, opening or closing sequence.
0887                 while (tpos < slen && !scriptedTranslation[tpos].isSpace() //
0888                        && scriptedTranslation[tpos] != QLatin1Char('\'') //
0889                        && scriptedTranslation.mid(tpos, islen) != s->startInterp //
0890                        && scriptedTranslation.mid(tpos, ielen) != s->endInterp) {
0891                     if (scriptedTranslation[tpos] == QLatin1Char('\\')) {
0892                         ++tpos; // escape next character
0893                     }
0894                     seg.append(scriptedTranslation[tpos]);
0895                     ++tpos;
0896                 }
0897                 if (tpos == slen) {
0898                     qCWarning(KI18N) << "Non-terminated interpolation" << scriptedTranslation.mid(pos, tpos - pos) << "in message"
0899                                      << shortenMessage(scriptedTranslation);
0900                     return -1;
0901                 }
0902 
0903                 // The free segment may look like a value reference;
0904                 // in that case, record which value it would reference,
0905                 // and add verbatim to the segment list.
0906                 // Otherwise, do a normal substitution on the segment.
0907                 vref = segmentToValue(seg);
0908                 if (vref.isValid()) {
0909                     segs.append(seg);
0910                 } else {
0911                     segs.append(substituteSimple(seg, arguments, s->scriptPlchar, true));
0912                 }
0913             }
0914         }
0915 
0916         // Append this argument to rest of the arguments.
0917         // If the there was a single text segment and it was a proper value
0918         // reference, add it instead of the joined segments.
0919         // Otherwise, add the joined segments.
0920         if (segs.size() == 1 && vref.isValid()) {
0921             iargs.append(vref);
0922         } else {
0923             iargs.append(segs.join(QString()));
0924         }
0925     }
0926     tpos += ielen; // skip to first character after closing sequence
0927 
0928     // NOTE: Why not substitute placeholders (via substituteSimple) in one
0929     // global pass, then handle interpolations in second pass? Because then
0930     // there is the danger of substituted text or sub-interpolations producing
0931     // quotes and escapes themselves, which would mess up the parsing.
0932 
0933     // Evaluate interpolation.
0934     QString msgctxt = QString::fromUtf8(context);
0935     QString msgid = QString::fromUtf8(text);
0936     QString scriptError;
0937     bool fallbackLocal;
0938     result = s->ktrs->eval(iargs,
0939                            language,
0940                            country,
0941                            msgctxt,
0942                            dynamicContext,
0943                            msgid,
0944                            arguments,
0945                            values,
0946                            ordinaryTranslation,
0947                            s->scriptModulesToLoad,
0948                            scriptError,
0949                            fallbackLocal);
0950     // s->scriptModulesToLoad will be cleared during the call.
0951 
0952     if (fallbackLocal) { // evaluation requested fallback
0953         fallback = true;
0954     }
0955     if (!scriptError.isEmpty()) { // problem with evaluation
0956         fallback = true; // also signal fallback
0957         if (!scriptError.isEmpty()) {
0958             qCWarning(KI18N) << "Interpolation" << scriptedTranslation.mid(pos, tpos - pos) << "in" << shortenMessage(scriptedTranslation)
0959                              << "failed:" << scriptError;
0960         }
0961     }
0962 
0963     return tpos;
0964 }
0965 
0966 QVariant KLocalizedStringPrivate::segmentToValue(const QString &segment) const
0967 {
0968     KLocalizedStringPrivateStatics *s = staticsKLSP();
0969 
0970     // Return invalid variant if segment is either not a proper
0971     // value reference, or the reference is out of bounds.
0972 
0973     // Value reference must start with a special character.
0974     if (!segment.startsWith(s->scriptVachar)) {
0975         return QVariant();
0976     }
0977 
0978     // Reference number must start with 1-9.
0979     // (If numstr is empty, toInt() will return 0.)
0980     QString numstr = segment.mid(1);
0981     int numstrAsInt = QStringView(numstr).left(1).toInt();
0982     if (numstrAsInt < 1) {
0983         return QVariant();
0984     }
0985 
0986     // Number must be valid and in bounds.
0987     bool ok;
0988     int index = numstr.toInt(&ok) - 1;
0989     if (!ok || index >= values.size()) {
0990         return QVariant();
0991     }
0992 
0993     // Passed all hoops.
0994     return values.at(index);
0995 }
0996 
0997 QString KLocalizedStringPrivate::postTranscript(const QString &pcall,
0998                                                 const QString &language,
0999                                                 const QString &country,
1000                                                 const QString &finalTranslation,
1001                                                 const QStringList &arguments,
1002                                                 const QList<QVariant> &values) const
1003 {
1004     KLocalizedStringPrivateStatics *s = staticsKLSP();
1005 
1006     if (s->ktrs == nullptr) {
1007         // Scripting engine not available.
1008         // (Though this cannot happen, we wouldn't be here then.)
1009         return QString();
1010     }
1011 
1012     // Resolve the post call.
1013     QList<QVariant> iargs;
1014     iargs.append(pcall);
1015     QString msgctxt = QString::fromUtf8(context);
1016     QString msgid = QString::fromUtf8(text);
1017     QString scriptError;
1018     bool fallback;
1019     s->ktrs->eval(iargs, language, country, msgctxt, dynamicContext, msgid, arguments, values, finalTranslation, s->scriptModulesToLoad, scriptError, fallback);
1020     // s->scriptModulesToLoad will be cleared during the call.
1021 
1022     // If the evaluation went wrong.
1023     if (!scriptError.isEmpty()) {
1024         qCWarning(KI18N) << "Post call" << pcall << "for message" << shortenMessage(msgid) << "failed:" << scriptError;
1025         return QString();
1026     }
1027 
1028     return finalTranslation;
1029 }
1030 
1031 KLocalizedString KLocalizedString::withLanguages(const QStringList &languages) const
1032 {
1033     KLocalizedString kls(*this);
1034     kls.d->languages = languages;
1035     return kls;
1036 }
1037 
1038 KLocalizedString KLocalizedString::withDomain(const char *domain) const
1039 {
1040     KLocalizedString kls(*this);
1041     kls.d->domain = domain;
1042     return kls;
1043 }
1044 
1045 KLocalizedString KLocalizedString::withFormat(Kuit::VisualFormat format) const
1046 {
1047     KLocalizedString kls(*this);
1048     kls.d->format = format;
1049     return kls;
1050 }
1051 
1052 KLocalizedString KLocalizedString::subs(int a, int fieldWidth, int base, QChar fillChar) const
1053 {
1054     KLocalizedString kls(*this);
1055     kls.d->checkNumber(std::abs(a));
1056     kls.d->arguments.append(QStringLiteral("%L1").arg(a, fieldWidth, base, fillChar));
1057     kls.d->values.append(static_cast<intn>(a));
1058     return kls;
1059 }
1060 
1061 KLocalizedString KLocalizedString::subs(uint a, int fieldWidth, int base, QChar fillChar) const
1062 {
1063     KLocalizedString kls(*this);
1064     kls.d->checkNumber(a);
1065     kls.d->arguments.append(QStringLiteral("%L1").arg(a, fieldWidth, base, fillChar));
1066     kls.d->values.append(static_cast<uintn>(a));
1067     return kls;
1068 }
1069 
1070 KLocalizedString KLocalizedString::subs(long a, int fieldWidth, int base, QChar fillChar) const
1071 {
1072     KLocalizedString kls(*this);
1073     kls.d->checkNumber(std::abs(a));
1074     kls.d->arguments.append(QStringLiteral("%L1").arg(a, fieldWidth, base, fillChar));
1075     kls.d->values.append(static_cast<intn>(a));
1076     return kls;
1077 }
1078 
1079 KLocalizedString KLocalizedString::subs(ulong a, int fieldWidth, int base, QChar fillChar) const
1080 {
1081     KLocalizedString kls(*this);
1082     kls.d->checkNumber(a);
1083     kls.d->arguments.append(QStringLiteral("%L1").arg(a, fieldWidth, base, fillChar));
1084     kls.d->values.append(static_cast<uintn>(a));
1085     return kls;
1086 }
1087 
1088 KLocalizedString KLocalizedString::subs(qlonglong a, int fieldWidth, int base, QChar fillChar) const
1089 {
1090     KLocalizedString kls(*this);
1091     kls.d->checkNumber(qAbs(a));
1092     kls.d->arguments.append(QStringLiteral("%L1").arg(a, fieldWidth, base, fillChar));
1093     kls.d->values.append(static_cast<intn>(a));
1094     return kls;
1095 }
1096 
1097 KLocalizedString KLocalizedString::subs(qulonglong a, int fieldWidth, int base, QChar fillChar) const
1098 {
1099     KLocalizedString kls(*this);
1100     kls.d->checkNumber(a);
1101     kls.d->arguments.append(QStringLiteral("%L1").arg(a, fieldWidth, base, fillChar));
1102     kls.d->values.append(static_cast<uintn>(a));
1103     return kls;
1104 }
1105 
1106 KLocalizedString KLocalizedString::subs(double a, int fieldWidth, char format, int precision, QChar fillChar) const
1107 {
1108     KLocalizedString kls(*this);
1109     kls.d->arguments.append(QStringLiteral("%L1").arg(a, fieldWidth, format, precision, fillChar));
1110     kls.d->values.append(static_cast<realn>(a));
1111     return kls;
1112 }
1113 
1114 KLocalizedString KLocalizedString::subs(QChar a, int fieldWidth, QChar fillChar) const
1115 {
1116     KLocalizedString kls(*this);
1117     QString baseArg = QString(a);
1118     QString fmtdArg = QStringLiteral("%1").arg(a, fieldWidth, fillChar);
1119     kls.d->arguments.append(fmtdArg);
1120     kls.d->values.append(baseArg);
1121     return kls;
1122 }
1123 
1124 KLocalizedString KLocalizedString::subs(const QString &a, int fieldWidth, QChar fillChar) const
1125 {
1126     KLocalizedString kls(*this);
1127     QString baseArg = a;
1128     QString fmtdArg = QStringLiteral("%1").arg(a, fieldWidth, fillChar);
1129     kls.d->arguments.append(fmtdArg);
1130     kls.d->values.append(baseArg);
1131     return kls;
1132 }
1133 
1134 KLocalizedString KLocalizedString::subs(const KLocalizedString &a, int fieldWidth, QChar fillChar) const
1135 {
1136     KLocalizedString kls(*this);
1137     // KLocalizedString arguments must be resolved inside toString
1138     // when the domain, language, visual format, etc. become known.
1139     int i = kls.d->arguments.size();
1140     kls.d->klsArguments[i] = a;
1141     kls.d->klsArgumentFieldWidths[i] = fieldWidth;
1142     kls.d->klsArgumentFillChars[i] = fillChar;
1143     kls.d->arguments.append(QString());
1144     kls.d->values.append(0);
1145     return kls;
1146 }
1147 
1148 KLocalizedString KLocalizedString::inContext(const QString &key, const QString &value) const
1149 {
1150     KLocalizedString kls(*this);
1151     kls.d->dynamicContext[key] = value;
1152     return kls;
1153 }
1154 
1155 KLocalizedString KLocalizedString::relaxSubs() const
1156 {
1157     KLocalizedString kls(*this);
1158     kls.d->relaxedSubs = true;
1159     return kls;
1160 }
1161 
1162 KLocalizedString KLocalizedString::ignoreMarkup() const
1163 {
1164     KLocalizedString kls(*this);
1165     kls.d->markupAware = false;
1166     return kls;
1167 }
1168 
1169 QByteArray KLocalizedString::untranslatedText() const
1170 {
1171     return d->text;
1172 }
1173 
1174 void KLocalizedString::setApplicationDomain(const QByteArray &domain)
1175 {
1176     KLocalizedStringPrivateStatics *s = staticsKLSP();
1177 
1178     QMutexLocker lock(&s->klspMutex);
1179 
1180     s->applicationDomain = domain;
1181 }
1182 
1183 QByteArray KLocalizedString::applicationDomain()
1184 {
1185     KLocalizedStringPrivateStatics *s = staticsKLSP();
1186 
1187     return s->applicationDomain;
1188 }
1189 
1190 QStringList KLocalizedString::languages()
1191 {
1192     KLocalizedStringPrivateStatics *s = staticsKLSP();
1193 
1194     return s->languages;
1195 }
1196 
1197 void KLocalizedString::setLanguages(const QStringList &languages)
1198 {
1199     KLocalizedStringPrivateStatics *s = staticsKLSP();
1200 
1201     QMutexLocker lock(&s->klspMutex);
1202 
1203     s->languages = languages;
1204 }
1205 
1206 void KLocalizedString::clearLanguages()
1207 {
1208     KLocalizedStringPrivateStatics *s = staticsKLSP();
1209 
1210     QMutexLocker lock(&s->klspMutex);
1211 
1212     s->languages = s->localeLanguages;
1213 }
1214 
1215 bool KLocalizedString::isApplicationTranslatedInto(const QString &language)
1216 {
1217     KLocalizedStringPrivateStatics *s = staticsKLSP();
1218 
1219     return language == s->codeLanguage || !KCatalog::catalogLocaleDir(s->applicationDomain, language).isEmpty();
1220 }
1221 
1222 QSet<QString> KLocalizedString::availableApplicationTranslations()
1223 {
1224     return availableDomainTranslations(staticsKLSP()->applicationDomain);
1225 }
1226 
1227 QSet<QString> KLocalizedString::availableDomainTranslations(const QByteArray &domain)
1228 {
1229     QSet<QString> availableLanguages;
1230 
1231     if (!domain.isEmpty()) {
1232         availableLanguages = KCatalog::availableCatalogLanguages(domain);
1233         availableLanguages.insert(staticsKLSP()->codeLanguage);
1234     }
1235 
1236     return availableLanguages;
1237 }
1238 
1239 const KCatalog &KLocalizedStringPrivate::getCatalog(const QByteArray &domain, const QString &language)
1240 {
1241     KLocalizedStringPrivateStatics *s = staticsKLSP();
1242 
1243     QMutexLocker lock(&s->klspMutex);
1244 
1245     QHash<QByteArray, KCatalogPtrHash>::iterator languageCatalogs = s->catalogs.find(domain);
1246     if (languageCatalogs == s->catalogs.end()) {
1247         languageCatalogs = s->catalogs.insert(domain, KCatalogPtrHash());
1248     }
1249     KCatalogPtrHash::iterator catalog = languageCatalogs->find(language);
1250     if (catalog == languageCatalogs->end()) {
1251         catalog = languageCatalogs->insert(language, new KCatalog(domain, language));
1252         locateScriptingModule(domain, language);
1253     }
1254     return **catalog;
1255 }
1256 
1257 void KLocalizedStringPrivate::locateScriptingModule(const QByteArray &domain, const QString &language)
1258 {
1259     KLocalizedStringPrivateStatics *s = staticsKLSP();
1260 
1261     QMutexLocker lock(&s->klspMutex);
1262 
1263     QString modapath =
1264         QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("locale/%1/%2/%3/%3.js").arg(language, s->scriptDir, QLatin1String{domain}));
1265 
1266     // If the module exists and hasn't been already included.
1267     if (!modapath.isEmpty() && !s->scriptModules[language].contains(domain)) {
1268         // Indicate that the module has been considered.
1269         s->scriptModules[language].append(domain);
1270 
1271         // Store the absolute path and language of the module,
1272         // to load on next script evaluation.
1273         QStringList module;
1274         module.append(modapath);
1275         module.append(language);
1276         s->scriptModulesToLoad.append(module);
1277     }
1278 }
1279 
1280 extern "C" {
1281 typedef KTranscript *(*InitFunc)();
1282 }
1283 
1284 void KLocalizedStringPrivate::loadTranscript()
1285 {
1286     KLocalizedStringPrivateStatics *s = staticsKLSP();
1287 
1288     QMutexLocker lock(&s->klspMutex);
1289 
1290     s->loadTranscriptCalled = true;
1291     s->ktrs = nullptr; // null indicates that Transcript is not available
1292 
1293     // QPluginLoader is just used to find the plugin
1294     QPluginLoader loader(QStringLiteral("kf6/ktranscript"));
1295     if (loader.fileName().isEmpty()) {
1296         qCWarning(KI18N) << "Cannot find Transcript plugin.";
1297         return;
1298     }
1299 
1300     QLibrary lib(loader.fileName());
1301     if (!lib.load()) {
1302         qCWarning(KI18N) << "Cannot load Transcript plugin:" << lib.errorString();
1303         return;
1304     }
1305 
1306     InitFunc initf = (InitFunc)lib.resolve("load_transcript");
1307     if (!initf) {
1308         lib.unload();
1309         qCWarning(KI18N) << "Cannot find function load_transcript in Transcript plugin.";
1310         return;
1311     }
1312 
1313     s->ktrs = initf();
1314 }
1315 
1316 QString KLocalizedString::localizedFilePath(const QString &filePath)
1317 {
1318     KLocalizedStringPrivateStatics *s = staticsKLSP();
1319 
1320     // Check if l10n subdirectory is present, stop if not.
1321     QFileInfo fileInfo(filePath);
1322     QString locDirPath = fileInfo.path() + QLatin1Char('/') + QLatin1String("l10n");
1323     QFileInfo locDirInfo(locDirPath);
1324     if (!locDirInfo.isDir()) {
1325         return filePath;
1326     }
1327 
1328     // Go through possible localized paths by priority of languages,
1329     // return first that exists.
1330     QString fileName = fileInfo.fileName();
1331     for (const QString &lang : std::as_const(s->languages)) {
1332         QString locFilePath = locDirPath + QLatin1Char('/') + lang + QLatin1Char('/') + fileName;
1333         QFileInfo locFileInfo(locFilePath);
1334         if (locFileInfo.isFile() && locFileInfo.isReadable()) {
1335             return locFilePath;
1336         }
1337     }
1338 
1339     return filePath;
1340 }
1341 
1342 QString KLocalizedString::removeAcceleratorMarker(const QString &label)
1343 {
1344     return ::removeAcceleratorMarker(label);
1345 }
1346 
1347 void KLocalizedString::addDomainLocaleDir(const QByteArray &domain, const QString &path)
1348 {
1349     KCatalog::addDomainLocaleDir(domain, path);
1350 }
1351 
1352 KLocalizedString ki18n(const char *text)
1353 {
1354     return KLocalizedString(nullptr, nullptr, text, nullptr, false);
1355 }
1356 
1357 KLocalizedString ki18nc(const char *context, const char *text)
1358 {
1359     return KLocalizedString(nullptr, context, text, nullptr, false);
1360 }
1361 
1362 KLocalizedString ki18np(const char *singular, const char *plural)
1363 {
1364     return KLocalizedString(nullptr, nullptr, singular, plural, false);
1365 }
1366 
1367 KLocalizedString ki18ncp(const char *context, const char *singular, const char *plural)
1368 {
1369     return KLocalizedString(nullptr, context, singular, plural, false);
1370 }
1371 
1372 KLocalizedString ki18nd(const char *domain, const char *text)
1373 {
1374     return KLocalizedString(domain, nullptr, text, nullptr, false);
1375 }
1376 
1377 KLocalizedString ki18ndc(const char *domain, const char *context, const char *text)
1378 {
1379     return KLocalizedString(domain, context, text, nullptr, false);
1380 }
1381 
1382 KLocalizedString ki18ndp(const char *domain, const char *singular, const char *plural)
1383 {
1384     return KLocalizedString(domain, nullptr, singular, plural, false);
1385 }
1386 
1387 KLocalizedString ki18ndcp(const char *domain, const char *context, const char *singular, const char *plural)
1388 {
1389     return KLocalizedString(domain, context, singular, plural, false);
1390 }
1391 
1392 KLocalizedString kxi18n(const char *text)
1393 {
1394     return KLocalizedString(nullptr, nullptr, text, nullptr, true);
1395 }
1396 
1397 KLocalizedString kxi18nc(const char *context, const char *text)
1398 {
1399     return KLocalizedString(nullptr, context, text, nullptr, true);
1400 }
1401 
1402 KLocalizedString kxi18np(const char *singular, const char *plural)
1403 {
1404     return KLocalizedString(nullptr, nullptr, singular, plural, true);
1405 }
1406 
1407 KLocalizedString kxi18ncp(const char *context, const char *singular, const char *plural)
1408 {
1409     return KLocalizedString(nullptr, context, singular, plural, true);
1410 }
1411 
1412 KLocalizedString kxi18nd(const char *domain, const char *text)
1413 {
1414     return KLocalizedString(domain, nullptr, text, nullptr, true);
1415 }
1416 
1417 KLocalizedString kxi18ndc(const char *domain, const char *context, const char *text)
1418 {
1419     return KLocalizedString(domain, context, text, nullptr, true);
1420 }
1421 
1422 KLocalizedString kxi18ndp(const char *domain, const char *singular, const char *plural)
1423 {
1424     return KLocalizedString(domain, nullptr, singular, plural, true);
1425 }
1426 
1427 KLocalizedString kxi18ndcp(const char *domain, const char *context, const char *singular, const char *plural)
1428 {
1429     return KLocalizedString(domain, context, singular, plural, true);
1430 }