1/* This file is part of the KDE libraries
2 SPDX-FileCopyrightText: 2006, 2013 Chusslove Illich <caslav.ilic@gmx.net>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7// We don't want i18n to be expanded to i18nd here
8#undef TRANSLATION_DOMAIN
9
10#include "config.h"
11
12#include <cstdlib>
13
14#include <QByteArray>
15#include <QCoreApplication>
16#include <QDir>
17#include <QFile>
18#include <QFileInfo>
19#include <QHash>
20#include <QLibrary>
21#include <QList>
22#include <QMutexLocker>
23#include <QPluginLoader>
24#include <QRecursiveMutex>
25#include <QStandardPaths>
26#include <QStringList>
27
28#include <common_helpers_p.h>
29#include <kcatalog_p.h>
30#include <klocalizedstring.h>
31#include <ktranscript_p.h>
32#include <kuitsetup_p.h>
33
34#include "ki18n_logging.h"
35
36using namespace Qt::Literals;
37
38// Truncate string, for output of long messages.
39static QString shortenMessage(const QString &str)
40{
41 const int maxlen = 20;
42 if (str.length() <= maxlen) {
43 return str;
44 } else {
45 return QStringView(str).left(n: maxlen) + QLatin1String("...");
46 }
47}
48
49static void splitLocale(const QString &aLocale, QStringView &language, QStringView &country, QStringView &modifier, QStringView &charset)
50{
51 QStringView locale(aLocale);
52
53 // In case there are several concatenated locale specifications,
54 // truncate all but first.
55 auto f = locale.indexOf(c: QLatin1Char(':'));
56 if (f >= 0) {
57 locale.truncate(n: f);
58 }
59
60 // now decompose into [language[_territory][.codeset][@modifier]]
61 f = locale.indexOf(c: QLatin1Char('@'));
62 if (f >= 0) {
63 modifier = locale.mid(pos: f + 1);
64 locale.truncate(n: f);
65 }
66
67 f = locale.indexOf(c: QLatin1Char('.'));
68 if (f >= 0) {
69 charset = locale.mid(pos: f + 1);
70 locale.truncate(n: f);
71 }
72
73 f = locale.indexOf(c: QLatin1Char('_'));
74 if (f >= 0) {
75 country = locale.mid(pos: f + 1);
76 locale.truncate(n: f);
77 }
78
79 language = locale;
80}
81
82static void appendLocaleString(QStringList &languages, const QString &value)
83{
84 // Process the value to create possible combinations.
85 QStringView language;
86 QStringView country;
87 QStringView modifier;
88 QStringView charset;
89 splitLocale(aLocale: value, language, country, modifier, charset);
90
91 if (language.isEmpty()) {
92 return;
93 }
94
95 if (!country.isEmpty() && !modifier.isEmpty()) {
96 languages += language + QLatin1Char('_') + country + QLatin1Char('@') + modifier;
97 }
98 // NOTE: Priority is unclear in case both the country and
99 // the modifier are present. Should really language@modifier be of
100 // higher priority than language_country?
101 // In at least one case (Serbian language), it is better this way.
102 if (!modifier.isEmpty()) {
103 languages += language + QLatin1Char('@') + modifier;
104 }
105 if (!country.isEmpty()) {
106 languages += language + QLatin1Char('_') + country;
107 }
108 languages += language.toString();
109}
110
111static void appendLanguagesFromVariable(QStringList &languages, const char *envar, bool isList = false)
112{
113 QByteArray qenvar(qgetenv(varName: envar));
114 if (!qenvar.isEmpty()) {
115 QString value = QFile::decodeName(localFileName: qenvar);
116 if (isList) {
117 const auto listLanguages = value.split(sep: QLatin1Char(':'), behavior: Qt::SkipEmptyParts);
118 for (const QString &v : listLanguages) {
119 appendLocaleString(languages, value: v);
120 }
121 } else {
122 appendLocaleString(languages, value);
123 }
124 }
125}
126
127#if !defined(Q_OS_UNIX) || defined(Q_OS_ANDROID)
128static void appendLanguagesFromQLocale(QStringList &languages, const QLocale &locale)
129{
130 const QStringList uiLangs = locale.uiLanguages();
131 for (QString value : uiLangs) { // no const ref because of replace() below
132 appendLocaleString(languages, value.replace(QLatin1Char('-'), QLatin1Char('_')));
133 }
134}
135#endif
136
137// Extract the first country code from a list of language_COUNTRY strings.
138// Country code is converted to all lower case letters.
139static QString extractCountry(const QStringList &languages)
140{
141 QString country;
142 for (const QString &language : languages) {
143 auto pos1 = language.indexOf(ch: QLatin1Char('_'));
144 if (pos1 >= 0) {
145 ++pos1;
146 auto pos2 = pos1;
147 while (pos2 < language.length() && language[pos2].isLetter()) {
148 ++pos2;
149 }
150 country = language.mid(position: pos1, n: pos2 - pos1);
151 break;
152 }
153 }
154 country = country.toLower();
155 return country;
156}
157
158typedef qulonglong pluraln;
159typedef qlonglong intn;
160typedef qulonglong uintn;
161typedef double realn;
162
163class KLocalizedStringPrivate
164{
165 friend class KLocalizedString;
166
167 QByteArray domain;
168 QStringList languages;
169 Kuit::VisualFormat format = {};
170 QByteArray context;
171 QByteArray text;
172 QByteArray plural;
173 QStringList arguments;
174 QList<QVariant> values;
175 QHash<int, KLocalizedString> klsArguments;
176 QHash<int, int> klsArgumentFieldWidths;
177 QHash<int, QChar> klsArgumentFillChars;
178 bool numberSet = false;
179 pluraln number;
180 qsizetype numberOrdinal;
181 QHash<QString, QString> dynamicContext;
182 bool markupAware = false;
183 bool relaxedSubs = false;
184
185 KLocalizedStringPrivate() = default;
186
187 static void translateRaw(const QByteArray &domain,
188 const QStringList &languages,
189 const QByteArray &msgctxt,
190 const QByteArray &msgid,
191 const QByteArray &msgid_plural,
192 qulonglong n,
193 QString &language,
194 QString &msgstr);
195
196 QString toString(const QByteArray &domain, const QStringList &languages, Kuit::VisualFormat format, bool isArgument = false) const;
197 QString substituteSimple(const QString &translation, const QStringList &arguments, QChar plchar = QLatin1Char('%'), bool isPartial = false) const;
198 QString formatMarkup(const QByteArray &domain, const QString &language, const QString &context, const QString &text, Kuit::VisualFormat format) const;
199 QString substituteTranscript(const QString &scriptedTranslation,
200 const QString &language,
201 const QString &country,
202 const QString &ordinaryTranslation,
203 const QStringList &arguments,
204 const QList<QVariant> &values,
205 bool &fallback) const;
206 qsizetype resolveInterpolation(const QString &scriptedTranslation,
207 qsizetype pos,
208 const QString &language,
209 const QString &country,
210 const QString &ordinaryTranslation,
211 const QStringList &arguments,
212 const QList<QVariant> &values,
213 QString &result,
214 bool &fallback) const;
215 QVariant segmentToValue(const QString &segment) const;
216 QString postTranscript(const QString &pcall,
217 const QString &language,
218 const QString &country,
219 const QString &finalTranslation,
220 const QStringList &arguments,
221 const QList<QVariant> &values) const;
222
223 static const KCatalog &getCatalog(const QByteArray &domain, const QString &language);
224 static void locateScriptingModule(const QByteArray &domain, const QString &language);
225
226 static void loadTranscript();
227
228 void checkNumber(pluraln a)
229 {
230 if (!plural.isEmpty() && !numberSet) {
231 number = a;
232 numberSet = true;
233 numberOrdinal = arguments.size();
234 }
235 }
236};
237
238typedef QHash<QString, KCatalog *> KCatalogPtrHash;
239
240class LanguageChangeEventHandler : public QObject
241{
242 Q_OBJECT
243public:
244 using QObject::QObject;
245 bool eventFilter(QObject *obj, QEvent *ev) override;
246};
247
248class KLocalizedStringPrivateStatics
249{
250public:
251 QHash<QByteArray, KCatalogPtrHash> catalogs;
252 QStringList languages;
253
254 QByteArray ourDomain = QByteArrayLiteral("ki18n6");
255 QByteArray applicationDomain;
256 const QString codeLanguage = u"en_US"_s;
257 QStringList localeLanguages;
258 LanguageChangeEventHandler *languageChangeEventHandler = nullptr;
259
260 static constexpr inline auto theFence = "|/|"_L1;
261 static constexpr inline auto startInterp = "$["_L1;
262 static constexpr inline auto endInterp = "]"_L1;
263 static constexpr inline auto scriptPlchar = '%'_L1;
264 static constexpr inline auto scriptVachar = '^'_L1;
265
266 static constexpr inline auto scriptDir = "LC_SCRIPTS"_L1;
267 QHash<QString, QList<QByteArray>> scriptModules;
268 QList<QStringList> scriptModulesToLoad;
269
270 bool loadTranscriptCalled = false;
271 KTranscript *ktrs = nullptr;
272
273 QHash<QString, KuitFormatter *> formatters;
274
275 QList<QByteArray> qtDomains;
276 QList<int> qtDomainInsertCount;
277
278 QRecursiveMutex klspMutex;
279
280 KLocalizedStringPrivateStatics();
281 ~KLocalizedStringPrivateStatics();
282
283 void initializeLocaleLanguages();
284 void initializeLanguageChangeHandler();
285};
286
287KLocalizedStringPrivateStatics::KLocalizedStringPrivateStatics()
288{
289 initializeLocaleLanguages();
290 languages = localeLanguages;
291 initializeLanguageChangeHandler();
292}
293
294KLocalizedStringPrivateStatics::~KLocalizedStringPrivateStatics()
295{
296 for (const KCatalogPtrHash &languageCatalogs : std::as_const(t&: catalogs)) {
297 qDeleteAll(c: languageCatalogs);
298 }
299 // ktrs is handled by QLibrary.
300 // delete ktrs;
301 qDeleteAll(c: formatters);
302}
303
304Q_GLOBAL_STATIC(KLocalizedStringPrivateStatics, staticsKLSP)
305
306bool LanguageChangeEventHandler::eventFilter(QObject *obj, QEvent *ev)
307{
308 if (ev->type() == QEvent::LanguageChange && obj == QCoreApplication::instance()) {
309 const auto langOverride = staticsKLSP->languages != staticsKLSP->localeLanguages;
310 staticsKLSP->localeLanguages.clear();
311 staticsKLSP->initializeLocaleLanguages();
312 qCDebug(KI18N) << "languages changes from" << staticsKLSP->languages << "to" << staticsKLSP->localeLanguages;
313 if (!langOverride) {
314 staticsKLSP->languages = staticsKLSP->localeLanguages;
315 }
316 }
317 return QObject::eventFilter(watched: obj, event: ev);
318}
319
320void KLocalizedStringPrivateStatics::initializeLocaleLanguages()
321{
322 QMutexLocker lock(&klspMutex);
323
324 // Collect languages by same order of priority as for gettext(3).
325 // LANGUAGE contains list of language codes, not locale string.
326 appendLanguagesFromVariable(languages&: localeLanguages, envar: "LANGUAGE", isList: true);
327 appendLanguagesFromVariable(languages&: localeLanguages, envar: "LC_ALL");
328 appendLanguagesFromVariable(languages&: localeLanguages, envar: "LC_MESSAGES");
329 appendLanguagesFromVariable(languages&: localeLanguages, envar: "LANG");
330#if !defined(Q_OS_UNIX) || defined(Q_OS_ANDROID)
331 // For non UNIX platforms the environment variables might not
332 // suffice so we add system locale UI languages, too.
333 appendLanguagesFromQLocale(localeLanguages, QLocale::system());
334#endif
335
336 localeLanguages.removeDuplicates();
337
338 // make sure "en" falls back to "en_US" rather than trying other languages first
339 // this needs special-casing as the implicit fallback (lang_COUNTRY -> lang) doesn't
340 // work here as we don't have an "en" translation, but an (implicit) "en_US" one
341 const auto codeBaseLanguage = QStringView(codeLanguage).left(n: codeLanguage.indexOf(ch: '_'_L1));
342 auto it = std::find(first: localeLanguages.begin(), last: localeLanguages.end(), val: codeBaseLanguage);
343 if (it != localeLanguages.end()) {
344 *it = codeLanguage;
345 }
346}
347
348void KLocalizedStringPrivateStatics::initializeLanguageChangeHandler()
349{
350 if (languageChangeEventHandler || !QCoreApplication::instance()) {
351 return;
352 }
353
354 languageChangeEventHandler = new LanguageChangeEventHandler(QCoreApplication::instance());
355 QCoreApplication::instance()->installEventFilter(filterObj: languageChangeEventHandler);
356}
357
358// in case we were already called prior to QCoreApplication existing
359static void initializeLanguageChangeHandlerStartupHook()
360{
361 if (staticsKLSP.exists()) {
362 staticsKLSP->initializeLanguageChangeHandler();
363 }
364}
365
366Q_COREAPP_STARTUP_FUNCTION(initializeLanguageChangeHandlerStartupHook)
367
368KLocalizedString::KLocalizedString()
369 : d(new KLocalizedStringPrivate)
370{
371}
372
373KLocalizedString::KLocalizedString(const char *domain, const char *context, const char *text, const char *plural, bool markupAware)
374 : d(new KLocalizedStringPrivate)
375{
376 d->domain = domain;
377 d->languages.clear();
378 d->format = Kuit::UndefinedFormat;
379 d->context = context;
380 d->text = text;
381 d->plural = plural;
382 d->numberSet = false;
383 d->number = 0;
384 d->numberOrdinal = 0;
385 d->markupAware = markupAware;
386 d->relaxedSubs = false;
387}
388
389KLocalizedString::KLocalizedString(const KLocalizedString &rhs)
390 : d(new KLocalizedStringPrivate(*rhs.d))
391{
392}
393
394KLocalizedString &KLocalizedString::operator=(const KLocalizedString &rhs)
395{
396 if (&rhs != this) {
397 *d = *rhs.d;
398 }
399 return *this;
400}
401
402KLocalizedString::~KLocalizedString()
403{
404 delete d;
405}
406
407bool KLocalizedString::isEmpty() const
408{
409 return d->text.isEmpty();
410}
411
412void KLocalizedStringPrivate::translateRaw(const QByteArray &domain,
413 const QStringList &languages,
414 const QByteArray &msgctxt,
415 const QByteArray &msgid,
416 const QByteArray &msgid_plural,
417 qulonglong n,
418 QString &language,
419 QString &msgstr)
420{
421 KLocalizedStringPrivateStatics *s = staticsKLSP();
422
423 // Empty msgid would result in returning the catalog header,
424 // which is never intended, so warn and return empty translation.
425 if (msgid.isNull() || msgid.isEmpty()) {
426 qCWarning(KI18N) << "KLocalizedString: "
427 "Trying to look up translation of \"\", fix the code.";
428 language.clear();
429 msgstr.clear();
430 return;
431 }
432 // Gettext semantics allows empty context, but it is pointless, so warn.
433 if (!msgctxt.isNull() && msgctxt.isEmpty()) {
434 qCWarning(KI18N) << "KLocalizedString: "
435 "Using \"\" as context, fix the code.";
436 }
437 // Gettext semantics allows empty plural, but it is pointless, so warn.
438 if (!msgid_plural.isNull() && msgid_plural.isEmpty()) {
439 qCWarning(KI18N) << "KLocalizedString: "
440 "Using \"\" as plural text, fix the code.";
441 }
442
443 // Set translation to text in code language, in case no translation found.
444 msgstr = msgid_plural.isNull() || n == 1 ? QString::fromUtf8(ba: msgid) : QString::fromUtf8(ba: msgid_plural);
445 language = s->codeLanguage;
446
447 if (domain.isEmpty()) {
448 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
449 << "msgctxt:" << msgctxt;
450 return;
451 }
452
453 // Languages are ordered from highest to lowest priority.
454 for (const QString &testLanguage : languages) {
455 // If code language reached, no catalog lookup is needed.
456 if (testLanguage == s->codeLanguage) {
457 return;
458 }
459 const KCatalog &catalog = getCatalog(domain, language: testLanguage);
460 QString testMsgstr;
461 if (!msgctxt.isNull() && !msgid_plural.isNull()) {
462 testMsgstr = catalog.translate(msgctxt, msgid, msgid_plural, n);
463 } else if (!msgid_plural.isNull()) {
464 testMsgstr = catalog.translate(msgid, msgid_plural, n);
465 } else if (!msgctxt.isNull()) {
466 testMsgstr = catalog.translate(msgctxt, msgid);
467 } else {
468 testMsgstr = catalog.translate(msgid);
469 }
470 if (!testMsgstr.isEmpty()) {
471 // Translation found.
472 language = testLanguage;
473 msgstr = testMsgstr;
474 return;
475 }
476 }
477}
478
479QString KLocalizedString::toString() const
480{
481 return d->toString(domain: d->domain, languages: d->languages, format: d->format);
482}
483
484QString KLocalizedString::toString(const char *domain) const
485{
486 return d->toString(domain, languages: d->languages, format: d->format);
487}
488
489QString KLocalizedString::toString(const QStringList &languages) const
490{
491 return d->toString(domain: d->domain, languages, format: d->format);
492}
493
494QString KLocalizedString::toString(Kuit::VisualFormat format) const
495{
496 return d->toString(domain: d->domain, languages: d->languages, format);
497}
498
499QString KLocalizedStringPrivate::toString(const QByteArray &domain, const QStringList &languages, Kuit::VisualFormat format, bool isArgument) const
500{
501 KLocalizedStringPrivateStatics *s = staticsKLSP();
502
503 QMutexLocker lock(&s->klspMutex);
504
505 // Assure the message has been supplied.
506 if (text.isEmpty()) {
507 qCWarning(KI18N) << "Trying to convert empty KLocalizedString to QString.";
508#ifndef NDEBUG
509 return QStringLiteral("(I18N_EMPTY_MESSAGE)");
510#else
511 return QString();
512#endif
513 }
514
515 // Check whether plural argument has been supplied, if message has plural.
516 if (!plural.isEmpty() && !numberSet) {
517 qCWarning(KI18N) << "Plural argument to message" << shortenMessage(str: QString::fromUtf8(ba: text)) << "not supplied before conversion.";
518 }
519
520 // Resolve inputs.
521 QByteArray resolvedDomain = domain;
522 if (resolvedDomain.isEmpty()) {
523 resolvedDomain = s->applicationDomain;
524 }
525 QStringList resolvedLanguages = languages;
526 if (resolvedLanguages.isEmpty()) {
527 resolvedLanguages = s->languages;
528 }
529 Kuit::VisualFormat resolvedFormat = format;
530
531 // Get raw translation.
532 QString language;
533 QString rawTranslation;
534 translateRaw(domain: resolvedDomain, languages: resolvedLanguages, msgctxt: context, msgid: text, msgid_plural: plural, n: number, language, msgstr&: rawTranslation);
535 std::optional<QString> country; // initialized when needed
536
537 // Set ordinary translation and possibly scripted translation.
538 QString translation;
539 QString scriptedTranslation;
540 auto fencePos = rawTranslation.indexOf(s: s->theFence);
541 if (fencePos > 0) {
542 // Script fence has been found, strip the scripted from the
543 // ordinary translation.
544 translation = rawTranslation.left(n: fencePos);
545
546 // Scripted translation.
547 scriptedTranslation = rawTranslation.mid(position: fencePos + s->theFence.length());
548
549 // Try to initialize Transcript if not initialized and script not empty.
550 // FIXME: And also if Transcript not disabled: where to configure this?
551 if (!s->loadTranscriptCalled && !scriptedTranslation.isEmpty()) {
552 loadTranscript();
553
554 // Definitions from this library's scripting module
555 // must be available to all other modules.
556 // So force creation of this library's catalog here,
557 // to make sure the scripting module is loaded.
558 getCatalog(domain: s->ourDomain, language);
559 }
560 } else if (fencePos < 0) {
561 // No script fence, use translation as is.
562 translation = rawTranslation;
563 } else { // fencePos == 0
564 // The msgstr starts with the script fence, no ordinary translation.
565 // This is not allowed, consider message not translated.
566 qCWarning(KI18N) << "Scripted message" << shortenMessage(str: translation) << "without ordinary translation, discarded.";
567 translation = plural.isEmpty() || number == 1 ? QString::fromUtf8(ba: text) : QString::fromUtf8(ba: plural);
568 }
569
570 // Resolve substituted KLocalizedString arguments.
571 QStringList resolvedArguments;
572 QList<QVariant> resolvedValues;
573 for (int i = 0; i < arguments.size(); i++) {
574 auto lsIt = klsArguments.constFind(key: i);
575 if (lsIt != klsArguments.constEnd()) {
576 const KLocalizedString &kls = *lsIt;
577 int fieldWidth = klsArgumentFieldWidths.value(key: i);
578 QChar fillChar = klsArgumentFillChars.value(key: i);
579 // Override argument's languages and format, but not domain.
580 bool isArgumentSub = true;
581 QString resdArg = kls.d->toString(domain: kls.d->domain, languages: resolvedLanguages, format: resolvedFormat, isArgument: isArgumentSub);
582 resolvedValues.append(t: resdArg);
583 if (markupAware && !kls.d->markupAware) {
584 resdArg = Kuit::escape(text: resdArg);
585 }
586 resdArg = QStringLiteral("%1").arg(a: resdArg, fieldWidth, fillChar);
587 resolvedArguments.append(t: resdArg);
588 } else {
589 QString resdArg = arguments[i];
590 if (markupAware) {
591 resdArg = Kuit::escape(text: resdArg);
592 }
593 resolvedArguments.append(t: resdArg);
594 resolvedValues.append(t: values[i]);
595 }
596 }
597
598 // Substitute placeholders in ordinary translation.
599 QString finalTranslation = substituteSimple(translation, arguments: resolvedArguments);
600 if (markupAware && !isArgument) {
601 // Resolve markup in ordinary translation.
602 finalTranslation = formatMarkup(domain: resolvedDomain, language, context: QString::fromUtf8(ba: context), text: finalTranslation, format: resolvedFormat);
603 }
604
605 // If there is also a scripted translation.
606 if (!scriptedTranslation.isEmpty()) {
607 // Evaluate scripted translation.
608 bool fallback = false;
609 country = extractCountry(languages: resolvedLanguages);
610 scriptedTranslation = substituteTranscript(scriptedTranslation, language, country: *country, ordinaryTranslation: finalTranslation, arguments: resolvedArguments, values: resolvedValues, fallback);
611
612 // If any translation produced and no fallback requested.
613 if (!scriptedTranslation.isEmpty() && !fallback) {
614 if (markupAware && !isArgument) {
615 // Resolve markup in scripted translation.
616 scriptedTranslation = formatMarkup(domain: resolvedDomain, language, context: QString::fromUtf8(ba: context), text: scriptedTranslation, format: resolvedFormat);
617 }
618 finalTranslation = scriptedTranslation;
619 }
620 }
621
622 // Execute any scripted post calls; they cannot modify the final result,
623 // but are used to set states.
624 if (s->ktrs != nullptr) {
625 if (!country.has_value()) {
626 country = extractCountry(languages: resolvedLanguages);
627 }
628 const QStringList pcalls = s->ktrs->postCalls(lang: language);
629 for (const QString &pcall : pcalls) {
630 postTranscript(pcall, language, country: *country, finalTranslation, arguments: resolvedArguments, values: resolvedValues);
631 }
632 }
633
634 return finalTranslation;
635}
636
637QString KLocalizedStringPrivate::substituteSimple(const QString &translationString, const QStringList &arguments, QChar plchar, bool isPartial) const
638{
639#ifdef NDEBUG
640 Q_UNUSED(isPartial);
641#endif
642
643 QStringView translation = translationString;
644 QVarLengthArray<QStringView, 8> tsegs; // text segments per placeholder occurrence
645 QVarLengthArray<int, 8> plords; // ordinal numbers per placeholder occurrence
646#ifndef NDEBUG
647 QList<int> ords; // indicates which placeholders are present
648#endif
649 auto slen = translation.length();
650 qsizetype spos = 0;
651 auto tpos = translation.indexOf(c: plchar);
652 while (tpos >= 0) {
653 auto ctpos = tpos;
654
655 ++tpos;
656 if (tpos == slen) {
657 break;
658 }
659
660 if (translation[tpos].digitValue() > 0) {
661 // NOTE: %0 is not considered a placeholder.
662 // Get the placeholder ordinal.
663 int plord = 0;
664 while (tpos < slen && translation[tpos].digitValue() >= 0) {
665 plord = 10 * plord + translation[tpos].digitValue();
666 ++tpos;
667 }
668 --plord; // ordinals are zero based
669
670#ifndef NDEBUG
671 // Perhaps enlarge storage for indicators.
672 // Note that QList<int> will initialize new elements to 0,
673 // as they are supposed to be.
674 if (plord >= ords.size()) {
675 ords.resize(size: plord + 1);
676 }
677
678 // Indicate that placeholder with computed ordinal is present.
679 ords[plord] = 1;
680#endif
681
682 // Store text segment prior to placeholder and placeholder number.
683 tsegs.append(t: translation.mid(pos: spos, n: ctpos - spos));
684 plords.append(t: plord);
685
686 // Position of next text segment.
687 spos = tpos;
688 }
689
690 tpos = translation.indexOf(c: plchar, from: tpos);
691 }
692 // Store last text segment.
693 tsegs.append(t: translation.mid(pos: spos));
694
695#ifndef NDEBUG
696 // Perhaps enlarge storage for plural-number ordinal.
697 if (!plural.isEmpty() && numberOrdinal >= ords.size()) {
698 ords.resize(size: numberOrdinal + 1);
699 }
700
701 // Message might have plural but without plural placeholder, which is an
702 // allowed state. To ease further logic, indicate that plural placeholder
703 // is present anyway if message has plural.
704 if (!plural.isEmpty()) {
705 ords[numberOrdinal] = 1;
706 }
707#endif
708
709 // Assemble the final string from text segments and arguments.
710 QString finalTranslation;
711 for (int i = 0; i < plords.size(); i++) {
712 finalTranslation.append(v: tsegs.at(idx: i));
713 if (plords.at(idx: i) >= arguments.size()) { // too little arguments
714 // put back the placeholder
715 finalTranslation.append(s: QLatin1Char('%') + QString::number(plords.at(idx: i) + 1));
716#ifndef NDEBUG
717 if (!isPartial) {
718 // spoof the message
719 finalTranslation.append(QStringLiteral("(I18N_ARGUMENT_MISSING)"));
720 }
721#endif
722 } else { // just fine
723 finalTranslation.append(s: arguments.at(i: plords.at(idx: i)));
724 }
725 }
726 finalTranslation.append(v: tsegs.last());
727
728#ifndef NDEBUG
729 if (!isPartial && !relaxedSubs) {
730 // Check that there are no gaps in numbering sequence of placeholders.
731 bool gaps = false;
732 for (int i = 0; i < ords.size(); i++) {
733 if (!ords.at(i)) {
734 gaps = true;
735 qCWarning(KI18N).nospace() << "Placeholder %" << QString::number(i + 1) << " skipped in message " << shortenMessage(str: translation.toString());
736 }
737 }
738 // If no gaps, check for mismatch between the number of
739 // unique placeholders and actually supplied arguments.
740 if (!gaps && ords.size() != arguments.size()) {
741 qCWarning(KI18N) << arguments.size() << "instead of" << ords.size() << "arguments to message" << shortenMessage(str: translation.toString())
742 << "supplied before conversion";
743 }
744
745 // Some spoofs.
746 if (gaps) {
747 finalTranslation.append(QStringLiteral("(I18N_GAPS_IN_PLACEHOLDER_SEQUENCE)"));
748 }
749 if (ords.size() < arguments.size()) {
750 finalTranslation.append(QStringLiteral("(I18N_EXCESS_ARGUMENTS_SUPPLIED)"));
751 }
752 }
753 if (!isPartial) {
754 if (!plural.isEmpty() && !numberSet) {
755 finalTranslation.append(QStringLiteral("(I18N_PLURAL_ARGUMENT_MISSING)"));
756 }
757 }
758#endif
759
760 return finalTranslation;
761}
762
763QString KLocalizedStringPrivate::formatMarkup(const QByteArray &domain,
764 const QString &language,
765 const QString &context,
766 const QString &text,
767 Kuit::VisualFormat format) const
768{
769 KLocalizedStringPrivateStatics *s = staticsKLSP();
770
771 QHash<QString, KuitFormatter *>::iterator formatter = s->formatters.find(key: language);
772 if (formatter == s->formatters.end()) {
773 formatter = s->formatters.insert(key: language, value: new KuitFormatter(language));
774 }
775 return (*formatter)->format(domain, context, text, format);
776}
777
778QString KLocalizedStringPrivate::substituteTranscript(const QString &scriptedTranslation,
779 const QString &language,
780 const QString &country,
781 const QString &ordinaryTranslation,
782 const QStringList &arguments,
783 const QList<QVariant> &values,
784 bool &fallback) const
785{
786 KLocalizedStringPrivateStatics *s = staticsKLSP();
787
788 if (s->ktrs == nullptr) {
789 // Scripting engine not available.
790 return QString();
791 }
792
793 // Iterate by interpolations.
794 QString finalTranslation;
795 fallback = false;
796 qsizetype ppos = 0;
797 auto tpos = scriptedTranslation.indexOf(s: s->startInterp);
798 while (tpos >= 0) {
799 // Resolve substitutions in preceding text.
800 QString ptext = substituteSimple(translationString: scriptedTranslation.mid(position: ppos, n: tpos - ppos), arguments, plchar: s->scriptPlchar, isPartial: true);
801 finalTranslation.append(s: ptext);
802
803 // Resolve interpolation.
804 QString result;
805 bool fallbackLocal;
806 tpos = resolveInterpolation(scriptedTranslation, pos: tpos, language, country, ordinaryTranslation, arguments, values, result, fallback&: fallbackLocal);
807
808 // If there was a problem in parsing the interpolation, cannot proceed
809 // (debug info already reported while parsing).
810 if (tpos < 0) {
811 return QString();
812 }
813 // If fallback has been explicitly requested, indicate global fallback
814 // but proceed with evaluations (other interpolations may set states).
815 if (fallbackLocal) {
816 fallback = true;
817 }
818
819 // Add evaluated interpolation to the text.
820 finalTranslation.append(s: result);
821
822 // On to next interpolation.
823 ppos = tpos;
824 tpos = scriptedTranslation.indexOf(s: s->startInterp, from: tpos);
825 }
826 // Last text segment.
827 finalTranslation.append(s: substituteSimple(translationString: scriptedTranslation.mid(position: ppos), arguments, plchar: s->scriptPlchar, isPartial: true));
828
829 // Return empty string if fallback was requested.
830 return fallback ? QString() : finalTranslation;
831}
832
833qsizetype KLocalizedStringPrivate::resolveInterpolation(const QString &scriptedTranslation,
834 qsizetype pos,
835 const QString &language,
836 const QString &country,
837 const QString &ordinaryTranslation,
838 const QStringList &arguments,
839 const QList<QVariant> &values,
840 QString &result,
841 bool &fallback) const
842{
843 // pos is the position of opening character sequence.
844 // Returns the position of first character after closing sequence,
845 // or -1 in case of parsing error.
846 // result is set to result of Transcript evaluation.
847 // fallback is set to true if Transcript evaluation requested so.
848
849 KLocalizedStringPrivateStatics *s = staticsKLSP();
850
851 result.clear();
852 fallback = false;
853
854 // Split interpolation into arguments.
855 QList<QVariant> iargs;
856 const qsizetype slen = scriptedTranslation.length();
857 const qsizetype islen = s->startInterp.length();
858 const qsizetype ielen = s->endInterp.length();
859 qsizetype tpos = pos + s->startInterp.length();
860 while (1) {
861 // Skip whitespace.
862 while (tpos < slen && scriptedTranslation[tpos].isSpace()) {
863 ++tpos;
864 }
865 if (tpos == slen) {
866 qCWarning(KI18N) << "Unclosed interpolation" << scriptedTranslation.mid(position: pos, n: tpos - pos) << "in message" << shortenMessage(str: scriptedTranslation);
867 return -1;
868 }
869 if (QStringView(scriptedTranslation).mid(pos: tpos, n: ielen) == s->endInterp) {
870 break; // no more arguments
871 }
872
873 // Parse argument: may be concatenated from free and quoted text,
874 // and sub-interpolations.
875 // Free and quoted segments may contain placeholders, substitute them;
876 // recurse into sub-interpolations.
877 // Free segments may be value references, parse and record for
878 // consideration at the end.
879 // Mind backslash escapes throughout.
880 QStringList segs;
881 QVariant vref;
882 while (!scriptedTranslation[tpos].isSpace() && scriptedTranslation.mid(position: tpos, n: ielen) != s->endInterp) {
883 if (scriptedTranslation[tpos] == QLatin1Char('\'')) { // quoted segment
884 QString seg;
885 ++tpos; // skip opening quote
886 // Find closing quote.
887 while (tpos < slen && scriptedTranslation[tpos] != QLatin1Char('\'')) {
888 if (scriptedTranslation[tpos] == QLatin1Char('\\')) {
889 ++tpos; // escape next character
890 }
891 seg.append(c: scriptedTranslation[tpos]);
892 ++tpos;
893 }
894 if (tpos == slen) {
895 qCWarning(KI18N) << "Unclosed quote in interpolation" << scriptedTranslation.mid(position: pos, n: tpos - pos) << "in message"
896 << shortenMessage(str: scriptedTranslation);
897 return -1;
898 }
899
900 // Append to list of segments, resolving placeholders.
901 segs.append(t: substituteSimple(translationString: seg, arguments, plchar: s->scriptPlchar, isPartial: true));
902
903 ++tpos; // skip closing quote
904 } else if (scriptedTranslation.mid(position: tpos, n: islen) == s->startInterp) { // sub-interpolation
905 QString resultLocal;
906 bool fallbackLocal;
907 tpos = resolveInterpolation(scriptedTranslation, pos: tpos, language, country, ordinaryTranslation, arguments, values, result&: resultLocal, fallback&: fallbackLocal);
908 if (tpos < 0) { // unrecoverable problem in sub-interpolation
909 // Error reported in the subcall.
910 return tpos;
911 }
912 if (fallbackLocal) { // sub-interpolation requested fallback
913 fallback = true;
914 }
915 segs.append(t: resultLocal);
916 } else { // free segment
917 QString seg;
918 // Find whitespace, quote, opening or closing sequence.
919 while (tpos < slen && !scriptedTranslation[tpos].isSpace() //
920 && scriptedTranslation[tpos] != QLatin1Char('\'') //
921 && scriptedTranslation.mid(position: tpos, n: islen) != s->startInterp //
922 && scriptedTranslation.mid(position: tpos, n: ielen) != s->endInterp) {
923 if (scriptedTranslation[tpos] == QLatin1Char('\\')) {
924 ++tpos; // escape next character
925 }
926 seg.append(c: scriptedTranslation[tpos]);
927 ++tpos;
928 }
929 if (tpos == slen) {
930 qCWarning(KI18N) << "Non-terminated interpolation" << scriptedTranslation.mid(position: pos, n: tpos - pos) << "in message"
931 << shortenMessage(str: scriptedTranslation);
932 return -1;
933 }
934
935 // The free segment may look like a value reference;
936 // in that case, record which value it would reference,
937 // and add verbatim to the segment list.
938 // Otherwise, do a normal substitution on the segment.
939 vref = segmentToValue(segment: seg);
940 if (vref.isValid()) {
941 segs.append(t: seg);
942 } else {
943 segs.append(t: substituteSimple(translationString: seg, arguments, plchar: s->scriptPlchar, isPartial: true));
944 }
945 }
946 }
947
948 // Append this argument to rest of the arguments.
949 // If the there was a single text segment and it was a proper value
950 // reference, add it instead of the joined segments.
951 // Otherwise, add the joined segments.
952 if (segs.size() == 1 && vref.isValid()) {
953 iargs.append(t: vref);
954 } else {
955 iargs.append(t: segs.join(sep: QString()));
956 }
957 }
958 tpos += ielen; // skip to first character after closing sequence
959
960 // NOTE: Why not substitute placeholders (via substituteSimple) in one
961 // global pass, then handle interpolations in second pass? Because then
962 // there is the danger of substituted text or sub-interpolations producing
963 // quotes and escapes themselves, which would mess up the parsing.
964
965 // Evaluate interpolation.
966 QString msgctxt = QString::fromUtf8(ba: context);
967 QString msgid = QString::fromUtf8(ba: text);
968 QString scriptError;
969 bool fallbackLocal;
970 result = s->ktrs->eval(argv: iargs,
971 lang: language,
972 ctry: country,
973 msgctxt,
974 dynctxt: dynamicContext,
975 msgid,
976 subs: arguments,
977 vals: values,
978 ftrans: ordinaryTranslation,
979 mods&: s->scriptModulesToLoad,
980 error&: scriptError,
981 fallback&: fallbackLocal);
982 // s->scriptModulesToLoad will be cleared during the call.
983
984 if (fallbackLocal) { // evaluation requested fallback
985 fallback = true;
986 }
987 if (!scriptError.isEmpty()) { // problem with evaluation
988 fallback = true; // also signal fallback
989 if (!scriptError.isEmpty()) {
990 qCWarning(KI18N) << "Interpolation" << scriptedTranslation.mid(position: pos, n: tpos - pos) << "in" << shortenMessage(str: scriptedTranslation)
991 << "failed:" << scriptError;
992 }
993 }
994
995 return tpos;
996}
997
998QVariant KLocalizedStringPrivate::segmentToValue(const QString &segment) const
999{
1000 KLocalizedStringPrivateStatics *s = staticsKLSP();
1001
1002 // Return invalid variant if segment is either not a proper
1003 // value reference, or the reference is out of bounds.
1004
1005 // Value reference must start with a special character.
1006 if (!segment.startsWith(c: s->scriptVachar)) {
1007 return QVariant();
1008 }
1009
1010 // Reference number must start with 1-9.
1011 // (If numstr is empty, toInt() will return 0.)
1012 QString numstr = segment.mid(position: 1);
1013 int numstrAsInt = QStringView(numstr).left(n: 1).toInt();
1014 if (numstrAsInt < 1) {
1015 return QVariant();
1016 }
1017
1018 // Number must be valid and in bounds.
1019 bool ok;
1020 int index = numstr.toInt(ok: &ok) - 1;
1021 if (!ok || index >= values.size()) {
1022 return QVariant();
1023 }
1024
1025 // Passed all hoops.
1026 return values.at(i: index);
1027}
1028
1029QString KLocalizedStringPrivate::postTranscript(const QString &pcall,
1030 const QString &language,
1031 const QString &country,
1032 const QString &finalTranslation,
1033 const QStringList &arguments,
1034 const QList<QVariant> &values) const
1035{
1036 KLocalizedStringPrivateStatics *s = staticsKLSP();
1037
1038 if (s->ktrs == nullptr) {
1039 // Scripting engine not available.
1040 // (Though this cannot happen, we wouldn't be here then.)
1041 return QString();
1042 }
1043
1044 // Resolve the post call.
1045 QList<QVariant> iargs;
1046 iargs.append(t: pcall);
1047 QString msgctxt = QString::fromUtf8(ba: context);
1048 QString msgid = QString::fromUtf8(ba: text);
1049 QString scriptError;
1050 bool fallback;
1051 s->ktrs->eval(argv: iargs, lang: language, ctry: country, msgctxt, dynctxt: dynamicContext, msgid, subs: arguments, vals: values, ftrans: finalTranslation, mods&: s->scriptModulesToLoad, error&: scriptError, fallback);
1052 // s->scriptModulesToLoad will be cleared during the call.
1053
1054 // If the evaluation went wrong.
1055 if (!scriptError.isEmpty()) {
1056 qCWarning(KI18N) << "Post call" << pcall << "for message" << shortenMessage(str: msgid) << "failed:" << scriptError;
1057 return QString();
1058 }
1059
1060 return finalTranslation;
1061}
1062
1063KLocalizedString KLocalizedString::withLanguages(const QStringList &languages) const
1064{
1065 KLocalizedString kls(*this);
1066 kls.d->languages = languages;
1067 return kls;
1068}
1069
1070KLocalizedString KLocalizedString::withDomain(const char *domain) const
1071{
1072 KLocalizedString kls(*this);
1073 kls.d->domain = domain;
1074 return kls;
1075}
1076
1077KLocalizedString KLocalizedString::withFormat(Kuit::VisualFormat format) const
1078{
1079 KLocalizedString kls(*this);
1080 kls.d->format = format;
1081 return kls;
1082}
1083
1084KLocalizedString KLocalizedString::subs(int a, int fieldWidth, int base, QChar fillChar) const
1085{
1086 KLocalizedString kls(*this);
1087 kls.d->checkNumber(a: std::abs(x: a));
1088 kls.d->arguments.append(QStringLiteral("%L1").arg(a, fieldWidth, base, fillChar));
1089 kls.d->values.append(t: static_cast<intn>(a));
1090 return kls;
1091}
1092
1093KLocalizedString KLocalizedString::subs(uint a, int fieldWidth, int base, QChar fillChar) const
1094{
1095 KLocalizedString kls(*this);
1096 kls.d->checkNumber(a);
1097 kls.d->arguments.append(QStringLiteral("%L1").arg(a, fieldWidth, base, fillChar));
1098 kls.d->values.append(t: static_cast<uintn>(a));
1099 return kls;
1100}
1101
1102KLocalizedString KLocalizedString::subs(long a, int fieldWidth, int base, QChar fillChar) const
1103{
1104 KLocalizedString kls(*this);
1105 kls.d->checkNumber(a: std::abs(i: a));
1106 kls.d->arguments.append(QStringLiteral("%L1").arg(a, fieldWidth, base, fillChar));
1107 kls.d->values.append(t: static_cast<intn>(a));
1108 return kls;
1109}
1110
1111KLocalizedString KLocalizedString::subs(ulong a, int fieldWidth, int base, QChar fillChar) const
1112{
1113 KLocalizedString kls(*this);
1114 kls.d->checkNumber(a);
1115 kls.d->arguments.append(QStringLiteral("%L1").arg(a, fieldWidth, base, fillChar));
1116 kls.d->values.append(t: static_cast<uintn>(a));
1117 return kls;
1118}
1119
1120KLocalizedString KLocalizedString::subs(qlonglong a, int fieldWidth, int base, QChar fillChar) const
1121{
1122 KLocalizedString kls(*this);
1123 kls.d->checkNumber(a: qAbs(t: a));
1124 kls.d->arguments.append(QStringLiteral("%L1").arg(a, fieldwidth: fieldWidth, base, fillChar));
1125 kls.d->values.append(t: static_cast<intn>(a));
1126 return kls;
1127}
1128
1129KLocalizedString KLocalizedString::subs(qulonglong a, int fieldWidth, int base, QChar fillChar) const
1130{
1131 KLocalizedString kls(*this);
1132 kls.d->checkNumber(a);
1133 kls.d->arguments.append(QStringLiteral("%L1").arg(a, fieldwidth: fieldWidth, base, fillChar));
1134 kls.d->values.append(t: static_cast<uintn>(a));
1135 return kls;
1136}
1137
1138KLocalizedString KLocalizedString::subs(double a, int fieldWidth, char format, int precision, QChar fillChar) const
1139{
1140 KLocalizedString kls(*this);
1141 kls.d->arguments.append(QStringLiteral("%L1").arg(a, fieldWidth, format, precision, fillChar));
1142 kls.d->values.append(t: static_cast<realn>(a));
1143 return kls;
1144}
1145
1146KLocalizedString KLocalizedString::subs(QChar a, int fieldWidth, QChar fillChar) const
1147{
1148 KLocalizedString kls(*this);
1149 QString baseArg = QString(a);
1150 QString fmtdArg = QStringLiteral("%1").arg(a, fieldWidth, fillChar);
1151 kls.d->arguments.append(t: fmtdArg);
1152 kls.d->values.append(t: baseArg);
1153 return kls;
1154}
1155
1156KLocalizedString KLocalizedString::subs(const QString &a, int fieldWidth, QChar fillChar) const
1157{
1158 KLocalizedString kls(*this);
1159 QString baseArg = a;
1160 QString fmtdArg = QStringLiteral("%1").arg(a, fieldWidth, fillChar);
1161 kls.d->arguments.append(t: fmtdArg);
1162 kls.d->values.append(t: baseArg);
1163 return kls;
1164}
1165
1166KLocalizedString KLocalizedString::subs(const KLocalizedString &a, int fieldWidth, QChar fillChar) const
1167{
1168 KLocalizedString kls(*this);
1169 // KLocalizedString arguments must be resolved inside toString
1170 // when the domain, language, visual format, etc. become known.
1171 int i = kls.d->arguments.size();
1172 kls.d->klsArguments[i] = a;
1173 kls.d->klsArgumentFieldWidths[i] = fieldWidth;
1174 kls.d->klsArgumentFillChars[i] = fillChar;
1175 kls.d->arguments.append(t: QString());
1176 kls.d->values.append(t: 0);
1177 return kls;
1178}
1179
1180KLocalizedString KLocalizedString::inContext(const QString &key, const QString &value) const
1181{
1182 KLocalizedString kls(*this);
1183 kls.d->dynamicContext[key] = value;
1184 return kls;
1185}
1186
1187KLocalizedString KLocalizedString::relaxSubs() const
1188{
1189 KLocalizedString kls(*this);
1190 kls.d->relaxedSubs = true;
1191 return kls;
1192}
1193
1194KLocalizedString KLocalizedString::ignoreMarkup() const
1195{
1196 KLocalizedString kls(*this);
1197 kls.d->markupAware = false;
1198 return kls;
1199}
1200
1201QByteArray KLocalizedString::untranslatedText() const
1202{
1203 return d->text;
1204}
1205
1206void KLocalizedString::setApplicationDomain(const QByteArray &domain)
1207{
1208 KLocalizedStringPrivateStatics *s = staticsKLSP();
1209
1210 QMutexLocker lock(&s->klspMutex);
1211
1212 s->applicationDomain = domain;
1213}
1214
1215QByteArray KLocalizedString::applicationDomain()
1216{
1217 KLocalizedStringPrivateStatics *s = staticsKLSP();
1218
1219 return s->applicationDomain;
1220}
1221
1222QStringList KLocalizedString::languages()
1223{
1224 KLocalizedStringPrivateStatics *s = staticsKLSP();
1225
1226 return s->languages;
1227}
1228
1229void KLocalizedString::setLanguages(const QStringList &languages)
1230{
1231 KLocalizedStringPrivateStatics *s = staticsKLSP();
1232
1233 QMutexLocker lock(&s->klspMutex);
1234
1235 s->languages = languages;
1236}
1237
1238void KLocalizedString::clearLanguages()
1239{
1240 KLocalizedStringPrivateStatics *s = staticsKLSP();
1241
1242 QMutexLocker lock(&s->klspMutex);
1243
1244 s->languages = s->localeLanguages;
1245}
1246
1247bool KLocalizedString::isApplicationTranslatedInto(const QString &language)
1248{
1249 KLocalizedStringPrivateStatics *s = staticsKLSP();
1250
1251 return language == s->codeLanguage || !KCatalog::catalogLocaleDir(domain: s->applicationDomain, language).isEmpty();
1252}
1253
1254QSet<QString> KLocalizedString::availableApplicationTranslations()
1255{
1256 return availableDomainTranslations(domain: staticsKLSP()->applicationDomain);
1257}
1258
1259QSet<QString> KLocalizedString::availableDomainTranslations(const QByteArray &domain)
1260{
1261 QSet<QString> availableLanguages;
1262
1263 if (!domain.isEmpty()) {
1264 availableLanguages = KCatalog::availableCatalogLanguages(domain);
1265 availableLanguages.insert(value: staticsKLSP()->codeLanguage);
1266 }
1267
1268 return availableLanguages;
1269}
1270
1271const KCatalog &KLocalizedStringPrivate::getCatalog(const QByteArray &domain, const QString &language)
1272{
1273 KLocalizedStringPrivateStatics *s = staticsKLSP();
1274
1275 QMutexLocker lock(&s->klspMutex);
1276
1277 QHash<QByteArray, KCatalogPtrHash>::iterator languageCatalogs = s->catalogs.find(key: domain);
1278 if (languageCatalogs == s->catalogs.end()) {
1279 languageCatalogs = s->catalogs.insert(key: domain, value: KCatalogPtrHash());
1280 }
1281 KCatalogPtrHash::iterator catalog = languageCatalogs->find(key: language);
1282 if (catalog == languageCatalogs->end()) {
1283 catalog = languageCatalogs->insert(key: language, value: new KCatalog(domain, language));
1284 locateScriptingModule(domain, language);
1285 }
1286 return **catalog;
1287}
1288
1289void KLocalizedStringPrivate::locateScriptingModule(const QByteArray &domain, const QString &language)
1290{
1291 KLocalizedStringPrivateStatics *s = staticsKLSP();
1292
1293 QMutexLocker lock(&s->klspMutex);
1294
1295 QString modapath =
1296 QStandardPaths::locate(type: QStandardPaths::GenericDataLocation, fileName: QLatin1String("locale/%1/%2/%3/%3.js").arg(args: language, args: s->scriptDir, args: QLatin1String{domain}));
1297
1298 // If the module exists and hasn't been already included.
1299 if (!modapath.isEmpty() && !s->scriptModules[language].contains(t: domain)) {
1300 // Indicate that the module has been considered.
1301 s->scriptModules[language].append(t: domain);
1302
1303 // Store the absolute path and language of the module,
1304 // to load on next script evaluation.
1305 QStringList module;
1306 module.append(t: modapath);
1307 module.append(t: language);
1308 s->scriptModulesToLoad.append(t: module);
1309 }
1310}
1311
1312extern "C" {
1313#if HAVE_STATIC_KTRANSCRIPT
1314extern KTranscript *load_transcript();
1315#else
1316typedef KTranscript *(*InitFunc)();
1317#endif
1318}
1319
1320void KLocalizedStringPrivate::loadTranscript()
1321{
1322 KLocalizedStringPrivateStatics *s = staticsKLSP();
1323
1324 QMutexLocker lock(&s->klspMutex);
1325
1326 s->loadTranscriptCalled = true;
1327 s->ktrs = nullptr; // null indicates that Transcript is not available
1328
1329#if HAVE_STATIC_KTRANSCRIPT
1330 s->ktrs = load_transcript();
1331#else
1332 // QPluginLoader is just used to find the plugin
1333 QPluginLoader loader(QStringLiteral("kf6/ktranscript"));
1334 if (loader.fileName().isEmpty()) {
1335 qCWarning(KI18N) << "Cannot find Transcript plugin.";
1336 return;
1337 }
1338
1339 QLibrary lib(loader.fileName());
1340 if (!lib.load()) {
1341 qCWarning(KI18N) << "Cannot load Transcript plugin:" << lib.errorString();
1342 return;
1343 }
1344
1345 InitFunc initf = (InitFunc)lib.resolve(symbol: "load_transcript");
1346 if (!initf) {
1347 lib.unload();
1348 qCWarning(KI18N) << "Cannot find function load_transcript in Transcript plugin.";
1349 return;
1350 }
1351
1352 s->ktrs = initf();
1353#endif
1354}
1355
1356QString KLocalizedString::localizedFilePath(const QString &filePath)
1357{
1358 KLocalizedStringPrivateStatics *s = staticsKLSP();
1359
1360 // Check if l10n subdirectory is present, stop if not.
1361 QFileInfo fileInfo(filePath);
1362 QString locDirPath = fileInfo.path() + QLatin1Char('/') + QLatin1String("l10n");
1363 QFileInfo locDirInfo(locDirPath);
1364 if (!locDirInfo.isDir()) {
1365 return filePath;
1366 }
1367
1368 // Go through possible localized paths by priority of languages,
1369 // return first that exists.
1370 QString fileName = fileInfo.fileName();
1371 for (const QString &lang : std::as_const(t&: s->languages)) {
1372 QString locFilePath = locDirPath + QLatin1Char('/') + lang + QLatin1Char('/') + fileName;
1373 QFileInfo locFileInfo(locFilePath);
1374 if (locFileInfo.isFile() && locFileInfo.isReadable()) {
1375 return locFilePath;
1376 }
1377 }
1378
1379 return filePath;
1380}
1381
1382QString KLocalizedString::removeAcceleratorMarker(const QString &label)
1383{
1384 return ::removeAcceleratorMarker(label);
1385}
1386
1387void KLocalizedString::addDomainLocaleDir(const QByteArray &domain, const QString &path)
1388{
1389 KCatalog::addDomainLocaleDir(domain, path);
1390}
1391
1392KLocalizedString ki18n(const char *text)
1393{
1394 return KLocalizedString(nullptr, nullptr, text, nullptr, false);
1395}
1396
1397KLocalizedString ki18nc(const char *context, const char *text)
1398{
1399 return KLocalizedString(nullptr, context, text, nullptr, false);
1400}
1401
1402KLocalizedString ki18np(const char *singular, const char *plural)
1403{
1404 return KLocalizedString(nullptr, nullptr, singular, plural, false);
1405}
1406
1407KLocalizedString ki18ncp(const char *context, const char *singular, const char *plural)
1408{
1409 return KLocalizedString(nullptr, context, singular, plural, false);
1410}
1411
1412KLocalizedString ki18nd(const char *domain, const char *text)
1413{
1414 return KLocalizedString(domain, nullptr, text, nullptr, false);
1415}
1416
1417KLocalizedString ki18ndc(const char *domain, const char *context, const char *text)
1418{
1419 return KLocalizedString(domain, context, text, nullptr, false);
1420}
1421
1422KLocalizedString ki18ndp(const char *domain, const char *singular, const char *plural)
1423{
1424 return KLocalizedString(domain, nullptr, singular, plural, false);
1425}
1426
1427KLocalizedString ki18ndcp(const char *domain, const char *context, const char *singular, const char *plural)
1428{
1429 return KLocalizedString(domain, context, singular, plural, false);
1430}
1431
1432KLocalizedString kxi18n(const char *text)
1433{
1434 return KLocalizedString(nullptr, nullptr, text, nullptr, true);
1435}
1436
1437KLocalizedString kxi18nc(const char *context, const char *text)
1438{
1439 return KLocalizedString(nullptr, context, text, nullptr, true);
1440}
1441
1442KLocalizedString kxi18np(const char *singular, const char *plural)
1443{
1444 return KLocalizedString(nullptr, nullptr, singular, plural, true);
1445}
1446
1447KLocalizedString kxi18ncp(const char *context, const char *singular, const char *plural)
1448{
1449 return KLocalizedString(nullptr, context, singular, plural, true);
1450}
1451
1452KLocalizedString kxi18nd(const char *domain, const char *text)
1453{
1454 return KLocalizedString(domain, nullptr, text, nullptr, true);
1455}
1456
1457KLocalizedString kxi18ndc(const char *domain, const char *context, const char *text)
1458{
1459 return KLocalizedString(domain, context, text, nullptr, true);
1460}
1461
1462KLocalizedString kxi18ndp(const char *domain, const char *singular, const char *plural)
1463{
1464 return KLocalizedString(domain, nullptr, singular, plural, true);
1465}
1466
1467KLocalizedString kxi18ndcp(const char *domain, const char *context, const char *singular, const char *plural)
1468{
1469 return KLocalizedString(domain, context, singular, plural, true);
1470}
1471
1472#include "klocalizedstring.moc"
1473

source code of ki18n/src/i18n/klocalizedstring.cpp