mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Translation] Name languages without reading the translation files The language selector derived every label from the language's own .qm file: it loaded the file and translated the literal "English" in the "i18n" context. lupdate could never extract that literal from the callers, which pass the DEFAULT_LANG_NAME macro, so a dummy QT_TRANSLATE_NOOP in cache_settings.cpp existed to give it something to extract. #7050 deleted that dummy, the next lupdate push dropped the string from Transifex, and the following pull rewrote the .ts files without it. lrelease only emits translated messages, so 22 of 25 .qm files returned an empty name. The names are data, not translations: a selector has to be readable before any translation is loaded, and a label must not depend on the translation of the language it names. TranslationLoader::languageDisplayName now holds them next to availableLanguages, falling back to Qt's native language name and then to the code, so a language added later still gets a label. Also select the configured language by item data instead of by matching the translated "English" string, which pointed at the wrong row whenever the lookup came back empty, and restore that English entry's own name: en_US is "English (United States)" so it can no longer be confused with the untranslated default. Oracle shared the code path, so its wizard language page is fixed too. * [Translation] Name the default language selection once Review follow-up: log the unresolvable language through the page's own category, and move the en_US entry both selectors fall back to for an unset setting into TranslationLoader::defaultLanguageCode. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
316 lines
13 KiB
C++
316 lines
13 KiB
C++
#include "gtest/gtest.h"
|
|
#include <QByteArray>
|
|
#include <QCoreApplication>
|
|
#include <QDir>
|
|
#include <QFileInfo>
|
|
#include <QLibraryInfo>
|
|
#include <QList>
|
|
#include <QSet>
|
|
#include <QString>
|
|
#include <QStringList>
|
|
#include <QStringLiteral>
|
|
#include <QTemporaryDir>
|
|
#include <QTranslator>
|
|
#include <libcockatrice/utility/translation_loader.h>
|
|
#include <string>
|
|
|
|
// The directory holding translation_loader_test_de.qm, compiled from translation_loader_test.ts by the
|
|
// build. QTranslator rejects anything that is not a real .qm file, so a stub cannot stand in here.
|
|
#ifndef TRANSLATION_LOADER_TEST_DIR
|
|
#define TRANSLATION_LOADER_TEST_DIR ""
|
|
#endif
|
|
|
|
// The client's translation files, whose language codes are the languages the language selector offers.
|
|
#ifndef COCKATRICE_TRANSLATIONS_DIR
|
|
#define COCKATRICE_TRANSLATIONS_DIR ""
|
|
#endif
|
|
|
|
namespace
|
|
{
|
|
const QString testContext = QStringLiteral("TranslationLoaderTest");
|
|
const QString firstMessage = QStringLiteral("Load into the first translator");
|
|
const QString secondMessage = QStringLiteral("Load into the second translator");
|
|
const QString germanFirstMessage = QStringLiteral("In den ersten Übersetzer laden");
|
|
const QString germanSecondMessage = QStringLiteral("In den zweiten Übersetzer laden");
|
|
|
|
QString translate(const QTranslator &_translator, const QString &_sourceText)
|
|
{
|
|
return _translator.translate(testContext.toUtf8().constData(), _sourceText.toUtf8().constData());
|
|
}
|
|
|
|
QString testTranslationDirectory()
|
|
{
|
|
return QString(TRANSLATION_LOADER_TEST_DIR);
|
|
}
|
|
|
|
// GTest's main() creates no QCoreApplication, which the search paths are derived from.
|
|
const QCoreApplication &application()
|
|
{
|
|
static int argc = 1;
|
|
static char executable[] = "translation_loader_test";
|
|
static char *argv[] = {executable, nullptr};
|
|
static const QCoreApplication app(argc, argv);
|
|
return app;
|
|
}
|
|
} // namespace
|
|
|
|
TEST(TranslationLoaderTest, LoadsFromTheFirstDirectoryHoldingTheFile)
|
|
{
|
|
QTemporaryDir emptyDir;
|
|
ASSERT_TRUE(emptyDir.isValid());
|
|
|
|
QTranslator translator;
|
|
|
|
// The first directory is searched but has no file, the second one has.
|
|
EXPECT_EQ(TranslationLoadResult::Loaded,
|
|
TranslationLoader::loadFrom(translator, QStringLiteral("translation_loader_test_de"),
|
|
{emptyDir.path(), testTranslationDirectory()}));
|
|
EXPECT_EQ(germanFirstMessage, translate(translator, firstMessage));
|
|
}
|
|
|
|
TEST(TranslationLoaderTest, LoadedTranslatorResolvesTheTranslatedString)
|
|
{
|
|
QTranslator translator;
|
|
|
|
ASSERT_EQ(TranslationLoadResult::Loaded,
|
|
TranslationLoader::loadFrom(translator, QStringLiteral("translation_loader_test_de"),
|
|
{testTranslationDirectory()}));
|
|
|
|
EXPECT_EQ(germanFirstMessage, translate(translator, firstMessage));
|
|
EXPECT_EQ(germanSecondMessage, translate(translator, secondMessage));
|
|
}
|
|
|
|
TEST(TranslationLoaderTest, SeparateTranslatorsKeepTheirOwnFiles)
|
|
{
|
|
// QTranslator discards its previous contents on every load, so a caller reusing one translator
|
|
// for two files keeps only the last of them. That is why Qt's translation and the application
|
|
// translation are loaded into translators of their own.
|
|
QTranslator firstTranslator;
|
|
QTranslator secondTranslator;
|
|
|
|
ASSERT_EQ(TranslationLoadResult::Loaded,
|
|
TranslationLoader::loadFrom(firstTranslator, QStringLiteral("translation_loader_test_de"),
|
|
{testTranslationDirectory()}));
|
|
ASSERT_EQ(TranslationLoadResult::Loaded,
|
|
TranslationLoader::loadFrom(secondTranslator, QStringLiteral("translation_loader_test_de"),
|
|
{testTranslationDirectory()}));
|
|
|
|
EXPECT_FALSE(firstTranslator.isEmpty());
|
|
EXPECT_FALSE(secondTranslator.isEmpty());
|
|
EXPECT_EQ(germanFirstMessage, translate(firstTranslator, firstMessage));
|
|
EXPECT_EQ(germanSecondMessage, translate(secondTranslator, secondMessage));
|
|
}
|
|
|
|
TEST(TranslationLoaderTest, ExistingDirectoryWithoutTheFileIsMissing)
|
|
{
|
|
QTemporaryDir emptyDir;
|
|
ASSERT_TRUE(emptyDir.isValid());
|
|
|
|
QTranslator translator;
|
|
|
|
EXPECT_EQ(TranslationLoadResult::Missing,
|
|
TranslationLoader::loadFrom(translator, QStringLiteral("translation_loader_test_de"), {emptyDir.path()}));
|
|
EXPECT_TRUE(translator.isEmpty());
|
|
}
|
|
|
|
TEST(TranslationLoaderTest, NonexistentDirectoriesAreNotSearched)
|
|
{
|
|
QTemporaryDir emptyDir;
|
|
ASSERT_TRUE(emptyDir.isValid());
|
|
|
|
QTranslator translator;
|
|
const QString missingDirectory = emptyDir.path() + QStringLiteral("/does-not-exist");
|
|
|
|
EXPECT_EQ(
|
|
TranslationLoadResult::NoDirectory,
|
|
TranslationLoader::loadFrom(translator, QStringLiteral("translation_loader_test_de"), {missingDirectory}));
|
|
EXPECT_EQ(TranslationLoadResult::NoDirectory,
|
|
TranslationLoader::loadFrom(translator, QStringLiteral("translation_loader_test_de"), {}));
|
|
EXPECT_TRUE(translator.isEmpty());
|
|
}
|
|
|
|
TEST(TranslationLoaderTest, EmptyDirectoryEntriesAreSkipped)
|
|
{
|
|
QTranslator translator;
|
|
|
|
EXPECT_EQ(TranslationLoadResult::Loaded,
|
|
TranslationLoader::loadFrom(translator, QStringLiteral("translation_loader_test_de"),
|
|
{QString(), testTranslationDirectory()}));
|
|
}
|
|
|
|
TEST(TranslationLoaderTest, LanguageVariantsFallBackToTheBaseLanguage)
|
|
{
|
|
QTranslator translator;
|
|
|
|
// Users pick languages such as en_US or zh-Hans that need not have a translation file of their
|
|
// own; a language without one falls back to the file without its country or script part.
|
|
EXPECT_EQ(TranslationLoadResult::Loaded,
|
|
TranslationLoader::loadFrom(translator, QStringLiteral("translation_loader_test_de_CH"),
|
|
{testTranslationDirectory()}));
|
|
}
|
|
|
|
TEST(TranslationLoaderTest, QtTranslationCandidatesCoverTheUsualInstallLayouts)
|
|
{
|
|
const QList<QString> candidates = TranslationLoader::qtTranslationCandidates();
|
|
|
|
EXPECT_FALSE(candidates.isEmpty());
|
|
for (const QString &candidate : candidates) {
|
|
EXPECT_FALSE(candidate.isEmpty());
|
|
}
|
|
EXPECT_EQ(candidates.size(), QSet<QString>(candidates.cbegin(), candidates.cend()).size());
|
|
|
|
// A relocated Qt can report a prefix-relative translations path that does not exist (the KDE
|
|
// runtime reports /usr/translations while its files live in /usr/share/qt6/translations), so the
|
|
// layout Qt actually installs into has to be among the candidates.
|
|
const QString prefix = QLibraryInfo::path(QLibraryInfo::PrefixPath);
|
|
EXPECT_TRUE(candidates.contains(prefix + QStringLiteral("/share/qt6/translations")));
|
|
}
|
|
|
|
TEST(TranslationLoaderTest, QtTranslationPathsOnlyHoldExistingDirectories)
|
|
{
|
|
const QList<QString> candidates = TranslationLoader::qtTranslationCandidates();
|
|
const QList<QString> paths = TranslationLoader::qtTranslationPaths();
|
|
|
|
for (const QString &path : paths) {
|
|
EXPECT_FALSE(path.isEmpty());
|
|
EXPECT_TRUE(QDir(path).exists()) << path.toStdString();
|
|
}
|
|
|
|
EXPECT_EQ(paths.size(), QSet<QString>(paths.cbegin(), paths.cend()).size());
|
|
|
|
// Every candidate that exists is searched, and nothing else is.
|
|
for (const QString &candidate : candidates) {
|
|
if (QDir(candidate).exists()) {
|
|
EXPECT_TRUE(paths.contains(candidate)) << candidate.toStdString();
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(TranslationLoaderTest, LoadApplicationUsesThePrefixAndTheLanguage)
|
|
{
|
|
QTranslator translator;
|
|
|
|
ASSERT_EQ(TranslationLoadResult::Loaded,
|
|
TranslationLoader::loadApplication(translator, QStringLiteral("translation_loader_test"),
|
|
QStringLiteral("de"), {testTranslationDirectory()}));
|
|
EXPECT_EQ(germanSecondMessage, translate(translator, secondMessage));
|
|
}
|
|
|
|
TEST(TranslationLoaderTest, ABuildTreeFindsTheTranslationsNextToTheExecutable)
|
|
{
|
|
// The regression this guards: a build tree keeps the .qm files next to the executable rather than
|
|
// in the installed layout, so a developer build loaded no translation and offered no language.
|
|
// This test binary's .qm file sits in its own directory, just like cockatrice's ones do.
|
|
const QString executableDirectory = application().applicationDirPath();
|
|
const QList<QString> paths = TranslationLoader::applicationTranslationPaths(
|
|
executableDirectory + QStringLiteral("/../share/cockatrice/translations"));
|
|
|
|
ASSERT_EQ(2, paths.size());
|
|
EXPECT_EQ(executableDirectory + QStringLiteral("/../share/cockatrice/translations"), paths.at(0));
|
|
EXPECT_EQ(executableDirectory, paths.at(1));
|
|
|
|
const QList<QString> languages =
|
|
TranslationLoader::availableLanguages(QStringLiteral("translation_loader_test"), paths);
|
|
EXPECT_EQ(QStringList({QStringLiteral("de")}), QStringList(languages.cbegin(), languages.cend()));
|
|
}
|
|
|
|
TEST(TranslationLoaderTest, ApplicationTranslationPathsHoldNoDuplicates)
|
|
{
|
|
const QString executableDirectory = application().applicationDirPath();
|
|
|
|
EXPECT_EQ(1, TranslationLoader::applicationTranslationPaths(executableDirectory).size());
|
|
|
|
// Without an installed layout to search there is nothing left but the executable's directory.
|
|
const QList<QString> withoutInstalledPath = TranslationLoader::applicationTranslationPaths(QString());
|
|
ASSERT_EQ(1, withoutInstalledPath.size());
|
|
EXPECT_EQ(executableDirectory, withoutInstalledPath.at(0));
|
|
}
|
|
|
|
TEST(TranslationLoaderTest, AvailableLanguagesListsTheTranslationsThatCanBeLoaded)
|
|
{
|
|
const QList<QString> languages =
|
|
TranslationLoader::availableLanguages(QStringLiteral("translation_loader_test"), {testTranslationDirectory()});
|
|
|
|
EXPECT_EQ(QStringList({QStringLiteral("de")}), QStringList(languages.cbegin(), languages.cend()));
|
|
|
|
// Files of another application and files without a language code are not languages.
|
|
EXPECT_TRUE(TranslationLoader::availableLanguages(QStringLiteral("some_other_app"), {testTranslationDirectory()})
|
|
.isEmpty());
|
|
EXPECT_TRUE(TranslationLoader::availableLanguages(QStringLiteral("translation_loader_test"), {}).isEmpty());
|
|
}
|
|
|
|
TEST(TranslationLoaderTest, LanguageDisplayNamesDoNotNeedATranslationFile)
|
|
{
|
|
// The regression this guards: the selector took every label from the language's own .qm file, so it
|
|
// went blank as soon as translators had not translated the "English" placeholder in the i18n context.
|
|
EXPECT_EQ(QStringLiteral("Deutsch (German)"), TranslationLoader::languageDisplayName(QStringLiteral("de")));
|
|
EXPECT_EQ(QStringLiteral("简体中文 (Chinese Simplified)"),
|
|
TranslationLoader::languageDisplayName(QStringLiteral("zh-Hans")));
|
|
EXPECT_EQ(QStringLiteral("English, arr! (Pirate English)"),
|
|
TranslationLoader::languageDisplayName(QStringLiteral("en@pirate")));
|
|
}
|
|
|
|
TEST(TranslationLoaderTest, EveryLanguageTheClientShipsHasItsOwnDisplayName)
|
|
{
|
|
// The shipped translation files are what the selector offers, so each of them has to end up with a
|
|
// label: an empty one is what made the dropdown look broken, and two equal ones are a coin flip.
|
|
const QDir translations(QStringLiteral(COCKATRICE_TRANSLATIONS_DIR));
|
|
const QStringList files = translations.entryList({QStringLiteral("cockatrice_*.ts")}, QDir::Files);
|
|
ASSERT_FALSE(files.isEmpty());
|
|
|
|
QStringList languages;
|
|
QSet<QString> displayNames;
|
|
for (const QString &file : files) {
|
|
const QString language = QFileInfo(file).completeBaseName().section(QLatin1Char('_'), 1);
|
|
const QString displayName = TranslationLoader::languageDisplayName(language);
|
|
EXPECT_FALSE(displayName.isEmpty()) << language.toStdString();
|
|
languages << language;
|
|
displayNames.insert(displayName);
|
|
}
|
|
|
|
EXPECT_EQ(files.size(), displayNames.size());
|
|
|
|
// The selector preselects this one for an unset setting, so it has to be offered at all.
|
|
EXPECT_TRUE(languages.contains(TranslationLoader::defaultLanguageCode()));
|
|
}
|
|
|
|
TEST(TranslationLoaderTest, UnknownLanguagesFallBackToTheNativeNameThenTheCode)
|
|
{
|
|
// A language added after the table, and a code Qt does not know either.
|
|
EXPECT_FALSE(TranslationLoader::languageDisplayName(QStringLiteral("sv_FI")).isEmpty());
|
|
EXPECT_EQ(QStringLiteral("zz_QQ"), TranslationLoader::languageDisplayName(QStringLiteral("zz_QQ")));
|
|
EXPECT_TRUE(TranslationLoader::languageDisplayName(QString()).isEmpty());
|
|
}
|
|
|
|
TEST(TranslationLoaderTest, LoadApplicationReportsAMissingTranslationDirectory)
|
|
{
|
|
QTemporaryDir emptyDir;
|
|
ASSERT_TRUE(emptyDir.isValid());
|
|
|
|
QTranslator translator;
|
|
|
|
EXPECT_EQ(TranslationLoadResult::NoDirectory,
|
|
TranslationLoader::loadApplication(translator, QStringLiteral("translation_loader_test"),
|
|
QStringLiteral("de"),
|
|
{emptyDir.path() + QStringLiteral("/translations")}));
|
|
}
|
|
|
|
TEST(TranslationLoaderTest, UnsetLanguageLoadsNothing)
|
|
{
|
|
QTranslator translator;
|
|
|
|
// An unset language is the default for English, which has no translation file to begin with:
|
|
// nothing is searched for and the translator is left untouched.
|
|
EXPECT_EQ(TranslationLoadResult::Missing,
|
|
TranslationLoader::loadApplication(translator, QStringLiteral("translation_loader_test"), QString(),
|
|
{testTranslationDirectory()}));
|
|
EXPECT_EQ(TranslationLoadResult::Missing, TranslationLoader::loadQt(translator, QString()));
|
|
EXPECT_TRUE(translator.isEmpty());
|
|
EXPECT_TRUE(translator.filePath().isEmpty());
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
return RUN_ALL_TESTS();
|
|
}
|