/* This file is part of the KDE libraries Copyright (C) 2006 Chusslove Illich This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include #include #include #include "kcatalogname_p.h" #include #include #include #include #include #include #include // Truncates string, for output of long messages. static QString shortenMessage (const QString &str) { const int maxlen = 20; if (str.length() <= maxlen) return str; else return str.left(maxlen).append(QLatin1String("...")); } typedef qulonglong pluraln; typedef qlonglong intn; typedef qulonglong uintn; typedef double realn; class KLocalizedStringPrivateStatics; class KLocalizedStringPrivate { friend class KLocalizedString; QStringList args; QList vals; bool numberSet; pluraln number; int numberOrd; QByteArray ctxt; QHash dynctxt; QByteArray msg; QByteArray plural; QString toString (const KLocale *locale, const QString *catalogName) const; QString selectForEnglish () const; QString substituteSimple (const QString &trans, const QChar &plchar = QLatin1Char('%'), bool partial = false) const; QString postFormat (const QString &text, const QString &lang, const QString &ctxt) const; int resolveInterpolation (const QString &trans, int pos, const QString &lang, const QString &ctry, const QString &finalstr, QString &result, bool &fallback) const; QVariant segmentToValue (const QString &arg) const; static void notifyCatalogsUpdated (const QStringList &languages, const QList &catalogs); }; class KLocalizedStringPrivateStatics { public: const QString theFence; const QString startInterp; const QString endInterp; const QChar scriptPlchar; const QChar scriptVachar; const QString scriptDir; QHash scriptModules; QList scriptModulesToLoad; QHash formatters; KLocalizedStringPrivateStatics () : theFence(QLatin1String("|/|")), startInterp(QLatin1String("$[")), endInterp(QLatin1String("]")), scriptPlchar(QLatin1Char('%')), scriptVachar(QLatin1Char('^')), scriptDir(QLatin1String("LC_SCRIPTS")), scriptModules(), scriptModulesToLoad(), formatters() {} ~KLocalizedStringPrivateStatics () { // ktrs is handled by KLibLoader. //delete ktrs; qDeleteAll(formatters); } }; K_GLOBAL_STATIC(KLocalizedStringPrivateStatics, staticsKLSP) KLocalizedString::KLocalizedString () : d(new KLocalizedStringPrivate) { d->numberSet = false; d->number = 0; d->numberOrd = 0; } KLocalizedString::KLocalizedString (const char *ctxt, const char *msg, const char *plural) : d(new KLocalizedStringPrivate) { d->ctxt = ctxt; d->msg = msg; d->plural = plural; d->numberSet = false; d->number = 0; d->numberOrd = 0; } KLocalizedString::KLocalizedString(const KLocalizedString &rhs) : d(new KLocalizedStringPrivate(*rhs.d)) { } KLocalizedString& KLocalizedString::operator= (const KLocalizedString &rhs) { if (&rhs != this) { *d = *rhs.d; } return *this; } KLocalizedString::~KLocalizedString () { delete d; } bool KLocalizedString::isEmpty () const { return d->msg.isEmpty(); } QString KLocalizedString::toString () const { return d->toString(KGlobal::locale(), NULL); } QString KLocalizedString::toString (const QString &catalogName) const { return d->toString(KGlobal::locale(), &catalogName); } QString KLocalizedString::toString (const KLocale *locale) const { return d->toString(locale, NULL); } QString KLocalizedString::toString (const KLocale *locale, const QString &catalogName) const { return d->toString(locale, &catalogName); } QString KLocalizedStringPrivate::toString (const KLocale *locale, const QString *catalogName) const { const KLocalizedStringPrivateStatics *s = staticsKLSP; QMutexLocker lock(kLocaleMutex()); // Assure the message has been supplied. if (msg.isEmpty()) { kDebug(173) << "Trying to convert empty KLocalizedString to QString."; #ifndef NDEBUG return QString::fromLatin1("(I18N_EMPTY_MESSAGE)"); #else return QString(); #endif } // Check whether plural argument has been supplied, if message has plural. if (!plural.isEmpty() && !numberSet) kDebug(173) << QString::fromLatin1("Plural argument to message {%1} not supplied before conversion.") .arg(shortenMessage(QString::fromUtf8(msg))); // Get raw translation. QString rawtrans, lang, ctry; QByteArray catname; if (catalogName != NULL) { catname = catalogName->toUtf8(); } if (locale != NULL) { if (!ctxt.isEmpty() && !plural.isEmpty()) { locale->translateRawFrom(catname, ctxt, msg, plural, number, &lang, &rawtrans); } else if (!plural.isEmpty()) { locale->translateRawFrom(catname, msg, plural, number, &lang, &rawtrans); } else if (!ctxt.isEmpty()) { locale->translateRawFrom(catname, ctxt, msg, &lang, &rawtrans); } else { locale->translateRawFrom(catname, msg, &lang, &rawtrans); } ctry = locale->country(); } else { lang = KLocale::defaultLanguage(); ctry = QLatin1Char('C'); rawtrans = selectForEnglish(); } // Set ordinary translation and possibly scripted translation. QString trans, strans; int cdpos = rawtrans.indexOf(s->theFence); if (cdpos > 0) { // Script fence has been found, strip the scripted from the // ordinary translation. trans = rawtrans.left(cdpos); // Scripted translation. strans = rawtrans.mid(cdpos + s->theFence.length()); } else if (cdpos < 0) { // No script fence, use translation as is. trans = rawtrans; } else // cdpos == 0 { // The msgstr starts with the script fence, no ordinary translation. // This is not allowed, consider message not translated. kDebug(173) << QString::fromLatin1("Scripted message {%1} without ordinary translation, discarded.") .arg(shortenMessage(trans)) ; trans = selectForEnglish(); } // Substitute placeholders in ordinary translation. QString finalstr = substituteSimple(trans); // Post-format ordinary translation. finalstr = postFormat(finalstr, lang, QString::fromLatin1(ctxt)); return finalstr; } QString KLocalizedStringPrivate::selectForEnglish () const { QString trans; if (!plural.isEmpty()) { if (number == 1) { trans = QString::fromUtf8(msg); } else { trans = QString::fromUtf8(plural); } } else { trans = QString::fromUtf8(msg); } return trans; } QString KLocalizedStringPrivate::substituteSimple (const QString &trans, const QChar &plchar, bool partial) const { #ifdef NDEBUG Q_UNUSED(partial); #endif QStringList tsegs; // text segments per placeholder occurrence QList plords; // ordinal numbers per placeholder occurrence #ifndef NDEBUG QVector ords; // indicates which placeholders are present #endif int slen = trans.length(); int spos = 0; int tpos = trans.indexOf(plchar); while (tpos >= 0) { int ctpos = tpos; tpos++; if (tpos == slen) break; if (trans[tpos].digitValue() > 0) // %0 not considered a placeholder { // Get the placeholder ordinal. int plord = 0; while (tpos < slen && trans[tpos].digitValue() >= 0) { plord = 10 * plord + trans[tpos].digitValue(); tpos++; } plord--; // ordinals are zero based #ifndef NDEBUG // Perhaps enlarge storage for indicators. // Note that QVector will initialize new elements to 0, // as they are supposed to be. if (plord >= ords.size()) ords.resize(plord + 1); // Indicate that placeholder with computed ordinal is present. ords[plord] = 1; #endif // Store text segment prior to placeholder and placeholder number. tsegs.append(trans.mid(spos, ctpos - spos)); plords.append(plord); // Position of next text segment. spos = tpos; } tpos = trans.indexOf(plchar, tpos); } // Store last text segment. tsegs.append(trans.mid(spos)); #ifndef NDEBUG // Perhaps enlarge storage for plural-number ordinal. if (!plural.isEmpty() && numberOrd >= ords.size()) ords.resize(numberOrd + 1); // Message might have plural but without plural placeholder, which is an // allowed state. To ease further logic, indicate that plural placeholder // is present anyway if message has plural. if (!plural.isEmpty()) ords[numberOrd] = 1; #endif // Assemble the final string from text segments and arguments. QString finalstr; for (int i = 0; i < plords.size(); i++) { finalstr.append(tsegs.at(i)); if (plords.at(i) >= args.size()) // too little arguments { // put back the placeholder finalstr.append(QLatin1Char('%') + QString::number(plords.at(i) + 1)); #ifndef NDEBUG if (!partial) // spoof the message finalstr.append(QLatin1String("(I18N_ARGUMENT_MISSING)")); #endif } else // just fine finalstr.append(args.at(plords.at(i))); } finalstr.append(tsegs.last()); #ifndef NDEBUG if (!partial) { // Check that there are no gaps in numbering sequence of placeholders. bool gaps = false; for (int i = 0; i < ords.size(); i++) if (!ords.at(i)) { gaps = true; kDebug(173) << QString::fromLatin1("Placeholder %%1 skipped in message {%2}.") .arg(QString::number(i + 1), shortenMessage(trans)); } // If no gaps, check for mismatch between number of unique placeholders and // actually supplied arguments. if (!gaps && ords.size() != args.size()) kDebug(173) << QString::fromLatin1("%1 instead of %2 arguments to message {%3} supplied before conversion.") .arg(args.size()).arg(ords.size()).arg(shortenMessage(trans)); // Some spoofs. if (gaps) finalstr.append(QLatin1String("(I18N_GAPS_IN_PLACEHOLDER_SEQUENCE)")); if (ords.size() < args.size()) finalstr.append(QLatin1String("(I18N_EXCESS_ARGUMENTS_SUPPLIED)")); if (!plural.isEmpty() && !numberSet) finalstr.append(QLatin1String("(I18N_PLURAL_ARGUMENT_MISSING)")); } #endif return finalstr; } QString KLocalizedStringPrivate::postFormat (const QString &text, const QString &lang, const QString &ctxt) const { const KLocalizedStringPrivateStatics *s = staticsKLSP; QMutexLocker lock(kLocaleMutex()); QString finalstr = text; // Transform any semantic markup into visual formatting. if (s->formatters.contains(lang)) { finalstr = s->formatters[lang]->format(finalstr, ctxt); } return finalstr; } int KLocalizedStringPrivate::resolveInterpolation (const QString &strans, int pos, const QString &lang, const QString &ctry, const QString &finalstr, QString &result, bool &fallback) const { // pos is the position of opening character sequence. // Returns the position of first character after closing sequence, // or -1 in case of parsing error. // result is set to result of Transcript evaluation. // fallback is set to true if Transcript evaluation requested so. KLocalizedStringPrivateStatics *s = staticsKLSP; QMutexLocker lock(kLocaleMutex()); result.clear(); fallback = false; // Split interpolation into arguments. QList iargs; int slen = strans.length(); int islen = s->startInterp.length(); int ielen = s->endInterp.length(); int tpos = pos + s->startInterp.length(); while (1) { // Skip whitespace. while (tpos < slen && strans[tpos].isSpace()) { ++tpos; } if (tpos == slen) { kDebug(173) << QString::fromLatin1("Unclosed interpolation {%1} in message {%2}.") .arg(strans.mid(pos, tpos - pos), shortenMessage(strans)); return -1; } if (strans.mid(tpos, ielen) == s->endInterp) { break; // no more arguments } // Parse argument: may be concatenated from free and quoted text, // and sub-interpolations. // Free and quoted segments may contain placeholders, substitute them; // recurse into sub-interpolations. // Free segments may be value references, parse and record for // consideration at the end. // Mind backslash escapes throughout. QStringList segs; QVariant vref; while ( !strans[tpos].isSpace() && strans.mid(tpos, ielen) != s->endInterp) { if (strans[tpos] == QLatin1Char('\'')) { // quoted segment QString seg; ++tpos; // skip opening quote // Find closing quote. while (tpos < slen && strans[tpos] != QLatin1Char('\'')) { if (strans[tpos] == QLatin1Char('\\')) ++tpos; // escape next character seg.append(strans[tpos]); ++tpos; } if (tpos == slen) { kDebug(173) << QString::fromLatin1("Unclosed quote in interpolation {%1} in message {%2}.") .arg(strans.mid(pos, tpos - pos), shortenMessage(strans)); return -1; } // Append to list of segments, resolving placeholders. segs.append(substituteSimple(seg, s->scriptPlchar, true)); ++tpos; // skip closing quote } else if (strans.mid(tpos, islen) == s->startInterp) { // sub-interpolation QString resultLocal; bool fallbackLocal; tpos = resolveInterpolation(strans, tpos, lang, ctry, finalstr, resultLocal, fallbackLocal); if (tpos < 0) { // unrecoverable problem in sub-interpolation // Error reported in the subcall. return tpos; } if (fallbackLocal) { // sub-interpolation requested fallback fallback = true; } segs.append(resultLocal); } else { // free segment QString seg; // Find whitespace, quote, opening or closing sequence. while ( tpos < slen && !strans[tpos].isSpace() && strans[tpos] != QLatin1Char('\'') && strans.mid(tpos, islen) != s->startInterp && strans.mid(tpos, ielen) != s->endInterp) { if (strans[tpos] == QLatin1Char('\\')) ++tpos; // escape next character seg.append(strans[tpos]); ++tpos; } if (tpos == slen) { kDebug(173) << QString::fromLatin1("Non-terminated interpolation {%1} in message {%2}.") .arg(strans.mid(pos, tpos - pos), shortenMessage(strans)); return -1; } // The free segment may look like a value reference; // in that case, record which value it would reference, // and add verbatim to the segment list. // Otherwise, do a normal substitution on the segment. vref = segmentToValue(seg); if (vref.isValid()) { segs.append(seg); } else { segs.append(substituteSimple(seg, s->scriptPlchar, true)); } } } // Append this argument to rest of the arguments. // If the there was a single text segment and it was a proper value // reference, add it instead of the joined segments. // Otherwise, add the joined segments. if (segs.size() == 1 && vref.isValid()) { iargs.append(vref); } else { iargs.append(segs.join(QString())); } } tpos += ielen; // skip to first character after closing sequence // NOTE: Why not substitute placeholders (via substituteSimple) in one // global pass, then handle interpolations in second pass? Because then // there is the danger of substituted text or sub-interpolations producing // quotes and escapes themselves, which would mess up the parsing. // Evaluate interpolation. QString msgctxt = QString::fromUtf8(ctxt); QString msgid = QString::fromUtf8(msg); QString scriptError; // s->scriptModulesToLoad will be cleared during the call. if (!scriptError.isEmpty()) { // problem with evaluation fallback = true; // also signal fallback if (!scriptError.isEmpty()) { kDebug(173) << QString::fromLatin1("Interpolation {%1} in {%2} failed: %3") .arg(strans.mid(pos, tpos - pos), shortenMessage(strans), scriptError); } } return tpos; } QVariant KLocalizedStringPrivate::segmentToValue (const QString &seg) const { const KLocalizedStringPrivateStatics *s = staticsKLSP; QMutexLocker lock(kLocaleMutex()); // Return invalid variant if segment is either not a proper // value reference, or the reference is out of bounds. // Value reference must start with a special character. if (seg.left(1) != s->scriptVachar) { return QVariant(); } // Reference number must start with 1-9. // (If numstr is empty, toInt() will return 0.) QString numstr = seg.mid(1); if (numstr.left(1).toInt() < 1) { return QVariant(); } // Number must be valid and in bounds. bool ok; int index = numstr.toInt(&ok) - 1; if (!ok || index >= vals.size()) { return QVariant(); } // Passed all hoops. return vals.at(index); } static QString wrapNum (const QString &tag, const QString &numstr, int fieldWidth, const QChar &fillChar) { QString optag; if (fieldWidth != 0) { QString fillString = KuitSemantics::escape(fillChar); optag = QString::fromLatin1("<%1 width='%2' fill='%3'>") .arg(tag, QString::number(fieldWidth), fillString); } else { optag = QString::fromLatin1("<%1>").arg(tag); } QString cltag = QString::fromLatin1("").arg(tag); return optag + numstr + cltag; } KLocalizedString KLocalizedString::subs (int a, int fieldWidth, int base, const QChar &fillChar) const { KLocalizedString kls(*this); if (!kls.d->plural.isEmpty() && !kls.d->numberSet) { kls.d->number = static_cast(abs(a)); kls.d->numberSet = true; kls.d->numberOrd = d->args.size(); } kls.d->args.append(wrapNum(QString::fromLatin1(KUIT_NUMINTG), QString::number(a, base), fieldWidth, fillChar)); kls.d->vals.append(static_cast(a)); return kls; } KLocalizedString KLocalizedString::subs (uint a, int fieldWidth, int base, const QChar &fillChar) const { KLocalizedString kls(*this); if (!kls.d->plural.isEmpty() && !kls.d->numberSet) { kls.d->number = static_cast(a); kls.d->numberSet = true; kls.d->numberOrd = d->args.size(); } kls.d->args.append(wrapNum(QString::fromLatin1(KUIT_NUMINTG), QString::number(a, base), fieldWidth, fillChar)); kls.d->vals.append(static_cast(a)); return kls; } KLocalizedString KLocalizedString::subs (long a, int fieldWidth, int base, const QChar &fillChar) const { KLocalizedString kls(*this); if (!kls.d->plural.isEmpty() && !kls.d->numberSet) { kls.d->number = static_cast(abs(a)); kls.d->numberSet = true; kls.d->numberOrd = d->args.size(); } kls.d->args.append(wrapNum(QString::fromLatin1(KUIT_NUMINTG), QString::number(a, base), fieldWidth, fillChar)); kls.d->vals.append(static_cast(a)); return kls; } KLocalizedString KLocalizedString::subs (ulong a, int fieldWidth, int base, const QChar &fillChar) const { KLocalizedString kls(*this); if (!kls.d->plural.isEmpty() && !kls.d->numberSet) { kls.d->number = static_cast(a); kls.d->numberSet = true; kls.d->numberOrd = d->args.size(); } kls.d->args.append(wrapNum(QString::fromLatin1(KUIT_NUMINTG), QString::number(a, base), fieldWidth, fillChar)); kls.d->vals.append(static_cast(a)); return kls; } KLocalizedString KLocalizedString::subs (qlonglong a, int fieldWidth, int base, const QChar &fillChar) const { KLocalizedString kls(*this); if (!kls.d->plural.isEmpty() && !kls.d->numberSet) { kls.d->number = static_cast(qAbs(a)); kls.d->numberSet = true; kls.d->numberOrd = d->args.size(); } kls.d->args.append(wrapNum(QString::fromLatin1(KUIT_NUMINTG), QString::number(a, base), fieldWidth, fillChar)); kls.d->vals.append(static_cast(a)); return kls; } KLocalizedString KLocalizedString::subs (qulonglong a, int fieldWidth, int base, const QChar &fillChar) const { KLocalizedString kls(*this); if (!kls.d->plural.isEmpty() && !kls.d->numberSet) { kls.d->number = static_cast(a); kls.d->numberSet = true; kls.d->numberOrd = d->args.size(); } kls.d->args.append(wrapNum(QString::fromLatin1(KUIT_NUMINTG), QString::number(a, base), fieldWidth, fillChar)); kls.d->vals.append(static_cast(a)); return kls; } KLocalizedString KLocalizedString::subs (double a, int fieldWidth, char format, int precision, const QChar &fillChar) const { KLocalizedString kls(*this); kls.d->args.append(wrapNum(QString::fromLatin1(KUIT_NUMREAL), QString::number(a, format, precision), fieldWidth, fillChar)); kls.d->vals.append(static_cast(a)); return kls; } KLocalizedString KLocalizedString::subs (QChar a, int fieldWidth, const QChar &fillChar) const { KLocalizedString kls(*this); kls.d->args.append(QString::fromLatin1("%1").arg(a, fieldWidth, fillChar)); kls.d->vals.append(QString(a)); return kls; } KLocalizedString KLocalizedString::subs (const QString &a, int fieldWidth, const QChar &fillChar) const { KLocalizedString kls(*this); // if (!Qt::mightBeRichText(a)) { ... // Do not try to auto-escape non-rich-text alike arguments; // breaks compatibility with 4.0. Perhaps for KDE 5? // Perhaps bad idea alltogether (too much surprise)? kls.d->args.append(QString::fromLatin1("%1").arg(a, fieldWidth, fillChar)); kls.d->vals.append(a); return kls; } KLocalizedString KLocalizedString::inContext (const QString &key, const QString &text) const { KLocalizedString kls(*this); kls.d->dynctxt[key] = text; return kls; } KLocalizedString ki18n (const char* msg) { return KLocalizedString(NULL, msg, NULL); } KLocalizedString ki18nc (const char* ctxt, const char *msg) { return KLocalizedString(ctxt, msg, NULL); } KLocalizedString ki18np (const char* singular, const char* plural) { return KLocalizedString(NULL, singular, plural); } KLocalizedString ki18ncp (const char* ctxt, const char* singular, const char* plural) { return KLocalizedString(ctxt, singular, plural); } void KLocalizedString::notifyCatalogsUpdated (const QStringList &languages, const QList &catalogs) { KLocalizedStringPrivate::notifyCatalogsUpdated(languages, catalogs); } void KLocalizedStringPrivate::notifyCatalogsUpdated (const QStringList &languages, const QList &catalogs) { if (staticsKLSP.isDestroyed()) { return; } KLocalizedStringPrivateStatics *s = staticsKLSP; // Very important: do not the mutex here. //QMutexLocker lock(kLocaleMutex()); // Find script modules for all included language/catalogs that have them, // and remember their paths. // A more specific module may reference the calls from a less specific, // and the catalog list is ordered from more to less specific. Therefore, // work on reversed list of catalogs. foreach (const QString &lang, languages) { for (int i = catalogs.size() - 1; i >= 0; --i) { const KCatalogName &cat(catalogs[i]); // Assemble module's relative path. QString modrpath = lang + QLatin1Char('/') + s->scriptDir + QLatin1Char('/') + cat.name + QLatin1Char('/') + cat.name + QLatin1String(".js"); // Try to find this module. QString modapath = KStandardDirs::locate("locale", modrpath); // If the module exists and hasn't been already included. if ( !modapath.isEmpty() && !s->scriptModules[lang].contains(cat.name)) { // Indicate that the module has been considered. s->scriptModules[lang].append(cat.name); // Store the absolute path and language of the module, // to load on next script evaluation. QStringList mod; mod.append(modapath); mod.append(lang); s->scriptModulesToLoad.append(mod); } } } // Create visual formatters for each new language. foreach (const QString &lang, languages) { if (!s->formatters.contains(lang)) { s->formatters.insert(lang, new KuitSemantics(lang)); } } }