diff --git a/autotests/klocalizedstringtest.cpp b/autotests/klocalizedstringtest.cpp index ee80209..42fb972 100644 --- a/autotests/klocalizedstringtest.cpp +++ b/autotests/klocalizedstringtest.cpp @@ -1,550 +1,587 @@ // krazy:excludeall=i18ncheckarg /* 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. */ // Tests explicitly use their own test catalogs. #undef TRANSLATION_DOMAIN #include "klocalizedstringtest.h" #include "klocalizedtranslator.h" #include #include #include #include #include #include #include void KLocalizedStringTest::initTestCase() { KLocalizedString::setApplicationDomain("ki18n-test"); m_hasFrench = true; + m_hasCatalan = true; + + setlocale(LC_ALL, "ca_ES.utf8"); + if (setlocale(LC_ALL, nullptr) != QByteArray("ca_ES.utf8")) { + qDebug() << "Failed to set locale to ca_ES.utf8."; + m_hasCatalan = false; + } + if (m_hasFrench) { setlocale(LC_ALL, "fr_FR.utf8"); if (setlocale(LC_ALL, nullptr) != QByteArray("fr_FR.utf8")) { qDebug() << "Failed to set locale to fr_FR.utf8."; m_hasFrench = false; } } if (m_hasFrench) { if (!m_tempDir.isValid()) { qDebug() << "Failed to create temporary directory for test data."; m_hasFrench = false; } } QDir dataDir(m_tempDir.path()); if (m_hasFrench) { - m_hasFrench = compileCatalogs({QFINDTESTDATA("po/fr/ki18n-test.po"), QFINDTESTDATA("po/fr/ki18n-test-qt.po")}, dataDir); + m_hasFrench = compileCatalogs({QFINDTESTDATA("po/fr/ki18n-test.po"), QFINDTESTDATA("po/fr/ki18n-test-qt.po")}, dataDir, "fr"); + } + if (m_hasCatalan) { + m_hasCatalan = compileCatalogs({QFINDTESTDATA("po/ca/ki18n-test.po")}, dataDir, "ca"); } if (m_hasFrench) { qputenv("XDG_DATA_DIRS", qgetenv("XDG_DATA_DIRS") + ":" + QFile::encodeName(dataDir.path())); // bind... dataDir.path() QStringList languages; languages.append("fr"); KLocalizedString::setLanguages(languages); } #if 0 // until locale system is ready if (m_hasFrench) { KLocale::global()->setLanguage(QStringList() << "fr" << "en_US"); } KLocale::global()->setThousandsSeparator(QLatin1String(",")); KLocale::global()->setDecimalSymbol(QLatin1String(".")); #endif } -bool KLocalizedStringTest::compileCatalogs(const QStringList &testPoPaths, const QDir &dataDir) +bool KLocalizedStringTest::compileCatalogs(const QStringList &testPoPaths, const QDir &dataDir, const QString &lang) { - if (!dataDir.mkpath("locale/fr/LC_MESSAGES")) { + const QString lcMessages = QString("locale/%1/LC_MESSAGES").arg(lang); + if (!dataDir.mkpath(lcMessages)) { qDebug() << "Failed to create locale subdirectory " "inside temporary directory."; return false; } QString msgfmt = QStandardPaths::findExecutable(QLatin1String("msgfmt")); if (msgfmt.isEmpty()) { qDebug() << "msgfmt(1) not found in path."; return false; } foreach (const QString &testPoPath, testPoPaths) { int pos_1 = testPoPath.lastIndexOf(QLatin1Char('/')); int pos_2 = testPoPath.lastIndexOf(QLatin1Char('.')); QString domain = testPoPath.mid(pos_1 + 1, pos_2 - pos_1 - 1); QString testMoPath; - testMoPath = QString::fromLatin1("%1/locale/fr/LC_MESSAGES/%2.mo") - .arg(dataDir.path(), domain); + testMoPath = QString::fromLatin1("%1/%3/%2.mo") + .arg(dataDir.path(), domain, lcMessages); QProcess process; QStringList arguments; arguments << testPoPath << QLatin1String("-o") << testMoPath; process.start(msgfmt, arguments); process.waitForFinished(10000); if (process.exitCode() != 0) { qDebug() << QString::fromLatin1("msgfmt(1) could not compile %1.") .arg(testPoPath); return false; } } return true; } void KLocalizedStringTest::correctSubs() { // Warm up. QCOMPARE(i18n("Daisies, daisies"), QString("Daisies, daisies")); // Placeholder in the middle. QCOMPARE(i18n("Fault in %1 unit", QString("AE35")), QString("Fault in AE35 unit")); // Placeholder at the start. QCOMPARE(i18n("%1, Tycho Magnetic Anomaly 1", QString("TMA-1")), QString("TMA-1, Tycho Magnetic Anomaly 1")); // Placeholder at the end. QCOMPARE(i18n("...odd things happening at %1", QString("Clavius")), QString("...odd things happening at Clavius")); QCOMPARE(i18n("Group %1", 1), QString("Group 1")); // Two placeholders. QCOMPARE(i18n("%1 and %2", QString("Bowman"), QString("Poole")), QString("Bowman and Poole")); // Two placeholders in inverted order. QCOMPARE(i18n("%2 and %1", QString("Poole"), QString("Bowman")), QString("Bowman and Poole")); // % which is not of placeholder. QCOMPARE(i18n("It's going to go %1% failure in 72 hours.", 100), QString("It's going to go 100% failure in 72 hours.")); // Usual plural. QCOMPARE(i18np("%1 pod", "%1 pods", 1), QString("1 pod")); QCOMPARE(i18np("%1 pod", "%1 pods", 10), QString("10 pods")); // No plural-number in singular. QCOMPARE(i18np("A pod", "%1 pods", 1), QString("A pod")); QCOMPARE(i18np("A pod", "%1 pods", 10), QString("10 pods")); // No plural-number in singular or plural. QCOMPARE(i18np("A pod", "Few pods", 1), QString("A pod")); QCOMPARE(i18np("A pod", "Few pods", 10), QString("Few pods")); // First of two arguments as plural-number. QCOMPARE(i18np("A pod left on %2", "%1 pods left on %2", 1, QString("Discovery")), QString("A pod left on Discovery")); QCOMPARE(i18np("A pod left on %2", "%1 pods left on %2", 2, QString("Discovery")), QString("2 pods left on Discovery")); // Second of two arguments as plural-number. QCOMPARE(i18np("%1 has a pod left", "%1 has %2 pods left", QString("Discovery"), 1), QString("Discovery has a pod left")); QCOMPARE(i18np("%1 has a pod left", "%1 has %2 pods left", QString("Discovery"), 2), QString("Discovery has 2 pods left")); // No plural-number in singular or plural, but another argument present. QCOMPARE(i18np("A pod left on %2", "Some pods left on %2", 1, QString("Discovery")), QString("A pod left on Discovery")); QCOMPARE(i18np("A pod left on %2", "Some pods left on %2", 2, QString("Discovery")), QString("Some pods left on Discovery")); // Visual formatting. // FIXME: Needs much more tests. QCOMPARE(xi18n("E = mc^2"), QString("E = mc^2")); QCOMPARE(xi18n("E < mc^2"), QString("E < mc^2")); QCOMPARE(xi18n("E ? mc^2"), QString("E ? *mc^2*")); QCOMPARE(xi18n("E < mc^2"), QString("E < *mc^2*")); QCOMPARE(xi18nc("@label", "E < mc^2"), QString("E < *mc^2*")); QCOMPARE(xi18nc("@info", "E < mc^2"), QString("E < mc^2")); QCOMPARE(xi18n("E = mc^2"), QString("E = mc^2")); QCOMPARE(xi18n("E = mc^2"), QString("E = mc^2")); // Number formatting. QCOMPARE(ki18n("%1").subs(42).toString(), QString("42")); QCOMPARE(ki18n("%1").subs(42, 5).toString(), QString(" 42")); QCOMPARE(ki18n("%1").subs(42, -5, 10, QChar('_')).toString(), QString("42___")); QCOMPARE(ki18n("%1").subs(4.2, 5, 'f', 2).toString(), QString(" 4.20")); } void KLocalizedStringTest::wrongSubs() { #ifndef NDEBUG // Too many arguments. QVERIFY(i18n("Europa", 1) != QString("Europa")); // Too few arguments. QVERIFY(i18n("%1, %2 and %3", QString("Hunter"), QString("Kimball")) != QString("Hunter, Kimball and %3")); // Gaps in placheholder numbering. QVERIFY(ki18n("Beyond the %2").subs("infinity").toString() != QString("Beyond the infinity")); // Plural argument not supplied. QVERIFY(ki18np("1 pod", "%1 pods").toString() != QString("1 pod")); QVERIFY(ki18np("1 pod", "%1 pods").toString() != QString("%1 pods")); #endif } void KLocalizedStringTest::semanticTags() { KLocalizedString::setLanguages({"en"}); // QCOMPARE(xi18nc("@action:inmenu", "Open with %1", "Okteta"), QString("Open with Okteta")); QCOMPARE(xi18nc("@info", "Open with %1", "Okteta"), QString("Open with Okteta")); // QCOMPARE(xi18nc("@info:whatsthis", "You can try the following snippet:" "\\begin{equation}\n" " C_{x_i} = \\frac{C_z^2}{e \\pi \\lambda}\n" "\\end{equation}" ""), QString("You can try the following snippet:\n\n
"
                     "\\begin{equation}\n"
                     "  C_{x_i} = \\frac{C_z^2}{e \\pi \\lambda}\n"
                     "\\end{equation}"
                     "
")); // QCOMPARE(xi18nc("@info", "This will call %1 internally.", "true"), QString("This will call true internally.")); QCOMPARE(xi18nc("@info", "Consult man entry for %1", "true", 1), QString("Consult man entry for true(1)")); // QCOMPARE(xi18nc("@info", "Send bug reports to %1.", "konqi@kde.org"), QString("Send bug reports to <konqi@kde.org>.")); QCOMPARE(xi18nc("@info", "Send praises to %2.", "konqi@kde.org", "Konqi"), QString("Send praises to Konqi.")); // QCOMPARE(xi18nc("@info:progress", "Checking feedback circuits..."), QString("Checking *feedback* circuits...")); QCOMPARE(xi18nc("@info:progress", "Checking feedback circuits..."), QString("Checking **feedback** circuits...")); // QCOMPARE(xi18nc("@info", "Assure that your PATH is properly set."), QString("Assure that your $PATH is properly set.")); // QCOMPARE(xi18nc("@info", "Cannot read %1.", "data.dat"), QString("Cannot read data.dat.")); // TODO: is nested really wanted? #ifndef Q_OS_WIN QString homeFooRc("$HOME/.foorc does not exist."); #else //TODO $HOME -> %HOME% ? QString homeFooRc("$HOME\\.foorc does not exist."); #endif QCOMPARE(xi18nc("@info", "HOME/.foorc does not exist."), homeFooRc); // QCOMPARE(xi18nc("@info:tooltip", "Execute svn merge on selected revisions."), QString("Execute svn merge on selected revisions.")); // QCOMPARE(xi18nc("@info:whatsthis", "If you make a mistake, click Reset to start again."), QString("If you make a mistake, click Reset to start again.")); QCOMPARE(xi18nc("@info:whatsthis", "The line colors can be changed under Settings->Visuals."), QString("The line colors can be changed under Settings->Visuals.")); // QCOMPARE(xi18nc("@info:tooltip", "Go to %1 website.", "http://kde.org/"), QString("Go to http://kde.org/ website.")); QCOMPARE(xi18nc("@info:tooltip", "Go to %2.", "http://kde.org/", "the KDE website"), QString("Go to the KDE website.")); // QCOMPARE(xi18nc("@info", "The fortune cookie says: %1", "Nothing"), QString("The fortune cookie says: Nothing")); // #ifndef Q_OS_WIN QString deleteEtcPasswd("Do you really want to delete:
/etc/passwd"); #else QString deleteEtcPasswd("Do you really want to delete:
\\etc\\passwd"); #endif QCOMPARE(xi18nc("@info", "Do you really want to delete:%1", "/etc/passwd"), deleteEtcPasswd); //check within filename doesn't break (Windows path separators) #ifndef Q_OS_WIN QString filenameWithNewline("/filename/with
/newline
"); #else QString filenameWithNewline("\\filename\\with
\\newline
"); #endif QCOMPARE(xi18nc("@info", "/filename/with/newline"), filenameWithNewline); // QEXPECT_FAIL("", "what happened to ? TODO.", Continue); QCOMPARE(xi18nc("@info:progress", "Connecting to %1...", 22), QString("Connecting to 22")); QCOMPARE(xi18nc("@info", "Replace name with your name."), QString("Replace <name> with your name.")); QCOMPARE(xi18nc("@item:inlistbox", "All images"), QString("")); // QCOMPARE(xi18nc("@info", "Apply color scheme %1?", "XXX"), QString("Apply color scheme “XXX”?")); QCOMPARE(xi18nc("@info:whatsthis", "Cycle through layouts using Alt+Space."), QString("Cycle through layouts using Alt+Space.")); // QCOMPARE(xi18nc("@info", "Probably the best known of all duck species is the Mallard. " "It breeds throughout the temperate areas around the world. " "Most domestic ducks are derived from Mallard."), QString("Probably the best known of all duck species is the Mallard. " "It breeds throughout the temperate areas around the world. " "Note: Most domestic ducks are derived from Mallard.")); QCOMPARE(xi18nc("@info", "Most domestic ducks are derived from Mallard."), QString("Trivia: Most domestic ducks are derived from Mallard.")); // QCOMPARE(xi18nc("@info", "Really delete this key?" "This cannot be undone."), QString("Really delete this key?" "Warning: This cannot be undone.")); QCOMPARE(xi18nc("@info", "This cannot be undone."), QString("Danger: This cannot be undone.")); } void KLocalizedStringTest::removeAcceleratorMarker() { // No accelerator marker. QCOMPARE(KLocalizedString::removeAcceleratorMarker(QString()), QString()); QCOMPARE(KLocalizedString::removeAcceleratorMarker("Foo bar"), QString("Foo bar")); // Run of the mill. QCOMPARE(KLocalizedString::removeAcceleratorMarker("&Foo bar"), QString("Foo bar")); QCOMPARE(KLocalizedString::removeAcceleratorMarker("Foo &bar"), QString("Foo bar")); QCOMPARE(KLocalizedString::removeAcceleratorMarker("Foo b&ar"), QString("Foo bar")); // - presence of escaped ampersands QCOMPARE(KLocalizedString::removeAcceleratorMarker("Foo && Bar"), QString("Foo & Bar")); QCOMPARE(KLocalizedString::removeAcceleratorMarker("Foo && &Bar"), QString("Foo & Bar")); QCOMPARE(KLocalizedString::removeAcceleratorMarker("&Foo && Bar"), QString("Foo & Bar")); // CJK-style markers. QCOMPARE(KLocalizedString::removeAcceleratorMarker("Foo bar (&F)"), QString("Foo bar")); QCOMPARE(KLocalizedString::removeAcceleratorMarker("(&F) Foo bar"), QString("Foo bar")); // - interpunction after/before parenthesis still qualifies CJK marker QCOMPARE(KLocalizedString::removeAcceleratorMarker("Foo bar (&F):"), QString("Foo bar:")); QCOMPARE(KLocalizedString::removeAcceleratorMarker("Foo bar (&F)..."), QString("Foo bar...")); QCOMPARE(KLocalizedString::removeAcceleratorMarker("...(&F) foo bar"), QString("...foo bar")); // - alphanumerics around parenthesis disqualify CJK marker QCOMPARE(KLocalizedString::removeAcceleratorMarker("Foo (&F) bar"), QString("Foo (F) bar")); // - something removed raw ampersands, leaving dangling reduced CJK markers. // Remove reduced markers only if CJK characters are found in the string. QCOMPARE(KLocalizedString::removeAcceleratorMarker(QString::fromUtf8("Foo bar (F)")), QString::fromUtf8("Foo bar (F)")); QCOMPARE(KLocalizedString::removeAcceleratorMarker(QString::fromUtf8("印刷(P)...")), QString::fromUtf8("印刷...")); // Shady cases, where ampersand is obviously not a marker // and should have been escaped, but it was not. QCOMPARE(KLocalizedString::removeAcceleratorMarker("&"), QString("&")); QCOMPARE(KLocalizedString::removeAcceleratorMarker("Foo bar &"), QString("Foo bar &")); QCOMPARE(KLocalizedString::removeAcceleratorMarker("Foo & Bar"), QString("Foo & Bar")); } void KLocalizedStringTest::miscMethods() { KLocalizedString k; QVERIFY(k.isEmpty()); if (m_hasFrench) { QSet availableLanguages; availableLanguages.insert("fr"); availableLanguages.insert("en_US"); + if (m_hasCatalan) { + availableLanguages.insert("ca"); + } QCOMPARE(KLocalizedString::availableApplicationTranslations(), availableLanguages); } } // Same as translateToFrench, but using libintl directly (bindtextdomain+dgettext). // Useful for debugging. This changes global state, though, so it's skipped by default. void KLocalizedStringTest::translateToFrenchLowlevel() { if (!m_hasFrench) { QSKIP("French test files not usable."); } QSKIP("Skipped by default to avoid changing global state."); // fr_FR locale was set by initTestCase already. if (QFile::exists("/usr/share/locale/fr/LC_MESSAGES/ki18n-test.mo")) { bindtextdomain("ki18n-test", "/usr/share/locale"); QCOMPARE(QString::fromUtf8(dgettext("ki18n-test", "Loadable modules")), QString::fromUtf8("Modules chargeables")); } } void KLocalizedStringTest::translateToFrench() { if (!m_hasFrench) { QSKIP("French test files not usable."); } QCOMPARE(i18n("Loadable modules"), QString::fromUtf8("Modules chargeables")); QCOMPARE(i18n("Job"), QString::fromUtf8("Tâche")); } void KLocalizedStringTest::translateQt() { KLocalizedString::insertQtDomain("ki18n-test-qt"); QString result = KLocalizedString::translateQt("QPrintPreviewDialog", "Landscape", nullptr, 0); // When we use the default language, translateQt returns an empty string. QString expected = m_hasFrench ? QString("Paysage") : QString(); QCOMPARE(result, expected); #if 0 // KLocalizedString no longer does anything with QTranslator, this needed? result = QCoreApplication::translate("QPrintPreviewDialog", "Landscape"); QString expected2 = m_hasFrench ? QString("Paysage") : QString("Landscape"); QCOMPARE(result, expected2); #endif #if 0 // translateRaw no longer public, this needed? // So let's use translateRaw instead for the threaded test QString lang; KLocale::global()->translateRaw("Landscape", &lang, &result); QCOMPARE(lang, m_hasFrench ? QString("fr") : QString("en_US")); QCOMPARE(result, m_hasFrench ? QString("Paysage") : QString("Landscape")); #endif KLocalizedString::removeQtDomain("ki18n-test-qt"); } void KLocalizedStringTest::testLocalizedTranslator() { if (!m_hasFrench) { QSKIP("French test files not usable."); } QScopedPointer translator(new KLocalizedTranslator()); QCoreApplication *app = QCoreApplication::instance(); app->installTranslator(translator.data()); // no translation domain and no context QCOMPARE(app->translate("foo", "Job"), QStringLiteral("Job")); // adding the translation domain still lacks the context translator->setTranslationDomain(QStringLiteral("ki18n-test")); QCOMPARE(app->translate("foo", "Job"), QStringLiteral("Job")); translator->addContextToMonitor(QStringLiteral("foo")); // now it should translate QCOMPARE(app->translate("foo", "Job"), QStringLiteral("Tâche")); // other context shouldn't translate QCOMPARE(app->translate("bar", "Job"), QStringLiteral("Job")); // with a mismatching disambiguation it shouldn't translate QCOMPARE(app->translate("foo", "Job", "bar"), QStringLiteral("Job")); } void KLocalizedStringTest::addCustomDomainPath() { if (!m_hasFrench) { QSKIP("French test files not usable."); } QTemporaryDir dir; - compileCatalogs({QFINDTESTDATA("po/fr/ki18n-test2.po")}, dir.path()); + compileCatalogs({QFINDTESTDATA("po/fr/ki18n-test2.po")}, dir.path(), "fr"); KLocalizedString::addDomainLocaleDir("ki18n-test2", dir.path() + "/locale"); QSet expectedAvailableTranslations({"en_US", "fr"}); QCOMPARE(KLocalizedString::availableDomainTranslations("ki18n-test2"), expectedAvailableTranslations); QCOMPARE(i18nd("ki18n-test2", "Cheese"), QString::fromUtf8("Fromage")); } +void KLocalizedStringTest::multipleLanguages() +{ + if (!m_hasFrench || !m_hasCatalan) { + QSKIP("French or Catalan test files not usable."); + } + KLocalizedString::setLanguages({"ca"}); + QCOMPARE(i18n("Job"), QString::fromUtf8("Job")); // This is not the actual catalan translation but who cares + KLocalizedString::setLanguages({"fr"}); + QCOMPARE(i18n("Job"), QString::fromUtf8("Tâche")); + KLocalizedString::setLanguages({"ca", "fr"}); + QCOMPARE(i18n("Job"), QString::fromUtf8("Job")); // This is not the actual catalan translation but who cares + + + KLocalizedString::setLanguages({"ca"}); + QCOMPARE(i18n("Loadable modules"), QString::fromUtf8("Loadable modules")); // The po doesn't have a translation so we get the English text + KLocalizedString::setLanguages({"fr"}); + QCOMPARE(i18n("Loadable modules"), QString::fromUtf8("Modules chargeables")); + KLocalizedString::setLanguages({"ca", "fr"}); + QCOMPARE(i18n("Loadable modules"), QString::fromUtf8("Modules chargeables")); // The Catalan po doesn't have a translation so we get the English text +} + + #include #include #include void KLocalizedStringTest::testThreads() { QThreadPool::globalInstance()->setMaxThreadCount(10); QFutureSynchronizer sync; sync.addFuture(QtConcurrent::run(this, &KLocalizedStringTest::correctSubs)); sync.addFuture(QtConcurrent::run(this, &KLocalizedStringTest::correctSubs)); sync.addFuture(QtConcurrent::run(this, &KLocalizedStringTest::correctSubs)); sync.addFuture(QtConcurrent::run(this, &KLocalizedStringTest::translateQt)); sync.addFuture(QtConcurrent::run(this, &KLocalizedStringTest::translateQt)); sync.addFuture(QtConcurrent::run(this, &KLocalizedStringTest::translateToFrench)); sync.waitForFinished(); QThreadPool::globalInstance()->setMaxThreadCount(1); // delete those threads } QTEST_MAIN(KLocalizedStringTest) diff --git a/autotests/klocalizedstringtest.h b/autotests/klocalizedstringtest.h index df7eecb..2bf63f8 100644 --- a/autotests/klocalizedstringtest.h +++ b/autotests/klocalizedstringtest.h @@ -1,50 +1,52 @@ /* This file is part of the KDE libraries Copyright (c) 2005 Thomas Braxton This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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. */ #ifndef KLOCALIZEDSTRINGTEST_H #define KLOCALIZEDSTRINGTEST_H #include #include class KLocalizedStringTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void correctSubs(); void wrongSubs(); void removeAcceleratorMarker(); void miscMethods(); void translateToFrenchLowlevel(); void translateToFrench(); void translateQt(); void addCustomDomainPath(); void testThreads(); void testLocalizedTranslator(); void semanticTags(); + void multipleLanguages(); private: bool m_hasFrench; + bool m_hasCatalan; QTemporaryDir m_tempDir; - bool compileCatalogs(const QStringList &catalogs, const QDir &dataDir); + bool compileCatalogs(const QStringList &catalogs, const QDir &dataDir, const QString &language); }; #endif // KLOCALIZEDSTRINGTEST_H diff --git a/autotests/po/ca/ki18n-test.po b/autotests/po/ca/ki18n-test.po new file mode 100644 index 0000000..3cb7535 --- /dev/null +++ b/autotests/po/ca/ki18n-test.po @@ -0,0 +1,21 @@ +msgid "" +msgstr "" +"Project-Id-Version: ki18n-test\n" +"POT-Creation-Date: 2012-01-01 00:00+0100\n" +"PO-Revision-Date: 2012-01-01 12:00+0100\n" +"Last-Translator: Catalunya \n" +"Language-Team: Catalan \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +# This is not a real Catalan translation, but +# who cares, it makes the test easier +msgid "Job" +msgstr "Job" + +# Untranslated on purpose +msgid "Loadable modules" +msgstr "" diff --git a/src/kcatalog.cpp b/src/kcatalog.cpp index adb6ba0..cceaae8 100644 --- a/src/kcatalog.cpp +++ b/src/kcatalog.cpp @@ -1,286 +1,292 @@ /* This file is part of the KDE libraries Copyright (c) 2001 Hans Petter Bieker Copyright (c) 2012, 2013 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 "gettext.h" #include "config.h" #include #include #include #include #include #include #include #include #include #include // not defined on win32 :( #ifdef _WIN32 #ifndef LC_MESSAGES #define LC_MESSAGES 42 #endif #endif #if defined(HAVE_NL_MSG_CAT_CNTR) extern "C" int Q_DECL_IMPORT _nl_msg_cat_cntr; #endif static char *langenv = nullptr; static const int langenvMaxlen = 42; // = "LANGUAGE=" + 32 chars for language code + terminating zero class KCatalogStaticData { public: KCatalogStaticData() {} QHash customCatalogDirs; QMutex mutex; }; Q_GLOBAL_STATIC(KCatalogStaticData, catalogStaticData) class KCatalogPrivate { public: KCatalogPrivate(); QByteArray domain; QByteArray language; QByteArray localeDir; QByteArray systemLanguage; bool bindDone; static QByteArray currentLanguage; void setupGettextEnv(); void resetSystemLanguage(); }; KCatalogPrivate::KCatalogPrivate() : bindDone(false) {} QByteArray KCatalogPrivate::currentLanguage; KCatalog::KCatalog(const QByteArray &domain, const QString &language_) : d(new KCatalogPrivate) { d->domain = domain; d->language = QFile::encodeName(language_); d->localeDir = QFile::encodeName(catalogLocaleDir(domain, language_)); if (!d->localeDir.isEmpty()) { // Always get translations in UTF-8, regardless of user's environment. bind_textdomain_codeset(d->domain, "UTF-8"); // Invalidate current language, to trigger binding at next translate call. KCatalogPrivate::currentLanguage.clear(); if (!langenv) { // Call putenv only here, to initialize LANGUAGE variable. // Later only change langenv to what is currently needed. langenv = new char[langenvMaxlen]; QByteArray baselang = qgetenv("LANGUAGE"); qsnprintf(langenv, langenvMaxlen, "LANGUAGE=%s", baselang.constData()); putenv(langenv); } } } KCatalog::~KCatalog() { delete d; } QString KCatalog::catalogLocaleDir(const QByteArray &domain, const QString &language) { QString relpath = QStringLiteral("%1/LC_MESSAGES/%2.mo") .arg(language, QFile::decodeName(domain)); { QMutexLocker lock(&catalogStaticData->mutex); const QString customLocaleDir = catalogStaticData->customCatalogDirs.value(domain); if (!customLocaleDir.isEmpty() && QFileInfo::exists(customLocaleDir + QLatin1Char('/') + relpath)) { return customLocaleDir; } } QString file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("locale/") + relpath); QString localeDir; if (file.isEmpty()) { localeDir = QString(); } else { // Path of the locale/ directory must be returned. localeDir = QFileInfo(file.left(file.size() - relpath.size())).absolutePath(); } return localeDir; } QSet KCatalog::availableCatalogLanguages(const QByteArray &domain_) { QString domain = QFile::decodeName(domain_); QStringList localeDirPaths = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("locale"), QStandardPaths::LocateDirectory); { QMutexLocker lock(&catalogStaticData->mutex); auto it = catalogStaticData->customCatalogDirs.constFind(domain_); if (it != catalogStaticData->customCatalogDirs.constEnd()) { localeDirPaths.prepend(*it); } } QSet availableLanguages; foreach (const QString &localDirPath, localeDirPaths) { QDir localeDir(localDirPath); QStringList languages = localeDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot); foreach (const QString &language, languages) { QString relPath = QStringLiteral("%1/LC_MESSAGES/%2.mo") .arg(language, domain); if (localeDir.exists(relPath)) { availableLanguages.insert(language); } } } return availableLanguages; } void KCatalogPrivate::setupGettextEnv() { // Point Gettext to current language, recording system value for recovery. systemLanguage = qgetenv("LANGUAGE"); if (systemLanguage != language) { // putenv has been called in the constructor, // it is enough to change the string set there. qsnprintf(langenv, langenvMaxlen, "LANGUAGE=%s", language.constData()); } // Rebind text domain if language actually changed from the last time, // as locale directories may differ for different languages of same catalog. if (language != currentLanguage || !bindDone) { currentLanguage = language; bindDone = true; //qDebug() << "bindtextdomain" << domain << localeDir; bindtextdomain(domain, localeDir); #if defined(HAVE_NL_MSG_CAT_CNTR) // Magic to make sure GNU Gettext doesn't use stale cached translation // from previous language. ++_nl_msg_cat_cntr; #endif } } void KCatalogPrivate::resetSystemLanguage() { if (language != systemLanguage) { qsnprintf(langenv, langenvMaxlen, "LANGUAGE=%s", systemLanguage.constData()); } } QString KCatalog::translate(const QByteArray &msgid) const { if (!d->localeDir.isEmpty()) { QMutexLocker locker(&catalogStaticData()->mutex); d->setupGettextEnv(); - const char *msgstr = dgettext(d->domain.constData(), msgid.constData()); + const char *msgid_char = msgid.constData(); + const char *msgstr = dgettext(d->domain.constData(), msgid_char); d->resetSystemLanguage(); - return msgstr != msgid + return msgstr != msgid_char // Yes we want pointer comparison ? QString::fromUtf8(msgstr) : QString(); } else { return QString(); } } QString KCatalog::translate(const QByteArray &msgctxt, const QByteArray &msgid) const { if (!d->localeDir.isEmpty()) { QMutexLocker locker(&catalogStaticData()->mutex); d->setupGettextEnv(); - const char *msgstr = dpgettext_expr(d->domain.constData(), msgctxt.constData(), msgid.constData()); + const char *msgid_char = msgid.constData(); + const char *msgstr = dpgettext_expr(d->domain.constData(), msgctxt.constData(), msgid_char); d->resetSystemLanguage(); - return msgstr != msgid + return msgstr != msgid_char // Yes we want pointer comparison ? QString::fromUtf8(msgstr) : QString(); } else { return QString(); } } QString KCatalog::translate(const QByteArray &msgid, const QByteArray &msgid_plural, qulonglong n) const { if (!d->localeDir.isEmpty()) { QMutexLocker locker(&catalogStaticData()->mutex); d->setupGettextEnv(); - const char *msgstr = dngettext(d->domain.constData(), msgid.constData(), msgid_plural.constData(), n); + const char *msgid_char = msgid.constData(); + const char *msgid_plural_char = msgid_plural.constData(); + const char *msgstr = dngettext(d->domain.constData(), msgid_char, msgid_plural_char, n); d->resetSystemLanguage(); // If original and translation are same, dngettext will return // the original pointer, which is generally fine, except in // the corner cases where e.g. msgstr[1] is same as msgid. // Therefore check for pointer difference only with msgid or // only with msgid_plural, and not with both. - return (n == 1 && msgstr != msgid) || (n != 1 && msgstr != msgid_plural) + return (n == 1 && msgstr != msgid_char) || (n != 1 && msgstr != msgid_plural_char) ? QString::fromUtf8(msgstr) : QString(); } else { return QString(); } } QString KCatalog::translate(const QByteArray &msgctxt, const QByteArray &msgid, const QByteArray &msgid_plural, qulonglong n) const { if (!d->localeDir.isEmpty()) { QMutexLocker locker(&catalogStaticData()->mutex); d->setupGettextEnv(); - const char *msgstr = dnpgettext_expr(d->domain.constData(), msgctxt.constData(), msgid.constData(), msgid_plural.constData(), n); + const char *msgid_char = msgid.constData(); + const char *msgid_plural_char = msgid_plural.constData(); + const char *msgstr = dnpgettext_expr(d->domain.constData(), msgctxt.constData(), msgid_char, msgid_plural_char, n); d->resetSystemLanguage(); - return (n == 1 && msgstr != msgid) || (n != 1 && msgstr != msgid_plural) + return (n == 1 && msgstr != msgid_char) || (n != 1 && msgstr != msgid_plural_char) ? QString::fromUtf8(msgstr) : QString(); } else { return QString(); } } void KCatalog::addDomainLocaleDir(const QByteArray &domain, const QString &path) { QMutexLocker(&catalogStaticData()->mutex); catalogStaticData()->customCatalogDirs.insert(domain, path); }