mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-09 11:33:25 -08:00
Compare commits
3 commits
b8b81170bf
...
5dc5390880
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5dc5390880 | ||
|
|
0934380a5c | ||
|
|
3aba1ef929 |
21 changed files with 1105 additions and 132 deletions
|
|
@ -798,6 +798,18 @@ if(Qt6LinguistTools_FOUND)
|
|||
elseif(WIN32)
|
||||
install(FILES ${cockatrice_QM} DESTINATION ${COCKATRICE_WIN32_QM_INSTALL_DIR})
|
||||
endif()
|
||||
|
||||
# Qt attaches the .qm files to the default build target only, so building the executable on its own
|
||||
# (CLion, --target cockatrice) leaves it without any translations: nothing is translated and the
|
||||
# language selector stays empty.
|
||||
# Depend on Qt's own lrelease target instead of attaching the .qm custom commands to a second
|
||||
# target: two independent targets owning the same outputs race on MSBuild/Makefiles and are an
|
||||
# error on Xcode. Older Qt versions already add this dependency themselves.
|
||||
foreach(_lrelease_target Cockatrice_lrelease cockatrice_lrelease)
|
||||
if(TARGET ${_lrelease_target})
|
||||
add_dependencies(cockatrice ${_lrelease_target})
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
qt6_finalize_target(cockatrice)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include <QRegularExpression>
|
||||
#include <QSysInfo>
|
||||
#include <QtGlobal>
|
||||
#include <optional>
|
||||
|
||||
#if defined(Q_OS_MACOS)
|
||||
#include <sys/sysctl.h>
|
||||
|
|
@ -26,6 +27,29 @@
|
|||
|
||||
#define GIT_SHORT_HASH_LEN 7
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
|
||||
// QSysInfo::productVersion() is not guaranteed to start with a number, e.g. it returns
|
||||
// "Server 2022" on Windows Server. Use the first numeric token as the major host version.
|
||||
std::optional<int> getProductVersionMajor()
|
||||
{
|
||||
const QString versionString = QSysInfo::productVersion();
|
||||
bool ok = false;
|
||||
static const QRegularExpression firstNumber(R"((\d+))");
|
||||
const auto match = firstNumber.match(versionString);
|
||||
const int version = match.hasMatch() ? match.captured(1).toInt(&ok) : 0;
|
||||
if (!ok) {
|
||||
qCWarning(ReleaseChannelLog) << "Unable to determine OS version from" << versionString;
|
||||
return std::nullopt;
|
||||
}
|
||||
return version;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
ReleaseChannel::ReleaseChannel() : netMan(new QNetworkAccessManager(this)), response(nullptr), lastRelease(nullptr)
|
||||
{
|
||||
}
|
||||
|
|
@ -43,55 +67,93 @@ void ReleaseChannel::checkForUpdates()
|
|||
connect(response, &QNetworkReply::finished, this, &ReleaseChannel::releaseListFinished);
|
||||
}
|
||||
|
||||
// Different release channel checking functions for different operating systems
|
||||
bool ReleaseChannel::downloadMatchesCurrentOS(const QString &fileName)
|
||||
// Find assets compatible with host platform (Linux is not supported by in-client updater)
|
||||
std::optional<int> ReleaseChannel::getTargetVersionForCurrentOS(const QString &fileName)
|
||||
{
|
||||
#if defined(Q_OS_MACOS)
|
||||
static QRegularExpression version_regex("macOS(\\d+)");
|
||||
auto match = version_regex.match(fileName);
|
||||
if (!match.hasMatch()) {
|
||||
return false;
|
||||
#if defined(Q_OS_WIN) && Q_PROCESSOR_WORDSIZE == 4
|
||||
// The published Windows assets are 64-bit only
|
||||
Q_UNUSED(fileName);
|
||||
return std::nullopt;
|
||||
#endif
|
||||
|
||||
const QRegularExpression *regex = nullptr;
|
||||
|
||||
// Only platforms shipping updater assets (macOS, Windows) need the host version
|
||||
static const std::optional<int> systemVersion =
|
||||
#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
|
||||
getProductVersionMajor();
|
||||
#else
|
||||
std::nullopt;
|
||||
#endif
|
||||
|
||||
if (!systemVersion) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto getSystemVersion = [] {
|
||||
#if defined(Q_OS_MACOS)
|
||||
static const bool isIntel = [] {
|
||||
// QSysInfo does not go through translation layers
|
||||
// We need to use sysctl to reliably detect the underlying architecture
|
||||
char arch[255];
|
||||
size_t len = sizeof(arch);
|
||||
if (sysctlbyname("machdep.cpu.brand_string", arch, &len, nullptr, 0) == 0) {
|
||||
// Intel mac is only supported on macOS 13 versions
|
||||
if (QString::fromUtf8(arch).contains("Intel")) {
|
||||
return 13;
|
||||
}
|
||||
return QString::fromUtf8(arch).contains("Intel");
|
||||
}
|
||||
return false;
|
||||
}();
|
||||
|
||||
return QSysInfo::productVersion().split(".")[0].toInt();
|
||||
};
|
||||
|
||||
// older(smaller) releases are compatible with a newer or the same system version
|
||||
int sys_maj = getSystemVersion();
|
||||
int rel_maj = match.captured(1).toInt();
|
||||
return rel_maj == sys_maj;
|
||||
// Apple (ARM) --> Cockatrice-3.0.0-macOS15.dmg
|
||||
// Apple (x86) --> Cockatrice-3.0.0-macOS15_Intel.dmg
|
||||
static const QRegularExpression macIntelRegex(R"(-macOS(\d+)_Intel\.[^.]+$)",
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
static const QRegularExpression macRegex(R"(-macOS(\d+)\.[^.]+$)", QRegularExpression::CaseInsensitiveOption);
|
||||
regex = isIntel ? &macIntelRegex : &macRegex;
|
||||
|
||||
#elif defined(Q_OS_WIN)
|
||||
#if Q_PROCESSOR_WORDSIZE == 4
|
||||
return fileName.contains("32bit");
|
||||
#elif Q_PROCESSOR_WORDSIZE == 8
|
||||
const QString &version = QSysInfo::productVersion();
|
||||
if (version.startsWith("7") || version.startsWith("8")) {
|
||||
return fileName.contains("Win7");
|
||||
} else {
|
||||
return fileName.contains("Win10");
|
||||
}
|
||||
#else
|
||||
// Windows (x86) --> Cockatrice-3.0.0-Windows10.exe
|
||||
static const QRegularExpression winRegex(R"(-(?:Win|Windows)(\d+)\.[^.]+$)",
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
regex = &winRegex;
|
||||
|
||||
#else // if the OS doesn't fit one of the above #defines, then it will never match
|
||||
Q_UNUSED(fileName);
|
||||
return false;
|
||||
return std::nullopt;
|
||||
#endif
|
||||
|
||||
#else // If the OS doesn't fit one of the above #defines, then it will never match
|
||||
Q_UNUSED(fileName);
|
||||
return false;
|
||||
#endif
|
||||
// Any asset targeting an OS version up to the current host version works
|
||||
auto match = regex->match(fileName);
|
||||
if (!match.hasMatch()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const int targetVersion = match.captured(1).toInt();
|
||||
if (targetVersion > *systemVersion) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return targetVersion;
|
||||
}
|
||||
|
||||
// Pick newest targeted version (highest number) amongst all compatible ones
|
||||
QString ReleaseChannel::findBestDownloadUrl(const QVariantList &assets)
|
||||
{
|
||||
QString bestUrl;
|
||||
int bestVersion = -1;
|
||||
for (const auto &rawAsset : assets) {
|
||||
QVariantMap asset = rawAsset.toMap();
|
||||
QString name = asset["name"].toString();
|
||||
QString url = asset["browser_download_url"].toString();
|
||||
auto version = getTargetVersionForCurrentOS(name);
|
||||
if (!version) {
|
||||
continue;
|
||||
}
|
||||
if (*version > bestVersion) {
|
||||
bestVersion = *version;
|
||||
bestUrl = url;
|
||||
}
|
||||
}
|
||||
if (!bestUrl.isEmpty()) {
|
||||
qCInfo(ReleaseChannelLog) << "Best compatible asset=" << bestUrl;
|
||||
}
|
||||
return bestUrl;
|
||||
}
|
||||
|
||||
QString StableReleaseChannel::getManualDownloadUrl() const
|
||||
|
|
@ -138,16 +200,9 @@ void StableReleaseChannel::releaseListFinished()
|
|||
lastRelease->setPublishDate(resultMap["published_at"].toDate());
|
||||
|
||||
if (resultMap.contains("assets")) {
|
||||
auto rawAssets = resultMap["assets"].toList();
|
||||
for (const auto &rawAsset : rawAssets) {
|
||||
QVariantMap asset = rawAsset.toMap();
|
||||
QString name = asset["name"].toString();
|
||||
QString url = asset["browser_download_url"].toString();
|
||||
|
||||
if (downloadMatchesCurrentOS(name)) {
|
||||
lastRelease->setDownloadUrl(url);
|
||||
break;
|
||||
}
|
||||
auto url = findBestDownloadUrl(resultMap["assets"].toList());
|
||||
if (!url.isEmpty()) {
|
||||
lastRelease->setDownloadUrl(url);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -289,21 +344,10 @@ void BetaReleaseChannel::fileListFinished()
|
|||
bool needToUpdate = (QString::compare(shortHash, myHash, Qt::CaseInsensitive) != 0);
|
||||
bool compatibleVersion = false;
|
||||
|
||||
QStringList resultUrlList{};
|
||||
for (QVariant file : resultList) {
|
||||
QVariantMap map = file.toMap();
|
||||
resultUrlList << map["browser_download_url"].toString();
|
||||
}
|
||||
|
||||
resultUrlList.sort();
|
||||
// iterate in reverse so the first item is the latest os version
|
||||
for (auto url = resultUrlList.rbegin(); url < resultUrlList.rend(); ++url) {
|
||||
if (downloadMatchesCurrentOS(*url)) {
|
||||
compatibleVersion = true;
|
||||
lastRelease->setDownloadUrl(*url);
|
||||
qCInfo(ReleaseChannelLog) << "Found compatible version url=" << *url;
|
||||
break;
|
||||
}
|
||||
QString downloadUrl = findBestDownloadUrl(resultList);
|
||||
if (!downloadUrl.isEmpty()) {
|
||||
compatibleVersion = true;
|
||||
lastRelease->setDownloadUrl(downloadUrl);
|
||||
}
|
||||
|
||||
emit finishedCheck(needToUpdate, compatibleVersion, lastRelease);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVariantMap>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(ReleaseChannelLog, "release_channel");
|
||||
|
|
@ -96,7 +97,8 @@ protected:
|
|||
Release *lastRelease;
|
||||
|
||||
protected:
|
||||
static bool downloadMatchesCurrentOS(const QString &fileName);
|
||||
static std::optional<int> getTargetVersionForCurrentOS(const QString &fileName);
|
||||
static QString findBestDownloadUrl(const QVariantList &assets);
|
||||
[[nodiscard]] virtual QString getReleaseChannelUrl() const = 0;
|
||||
|
||||
public:
|
||||
|
|
|
|||
|
|
@ -110,6 +110,16 @@ SettingsCache::SettingsCache()
|
|||
|
||||
settings = new QSettings(settingsPath + "global.ini", QSettings::IniFormat, this);
|
||||
|
||||
// Both migrations below overwrite per-domain files in place, so snapshot the whole settings
|
||||
// folder first: global.ini is preserved as global.ini.old, but pre-existing per-domain files
|
||||
// (and a re-migration after a downgrade) would otherwise have no way back.
|
||||
if (SettingsMigration::isMigrationPending(settingsPath)) {
|
||||
const QString backupPath = SettingsMigration::backupSettingsDirectory(settingsPath);
|
||||
if (!backupPath.isEmpty()) {
|
||||
qCInfo(SettingsCacheLog) << "[SettingsCache] Settings backed up before migration:" << backupPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate from legacy NativeFormat settings to per-class INI files (runs at most once)
|
||||
if (!isPortableBuild) {
|
||||
SettingsMigration::migrateLegacySettings(settingsPath);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
#include <libcockatrice/settings/tabs_settings.h>
|
||||
#include <libcockatrice/settings/updates_settings.h>
|
||||
#include <libcockatrice/utility/macros.h>
|
||||
#include <libcockatrice/utility/translation_loader.h>
|
||||
|
||||
enum startupCardUpdateCheckBehaviorIndex
|
||||
{
|
||||
|
|
@ -333,22 +334,18 @@ GeneralSettingsPage::GeneralSettingsPage()
|
|||
|
||||
QStringList GeneralSettingsPage::findQmFiles()
|
||||
{
|
||||
QDir dir(translationPath);
|
||||
QStringList fileNames = dir.entryList(QStringList(translationPrefix + "_*.qm"), QDir::Files, QDir::Name);
|
||||
fileNames.replaceInStrings(QRegularExpression(translationPrefix + "_(.*)\\.qm"), "\\1");
|
||||
return fileNames;
|
||||
return TranslationLoader::availableLanguages(translationPrefix,
|
||||
TranslationLoader::applicationTranslationPaths(translationPath));
|
||||
}
|
||||
|
||||
QString GeneralSettingsPage::languageName(const QString &lang)
|
||||
{
|
||||
QTranslator qTranslator;
|
||||
|
||||
QString appNameHint = translationPrefix + "_" + lang;
|
||||
bool appTranslationLoaded = qTranslator.load(appNameHint, translationPath);
|
||||
if (!appTranslationLoaded) {
|
||||
qCWarning(GeneralSettingsPageLog)
|
||||
<< "Unable to load" << translationPrefix << "translation" << appNameHint << "at" << translationPath;
|
||||
}
|
||||
// Only the language name is needed here, so the file is loaded without logging a line per
|
||||
// language: findQmFiles() asked for languages whose files exist in the first place.
|
||||
TranslationLoader::loadFrom(qTranslator, translationPrefix + "_" + lang,
|
||||
TranslationLoader::applicationTranslationPaths(translationPath));
|
||||
|
||||
return qTranslator.translate("i18n", DEFAULT_LANG_NAME);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@
|
|||
#include <QCryptographicHash>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QLibraryInfo>
|
||||
#include <QLocale>
|
||||
#include <QMessageBox>
|
||||
#include <QSystemTrayIcon>
|
||||
|
|
@ -55,6 +54,7 @@
|
|||
#include <libcockatrice/settings/network_settings.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
#include <libcockatrice/utility/cryptoutil.h>
|
||||
#include <libcockatrice/utility/translation_loader.h>
|
||||
|
||||
QTranslator *translator, *qtTranslator;
|
||||
RNG_Abstract *rng;
|
||||
|
|
@ -141,26 +141,13 @@ void installNewTranslator()
|
|||
{
|
||||
QString lang = SettingsCache::instance().personal().getLang();
|
||||
|
||||
QString qtNameHint = "qt_" + lang;
|
||||
QString qtTranslationPath = QLibraryInfo::path(QLibraryInfo::TranslationsPath);
|
||||
|
||||
bool qtTranslationLoaded = qtTranslator->load(qtNameHint, qtTranslationPath);
|
||||
if (!qtTranslationLoaded) {
|
||||
qCWarning(QtTranslatorDebug) << "Unable to load qt translation" << qtNameHint << "at" << qtTranslationPath;
|
||||
} else {
|
||||
qCInfo(QtTranslatorDebug) << "Loaded qt translation" << qtNameHint << "at" << qtTranslationPath;
|
||||
}
|
||||
// Qt's own strings and ours need a translator each: QTranslator discards its previous contents
|
||||
// on every load, so loading both files into one translator silently drops the first of them.
|
||||
TranslationLoader::loadQt(*qtTranslator, lang);
|
||||
qApp->installTranslator(qtTranslator);
|
||||
|
||||
QString appNameHint = translationPrefix + "_" + lang;
|
||||
bool appTranslationLoaded = qtTranslator->load(appNameHint, translationPath);
|
||||
if (!appTranslationLoaded) {
|
||||
qCWarning(QtTranslatorDebug) << "Unable to load" << translationPrefix << "translation" << appNameHint << "at"
|
||||
<< translationPath;
|
||||
} else {
|
||||
qCInfo(QtTranslatorDebug) << "Loaded" << translationPrefix << "translation" << appNameHint << "at"
|
||||
<< translationPath;
|
||||
}
|
||||
TranslationLoader::loadApplication(*translator, translationPrefix, lang,
|
||||
TranslationLoader::applicationTranslationPaths(translationPath));
|
||||
qApp->installTranslator(translator);
|
||||
}
|
||||
|
||||
|
|
@ -390,8 +377,11 @@ int main(int argc, char *argv[])
|
|||
qCInfo(MainLog) << "MainWindow constructor finished";
|
||||
|
||||
ui.setWindowIcon(themePixmap(QStringLiteral("cockatrice")));
|
||||
// Set name of the app desktop file; used by wayland to load the window icon
|
||||
QGuiApplication::setDesktopFileName("Cockatrice");
|
||||
// Base name of the desktop file installed next to the binary, without the ".desktop"
|
||||
// extension. Wayland reads the window icon from it, and xdg-desktop-portal looks up
|
||||
// org.freedesktop.host.portal.Registry by this app ID, so it has to match the file name
|
||||
// exactly, down to the case.
|
||||
QGuiApplication::setDesktopFileName("cockatrice");
|
||||
|
||||
SettingsCache::instance().network().setClientID(generateClientID());
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
#include <QLoggingCategory>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(MainLog, "main");
|
||||
inline Q_LOGGING_CATEGORY(QtTranslatorDebug, "qt_translator");
|
||||
|
||||
class CardDatabase;
|
||||
class QString;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@
|
|||
|
||||
- @subpage beta_release
|
||||
|
||||
## Settings
|
||||
|
||||
- @subpage settings_folder
|
||||
|
||||
## Card Pictures
|
||||
|
||||
- @subpage custom_card_pictures
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
@page settings_folder Settings Folder and Backups
|
||||
|
||||
Cockatrice keeps all of its preferences in a settings folder on your computer, one `.ini` file per
|
||||
group of settings (appearance, sound, decks, and so on). Open it from the main window via
|
||||
Help → Open Settings Folder.
|
||||
|
||||
# The Per-File Migration
|
||||
|
||||
Older versions of Cockatrice stored every setting in a single `global.ini` file. The settings were
|
||||
split into per-group `.ini` files so that unrelated groups no longer share one file. The first time
|
||||
you start a version that uses the new layout, your settings are moved into the new files
|
||||
automatically and nothing is lost. The old `global.ini` is kept next to the new files as
|
||||
`global.ini.old`.
|
||||
|
||||
# Automatic Backups
|
||||
|
||||
Both migrations overwrite files in the settings folder in place, so before either one runs,
|
||||
Cockatrice copies the whole folder to a timestamped backup folder:
|
||||
|
||||
```
|
||||
<data folder>/settings-backup-20261002-193000/
|
||||
```
|
||||
|
||||
The backup folder sits next to the settings folder, not inside it. Every backup is kept, so if
|
||||
settings ever end up wrong you can compare against, or copy files back from, the earlier snapshot.
|
||||
Backups are only created when a migration actually has work to do, so a normal start creates
|
||||
nothing.
|
||||
|
||||
# Downgrading
|
||||
|
||||
Settings migrations only run forwards. If you ever go back to an older version of Cockatrice, that
|
||||
version does not read the new per-group files, so it will start with default settings.
|
||||
|
||||
Before downgrading, copy your settings folder somewhere safe, for example:
|
||||
|
||||
- Linux: `~/.local/share/Cockatrice/settings/`
|
||||
- macOS: `~/Library/Application Support/Cockatrice/settings/`
|
||||
- Windows: `%LOCALAPPDATA%\Cockatrice\settings\`
|
||||
|
||||
Portable installs keep the settings folder in the `data` folder next to the executable.
|
||||
|
||||
Copying that folder back after downgrading restores your settings. The automatic backup folder from
|
||||
the migration is there as a second copy of the same data if you need it.
|
||||
|
|
@ -1,12 +1,44 @@
|
|||
#include "settings_migration.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMap>
|
||||
#include <QSettings>
|
||||
#include <QStringList>
|
||||
#include <algorithm>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(SettingsMigrationLog, "settings_migration");
|
||||
|
||||
static const QString MIGRATION_SENTINEL_KEY = QStringLiteral("migration/perfile_complete");
|
||||
|
||||
/**
|
||||
* True when global.ini holds keys that migrateSettingsFromGlobalIni() would still move, i.e. the
|
||||
* exact condition that function uses to decide whether it has work to do. If a user runs an older
|
||||
* Cockatrice build that writes to global.ini (non-sentinel keys) and then upgrades back, those new
|
||||
* keys would otherwise be silently ignored, so they are re-migrated instead.
|
||||
*/
|
||||
static bool hasPendingGlobalIniSettings(const QString &settingsPath)
|
||||
{
|
||||
if (!QFile::exists(settingsPath + "global.ini")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QSettings globalIni(settingsPath + "global.ini", QSettings::IniFormat);
|
||||
if (!globalIni.value(MIGRATION_SENTINEL_KEY, false).toBool()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The sentinel is set, so any key besides it was written after the migration, i.e. by an
|
||||
// older build sharing this file. Those are exactly the keys to re-migrate.
|
||||
QStringList otherKeys = globalIni.allKeys();
|
||||
otherKeys.removeAll(MIGRATION_SENTINEL_KEY);
|
||||
return !otherKeys.isEmpty();
|
||||
}
|
||||
|
||||
static void migrateTabsSettings(const QString &settingsPath, QSettings &globalIni)
|
||||
{
|
||||
if (!globalIni.contains("tabs/visualDeckStorage") && !globalIni.contains("tabs/server") &&
|
||||
|
|
@ -658,21 +690,11 @@ bool SettingsMigration::migrateLegacySettings(const QString &settingsPath)
|
|||
|
||||
bool SettingsMigration::migrateSettingsFromGlobalIni(const QString &settingsPath)
|
||||
{
|
||||
if (!QFile::exists(settingsPath + "global.ini")) {
|
||||
if (!hasPendingGlobalIniSettings(settingsPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QSettings globalIni(settingsPath + "global.ini", QSettings::IniFormat);
|
||||
if (globalIni.value(MIGRATION_SENTINEL_KEY, false).toBool()) {
|
||||
// If a user runs an older Cockatrice build that writes to global.ini (non-sentinel keys)
|
||||
// and then upgrades back, those new keys will be silently ignored. Re-migrate them.
|
||||
globalIni.sync();
|
||||
auto allKeys = globalIni.allKeys();
|
||||
allKeys.removeAll(MIGRATION_SENTINEL_KEY);
|
||||
if (allKeys.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
migrateTabsSettings(settingsPath, globalIni);
|
||||
migrateSoundSettings(settingsPath, globalIni);
|
||||
|
|
@ -700,3 +722,78 @@ bool SettingsMigration::migrateSettingsFromGlobalIni(const QString &settingsPath
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SettingsMigration::isMigrationPending(const QString &settingsPath)
|
||||
{
|
||||
if (hasPendingGlobalIniSettings(settingsPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
QSettings personalIni(settingsPath + "personal.ini", QSettings::IniFormat);
|
||||
if (personalIni.value("migration/legacy_complete", false).toBool()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// migrateLegacySettings() also runs on an empty profile, where it only writes its sentinel.
|
||||
// Reporting that as pending would leave a backup folder behind on every fresh install, so
|
||||
// only count it when there really are legacy keys left to move.
|
||||
return !QSettings().allKeys().isEmpty();
|
||||
}
|
||||
|
||||
QString SettingsMigration::backupSettingsDirectory(const QString &settingsPath)
|
||||
{
|
||||
QDir settingsDir(QDir::cleanPath(settingsPath));
|
||||
if (!settingsDir.exists()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
const QDir::Filters fileFilters = QDir::Files | QDir::Hidden | QDir::System | QDir::NoDotAndDotDot;
|
||||
|
||||
// A profile holding nothing but empty files has nothing to lose, and backing it up would only
|
||||
// leave clutter next to the settings folder.
|
||||
const QFileInfoList files = settingsDir.entryInfoList(fileFilters);
|
||||
const bool hasContent =
|
||||
std::any_of(files.cbegin(), files.cend(), [](const QFileInfo &file) { return file.size() > 0; });
|
||||
if (!hasContent) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
// Keep the backup a sibling of the settings folder rather than a child: a folder that is never
|
||||
// copied into its own backup, and one the "Open settings folder" action does not have to skip.
|
||||
QDir backupParentDir(settingsDir.absolutePath());
|
||||
if (!backupParentDir.cdUp()) {
|
||||
qCWarning(SettingsMigrationLog) << "[SettingsMigration] Could not resolve the parent of the settings folder:"
|
||||
<< settingsDir.absolutePath();
|
||||
return QString();
|
||||
}
|
||||
|
||||
const QString backupName =
|
||||
QStringLiteral("settings-backup-") + QDateTime::currentDateTime().toString("yyyyMMdd-HHmmss");
|
||||
QString backupPath = backupParentDir.absoluteFilePath(backupName);
|
||||
// Two migrations inside the same second must not merge into a single folder.
|
||||
for (int suffix = 1; QFileInfo::exists(backupPath); ++suffix) {
|
||||
backupPath = backupParentDir.absoluteFilePath(backupName + '-' + QString::number(suffix));
|
||||
}
|
||||
|
||||
if (!QDir().mkpath(backupPath)) {
|
||||
qCWarning(SettingsMigrationLog) << "[SettingsMigration] Could not create the settings backup folder:"
|
||||
<< backupPath;
|
||||
return QString();
|
||||
}
|
||||
|
||||
QDir backupDir(backupPath);
|
||||
QDirIterator it(settingsDir.absolutePath(), fileFilters, QDirIterator::Subdirectories);
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
|
||||
const QString target = backupDir.absoluteFilePath(settingsDir.relativeFilePath(it.filePath()));
|
||||
QDir().mkpath(QFileInfo(target).absolutePath());
|
||||
if (!QFile::copy(it.filePath(), target)) {
|
||||
// A partially copied backup still beats none: report it and let the migration run.
|
||||
qCWarning(SettingsMigrationLog) << "[SettingsMigration] Could not back up settings file:" << it.filePath();
|
||||
}
|
||||
}
|
||||
|
||||
qCInfo(SettingsMigrationLog) << "[SettingsMigration] Settings backed up to" << backupPath;
|
||||
return backupPath;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,19 @@ class SettingsMigration
|
|||
public:
|
||||
static bool migrateSettingsFromGlobalIni(const QString &settingsPath);
|
||||
static bool migrateLegacySettings(const QString &settingsPath);
|
||||
|
||||
/**
|
||||
* Returns true when at least one of the two migrations would still do work, i.e. when a
|
||||
* backup of the settings directory is worth taking before they run.
|
||||
*/
|
||||
static bool isMigrationPending(const QString &settingsPath);
|
||||
|
||||
/**
|
||||
* Copies the whole settings directory to a timestamped sibling folder, so a migration that
|
||||
* overwrites per-domain files can be undone by hand. Returns the backup path, or an empty
|
||||
* string when there was nothing to back up or the copy failed. Never aborts the migration.
|
||||
*/
|
||||
static QString backupSettingsDirectory(const QString &settingsPath);
|
||||
};
|
||||
|
||||
#endif // SETTINGS_MIGRATION_H
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ set(UTILITY_SOURCES
|
|||
libcockatrice/utility/passwordhasher.cpp
|
||||
libcockatrice/utility/report_categories.cpp
|
||||
libcockatrice/utility/server_rate_limiter.cpp
|
||||
libcockatrice/utility/translation_loader.cpp
|
||||
libcockatrice/utility/warning_categories.cpp
|
||||
)
|
||||
|
||||
|
|
@ -32,6 +33,7 @@ set(UTILITY_HEADERS
|
|||
libcockatrice/utility/zone_names.h
|
||||
libcockatrice/utility/days_years_between.h
|
||||
libcockatrice/utility/server_rate_limiter.h
|
||||
libcockatrice/utility/translation_loader.h
|
||||
libcockatrice/utility/warning_categories.h
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,166 @@
|
|||
#include "translation_loader.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QLibraryInfo>
|
||||
#include <QRegularExpression>
|
||||
#include <QSet>
|
||||
#include <QTranslator>
|
||||
#include <algorithm>
|
||||
|
||||
// Kept under the historical category name so existing QT_LOGGING_RULES filters keep working.
|
||||
Q_LOGGING_CATEGORY(TranslationLoaderLog, "qt_translator")
|
||||
|
||||
QList<QString> TranslationLoader::qtTranslationCandidates()
|
||||
{
|
||||
QStringList candidates;
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||
// Qt 6.8 exposes every path listed in qt.conf, path() only returns the first one.
|
||||
candidates << QLibraryInfo::paths(QLibraryInfo::TranslationsPath);
|
||||
#else
|
||||
candidates << QLibraryInfo::path(QLibraryInfo::TranslationsPath);
|
||||
#endif
|
||||
|
||||
// A relocated Qt can report a prefix-relative path that does not exist, so probe the usual
|
||||
// install layouts as well instead of trusting that single report.
|
||||
const QString prefix = QLibraryInfo::path(QLibraryInfo::PrefixPath);
|
||||
const QString dataPath = QLibraryInfo::path(QLibraryInfo::DataPath);
|
||||
candidates << prefix + QStringLiteral("/share/qt6/translations")
|
||||
<< prefix + QStringLiteral("/share/qt/translations") << prefix + QStringLiteral("/translations")
|
||||
<< dataPath + QStringLiteral("/translations");
|
||||
|
||||
QList<QString> uniqueCandidates;
|
||||
for (const QString &candidate : candidates) {
|
||||
if (!candidate.isEmpty() && !uniqueCandidates.contains(candidate)) {
|
||||
uniqueCandidates << candidate;
|
||||
}
|
||||
}
|
||||
return uniqueCandidates;
|
||||
}
|
||||
|
||||
QList<QString> TranslationLoader::qtTranslationPaths()
|
||||
{
|
||||
QList<QString> existingPaths;
|
||||
for (const QString &candidate : qtTranslationCandidates()) {
|
||||
if (QFileInfo::exists(candidate)) {
|
||||
existingPaths << candidate;
|
||||
}
|
||||
}
|
||||
return existingPaths;
|
||||
}
|
||||
|
||||
QList<QString> TranslationLoader::applicationTranslationPaths(const QString &_installedPath)
|
||||
{
|
||||
// A build tree keeps the .qm files next to the executable, the installed layout puts them in
|
||||
// _installedPath. Searching both keeps a developer build translated as well.
|
||||
const QString executableDirectory = QCoreApplication::applicationDirPath();
|
||||
|
||||
QList<QString> paths;
|
||||
for (const QString &path : {_installedPath, executableDirectory}) {
|
||||
if (!path.isEmpty() && !paths.contains(path)) {
|
||||
paths << path;
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
QList<QString> TranslationLoader::availableLanguages(const QString &_prefix, const QList<QString> &_directories)
|
||||
{
|
||||
const QRegularExpression filePattern(
|
||||
QRegularExpression::anchoredPattern(QRegularExpression::escape(_prefix) + QStringLiteral("_(.*)\\.qm")));
|
||||
|
||||
QSet<QString> languages;
|
||||
for (const QString &directory : _directories) {
|
||||
if (directory.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
const QDir dir(directory);
|
||||
for (const QString &fileName : dir.entryList(QDir::Files)) {
|
||||
const auto match = filePattern.match(fileName);
|
||||
if (match.hasMatch()) {
|
||||
languages.insert(match.captured(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QList<QString> sortedLanguages(languages.cbegin(), languages.cend());
|
||||
std::sort(sortedLanguages.begin(), sortedLanguages.end());
|
||||
return sortedLanguages;
|
||||
}
|
||||
|
||||
TranslationLoadResult
|
||||
TranslationLoader::loadFrom(QTranslator &_translator, const QString &_nameHint, const QList<QString> &_directories)
|
||||
{
|
||||
bool anyDirectoryExists = false;
|
||||
|
||||
for (const QString &directory : _directories) {
|
||||
if (directory.isEmpty() || !QFileInfo::exists(directory)) {
|
||||
continue;
|
||||
}
|
||||
anyDirectoryExists = true;
|
||||
if (_translator.load(_nameHint, directory)) {
|
||||
return TranslationLoadResult::Loaded;
|
||||
}
|
||||
}
|
||||
|
||||
return anyDirectoryExists ? TranslationLoadResult::Missing : TranslationLoadResult::NoDirectory;
|
||||
}
|
||||
|
||||
TranslationLoadResult TranslationLoader::loadQt(QTranslator &_translator, const QString &_lang)
|
||||
{
|
||||
// An unset language is the default for English, which has no translation file to begin with.
|
||||
if (_lang.isEmpty()) {
|
||||
return TranslationLoadResult::Missing;
|
||||
}
|
||||
|
||||
const QString nameHint = QStringLiteral("qt_") + _lang;
|
||||
const auto result = loadFrom(_translator, nameHint, qtTranslationPaths());
|
||||
|
||||
switch (result) {
|
||||
case TranslationLoadResult::Loaded:
|
||||
qCInfo(TranslationLoaderLog) << "Loaded Qt translation" << nameHint << "at" << _translator.filePath();
|
||||
break;
|
||||
case TranslationLoadResult::Missing:
|
||||
qCDebug(TranslationLoaderLog)
|
||||
<< "No Qt translation" << nameHint << "in" << QStringList(qtTranslationPaths());
|
||||
break;
|
||||
case TranslationLoadResult::NoDirectory:
|
||||
qCWarning(TranslationLoaderLog)
|
||||
<< "No Qt translations directory exists, looked in" << QStringList(qtTranslationPaths());
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
TranslationLoadResult TranslationLoader::loadApplication(QTranslator &_translator,
|
||||
const QString &_prefix,
|
||||
const QString &_lang,
|
||||
const QList<QString> &_directories)
|
||||
{
|
||||
if (_lang.isEmpty()) {
|
||||
return TranslationLoadResult::Missing;
|
||||
}
|
||||
|
||||
const QString nameHint = _prefix + QLatin1Char('_') + _lang;
|
||||
const auto result = loadFrom(_translator, nameHint, _directories);
|
||||
|
||||
switch (result) {
|
||||
case TranslationLoadResult::Loaded:
|
||||
qCInfo(TranslationLoaderLog) << "Loaded" << _prefix << "translation" << nameHint << "at"
|
||||
<< _translator.filePath();
|
||||
break;
|
||||
case TranslationLoadResult::Missing:
|
||||
qCDebug(TranslationLoaderLog)
|
||||
<< "No" << _prefix << "translation" << nameHint << "in" << QStringList(_directories);
|
||||
break;
|
||||
case TranslationLoadResult::NoDirectory:
|
||||
qCWarning(TranslationLoaderLog)
|
||||
<< "No translation directory exists, looked in" << QStringList(_directories);
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
#ifndef TRANSLATION_LOADER_H
|
||||
#define TRANSLATION_LOADER_H
|
||||
|
||||
#include <QList>
|
||||
#include <QLoggingCategory>
|
||||
#include <QString>
|
||||
|
||||
class QTranslator;
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(TranslationLoaderLog)
|
||||
|
||||
/** @brief The outcome of a translation load attempt. */
|
||||
enum class TranslationLoadResult
|
||||
{
|
||||
Loaded, /**< A translation file was found and loaded into the translator. */
|
||||
Missing, /**< The search directories exist but hold no file for this language. */
|
||||
NoDirectory /**< Not a single search directory exists, so no lookup could be attempted. */
|
||||
};
|
||||
|
||||
namespace TranslationLoader
|
||||
{
|
||||
/**
|
||||
* @brief Every directory that may hold Qt's own translations, in order of preference.
|
||||
*
|
||||
* More than one entry is needed because a relocated Qt (Flatpak, AppImage, a bundled SDK) can report
|
||||
* a prefix-relative translations path that does not exist: the KDE runtime reports /usr/translations
|
||||
* while its qt_*.qm files live in /usr/share/qt6/translations.
|
||||
*/
|
||||
QList<QString> qtTranslationCandidates();
|
||||
|
||||
/**
|
||||
* @brief The candidates that actually exist, most likely first, so no caller searches a path that is
|
||||
* not there.
|
||||
*/
|
||||
QList<QString> qtTranslationPaths();
|
||||
|
||||
/**
|
||||
* @brief The directories to search for an application's own translation files, in order of preference.
|
||||
*
|
||||
* @param _installedPath The location the platform installs the translation files in, derived from the
|
||||
* executable's path.
|
||||
* @return The installed location, followed by the executable's own directory: a build tree keeps the
|
||||
* .qm files next to the binary instead of in the installed layout, so a developer build would
|
||||
* otherwise find no translations at all.
|
||||
*/
|
||||
QList<QString> applicationTranslationPaths(const QString &_installedPath);
|
||||
|
||||
/**
|
||||
* @brief Loads the first _nameHint + language translation file found in _directories into _translator.
|
||||
*
|
||||
* QTranslator discards whatever it held before every load, so each translation file needs its own
|
||||
* translator; loading a second file into the first one silently drops the first.
|
||||
*
|
||||
* @param _translator The translator to load into.
|
||||
* @param _nameHint The translation file name without its language and suffix, e.g. "qt_de" or "cockatrice_de".
|
||||
* @param _directories The directories to search, in order of preference.
|
||||
* @return What the search found.
|
||||
*/
|
||||
TranslationLoadResult loadFrom(QTranslator &_translator, const QString &_nameHint, const QList<QString> &_directories);
|
||||
|
||||
/**
|
||||
* @brief Loads Qt's own translation for _lang, which covers the strings of Qt's own dialogs.
|
||||
*
|
||||
* @param _translator The translator to load into; must not also be used for an application translation.
|
||||
* @param _lang The language code, e.g. "de". An empty language loads nothing.
|
||||
* @return What the search found.
|
||||
*/
|
||||
TranslationLoadResult loadQt(QTranslator &_translator, const QString &_lang);
|
||||
|
||||
/**
|
||||
* @brief The language codes of the <_prefix>_<lang>.qm files in _directories, sorted by code.
|
||||
*
|
||||
* Lets a language selector offer exactly the languages that can be loaded.
|
||||
*/
|
||||
QList<QString> availableLanguages(const QString &_prefix, const QList<QString> &_directories);
|
||||
|
||||
/**
|
||||
* @brief Loads the <_prefix>_<_lang>.qm translation file from _directories into _translator.
|
||||
*
|
||||
* @param _translator The translator to load into; must not also be used for Qt's own translation.
|
||||
* @param _prefix The translation file prefix, e.g. "cockatrice" or "oracle".
|
||||
* @param _lang The language code, e.g. "de". An empty language loads nothing.
|
||||
* @param _directories The directories to search, in order of preference.
|
||||
* @return What the search found.
|
||||
*/
|
||||
TranslationLoadResult loadApplication(QTranslator &_translator,
|
||||
const QString &_prefix,
|
||||
const QString &_lang,
|
||||
const QList<QString> &_directories);
|
||||
} // namespace TranslationLoader
|
||||
|
||||
#endif // TRANSLATION_LOADER_H
|
||||
|
|
@ -299,6 +299,18 @@ if(Qt6LinguistTools_FOUND)
|
|||
elseif(WIN32)
|
||||
install(FILES ${oracle_QM} DESTINATION ${ORACLE_WIN32_QM_INSTALL_DIR})
|
||||
endif()
|
||||
|
||||
# Qt attaches the .qm files to the default build target only, so building the executable on its own
|
||||
# (CLion, --target oracle) leaves it without any translations: nothing is translated and the
|
||||
# language selector in the wizard stays empty.
|
||||
# Depend on Qt's own lrelease target instead of attaching the .qm custom commands to a second
|
||||
# target: two independent targets owning the same outputs race on MSBuild/Makefiles and are an
|
||||
# error on Xcode. Older Qt versions already add this dependency themselves.
|
||||
foreach(_lrelease_target Oracle_lrelease oracle_lrelease)
|
||||
if(TARGET ${_lrelease_target})
|
||||
add_dependencies(oracle ${_lrelease_target})
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
qt6_finalize_target(oracle)
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@
|
|||
#include <QApplication>
|
||||
#include <QCommandLineParser>
|
||||
#include <QIcon>
|
||||
#include <QLibraryInfo>
|
||||
#include <QTimer>
|
||||
#include <QTranslator>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
#include <libcockatrice/utility/translation_loader.h>
|
||||
|
||||
QTranslator *translator, *qtTranslator;
|
||||
ThemeManager *themeManager;
|
||||
|
|
@ -24,24 +24,13 @@ void installNewTranslator()
|
|||
{
|
||||
QString lang = SettingsCache::instance().personal().getLang();
|
||||
|
||||
QString qtNameHint = "qt_" + lang;
|
||||
QString qtTranslationPath = QLibraryInfo::path(QLibraryInfo::TranslationsPath);
|
||||
|
||||
bool qtTranslationLoaded = qtTranslator->load(qtNameHint, qtTranslationPath);
|
||||
if (!qtTranslationLoaded) {
|
||||
qDebug() << "Unable to load qt translation" << qtNameHint << "at" << qtTranslationPath;
|
||||
} else {
|
||||
qDebug() << "Loaded qt translation" << qtNameHint << "at" << qtTranslationPath;
|
||||
}
|
||||
// Qt's own strings and ours need a translator each: QTranslator discards its previous contents
|
||||
// on every load, so loading both files into one translator silently drops the first of them.
|
||||
TranslationLoader::loadQt(*qtTranslator, lang);
|
||||
qApp->installTranslator(qtTranslator);
|
||||
|
||||
QString appNameHint = translationPrefix + "_" + lang;
|
||||
bool appTranslationLoaded = qtTranslator->load(appNameHint, translationPath);
|
||||
if (!appTranslationLoaded) {
|
||||
qDebug() << "Unable to load" << translationPrefix << "translation" << appNameHint << "at" << translationPath;
|
||||
} else {
|
||||
qDebug() << "Loaded" << translationPrefix << "translation" << appNameHint << "at" << translationPath;
|
||||
}
|
||||
TranslationLoader::loadApplication(*translator, translationPrefix, lang,
|
||||
TranslationLoader::applicationTranslationPaths(translationPath));
|
||||
qApp->installTranslator(translator);
|
||||
}
|
||||
|
||||
|
|
@ -82,8 +71,9 @@ int main(int argc, char *argv[])
|
|||
|
||||
QIcon icon("theme:appicon.svg");
|
||||
wizard.setWindowIcon(icon);
|
||||
// set name of the app desktop file; used by wayland to load the window icon
|
||||
QGuiApplication::setDesktopFileName("Oracle");
|
||||
// Base name of the installed oracle.desktop; wayland reads the window icon from it and
|
||||
// xdg-desktop-portal registers it as the app ID, so the case has to match the file name.
|
||||
QGuiApplication::setDesktopFileName("oracle");
|
||||
|
||||
wizard.show();
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
#include <QtGui>
|
||||
#include <cstdio>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
#include <libcockatrice/utility/translation_loader.h>
|
||||
|
||||
#ifdef HAS_LZMA
|
||||
#include "lzma/decompress.h"
|
||||
|
|
@ -212,21 +213,18 @@ void IntroPage::initializePage()
|
|||
|
||||
QStringList IntroPage::findQmFiles()
|
||||
{
|
||||
QDir dir(translationPath);
|
||||
QStringList fileNames = dir.entryList(QStringList(translationPrefix + "_*.qm"), QDir::Files, QDir::Name);
|
||||
fileNames.replaceInStrings(QRegularExpression(translationPrefix + "_(.*)\\.qm"), "\\1");
|
||||
return fileNames;
|
||||
return TranslationLoader::availableLanguages(translationPrefix,
|
||||
TranslationLoader::applicationTranslationPaths(translationPath));
|
||||
}
|
||||
|
||||
QString IntroPage::languageName(const QString &lang)
|
||||
{
|
||||
QTranslator qTranslator;
|
||||
|
||||
QString appNameHint = translationPrefix + "_" + lang;
|
||||
bool appTranslationLoaded = qTranslator.load(appNameHint, translationPath);
|
||||
if (!appTranslationLoaded) {
|
||||
qDebug() << "Unable to load" << translationPrefix << "translation" << appNameHint << "at" << translationPath;
|
||||
}
|
||||
// Only the language name is needed here, so the file is loaded without logging a line per
|
||||
// language: findQmFiles() asked for languages whose files exist in the first place.
|
||||
TranslationLoader::loadFrom(qTranslator, translationPrefix + "_" + lang,
|
||||
TranslationLoader::applicationTranslationPaths(translationPath));
|
||||
|
||||
return qTranslator.translate("i18n", DEFAULT_LANG_NAME);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ add_test(NAME server_rate_limiter_test COMMAND server_rate_limiter_test)
|
|||
add_test(NAME server_developer_role_test COMMAND server_developer_role_test)
|
||||
add_test(NAME server_game_join_test COMMAND server_game_join_test)
|
||||
add_test(NAME warning_categories_test COMMAND warning_categories_test)
|
||||
# The translation loader test compiles a .qm fixture, which needs lrelease from Qt Linguist.
|
||||
if(TARGET Qt6::lrelease)
|
||||
add_test(NAME translation_loader_test COMMAND translation_loader_test)
|
||||
endif()
|
||||
add_test(NAME report_categories_test COMMAND report_categories_test)
|
||||
add_test(NAME lag_monitor_test COMMAND lag_monitor_test)
|
||||
add_test(NAME latency_tracker_test COMMAND latency_tracker_test)
|
||||
|
|
@ -38,6 +42,34 @@ add_executable(server_rate_limiter_test server_rate_limiter_test.cpp)
|
|||
add_executable(server_developer_role_test server_developer_role_test.cpp)
|
||||
add_executable(server_game_join_test server_game_join_test.cpp)
|
||||
add_executable(warning_categories_test warning_categories_test.cpp)
|
||||
if(TARGET Qt6::lrelease)
|
||||
add_executable(translation_loader_test translation_loader_test.cpp)
|
||||
|
||||
# QTranslator only accepts real .qm files, so the translation loader test gets one compiled from its
|
||||
# own .ts fixture rather than a stub.
|
||||
set(TRANSLATION_LOADER_TEST_QM "${CMAKE_CURRENT_BINARY_DIR}/translation_loader_test_de.qm")
|
||||
add_custom_command(
|
||||
OUTPUT "${TRANSLATION_LOADER_TEST_QM}"
|
||||
COMMAND Qt6::lrelease "${CMAKE_CURRENT_SOURCE_DIR}/translation_loader_test.ts" -qm "${TRANSLATION_LOADER_TEST_QM}"
|
||||
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/translation_loader_test.ts"
|
||||
COMMENT "Compiling translation_loader_test.ts"
|
||||
)
|
||||
add_custom_target(translation_loader_test_qm DEPENDS "${TRANSLATION_LOADER_TEST_QM}")
|
||||
add_dependencies(translation_loader_test translation_loader_test_qm)
|
||||
target_compile_definitions(translation_loader_test PRIVATE TRANSLATION_LOADER_TEST_DIR="${CMAKE_CURRENT_BINARY_DIR}")
|
||||
|
||||
# A multi-config generator (MSBuild, Xcode) puts the executable in a per-config subdirectory while
|
||||
# the .qm stays in the current binary directory, so copy the fixture next to the binary: that is the
|
||||
# layout ABuildTreeFindsTheTranslationsNextToTheExecutable is about, and the only one it finds on
|
||||
# those generators.
|
||||
add_custom_command(
|
||||
TARGET translation_loader_test
|
||||
POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${TRANSLATION_LOADER_TEST_QM}"
|
||||
"$<TARGET_FILE_DIR:translation_loader_test>/"
|
||||
VERBATIM
|
||||
)
|
||||
endif()
|
||||
add_executable(report_categories_test report_categories_test.cpp)
|
||||
add_executable(lag_monitor_test ${CMAKE_SOURCE_DIR}/cockatrice/src/client/lag_monitor.cpp lag_monitor_test.cpp)
|
||||
target_include_directories(lag_monitor_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
||||
|
|
@ -104,6 +136,9 @@ if(NOT GTEST_FOUND)
|
|||
add_dependencies(server_developer_role_test gtest)
|
||||
add_dependencies(server_game_join_test gtest)
|
||||
add_dependencies(warning_categories_test gtest)
|
||||
if(TARGET translation_loader_test)
|
||||
add_dependencies(translation_loader_test gtest)
|
||||
endif()
|
||||
add_dependencies(report_categories_test gtest)
|
||||
add_dependencies(lag_monitor_test gtest)
|
||||
add_dependencies(latency_tracker_test gtest)
|
||||
|
|
@ -152,6 +187,11 @@ target_link_libraries(
|
|||
target_link_libraries(
|
||||
warning_categories_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
if(TARGET translation_loader_test)
|
||||
target_link_libraries(
|
||||
translation_loader_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
endif()
|
||||
target_link_libraries(
|
||||
report_categories_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,11 @@ protected:
|
|||
|
||||
void SetUp() override
|
||||
{
|
||||
settingsPath = tempDir.path() + "/";
|
||||
// Mirror the production layout: the settings folder lives inside the data folder, so
|
||||
// migration backups (siblings of settings/) stay isolated inside tempDir instead of
|
||||
// landing in the shared system temp directory.
|
||||
settingsPath = tempDir.path() + "/settings/";
|
||||
QDir().mkpath(settingsPath);
|
||||
|
||||
// Isolate the settings used by the legacy migration tests inside the temporary
|
||||
// directory so the tests never read or write the real user config (registry on
|
||||
|
|
@ -64,6 +68,25 @@ protected:
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The migration backups are timestamped, so tests address them by index rather than by name.
|
||||
QStringList backupFolderNames() const
|
||||
{
|
||||
return QDir(tempDir.path())
|
||||
.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name)
|
||||
.filter(QStringLiteral("settings-backup-"));
|
||||
}
|
||||
|
||||
QVariant readFromBackup(const QString &folderName, const QString &fileName, const QString &key) const
|
||||
{
|
||||
QSettings ini(QDir::cleanPath(tempDir.path() + '/' + folderName + '/' + fileName), QSettings::IniFormat);
|
||||
return ini.value(key);
|
||||
}
|
||||
|
||||
bool backupFileExists(const QString &folderName, const QString &fileName) const
|
||||
{
|
||||
return QFile::exists(QDir::cleanPath(tempDir.path() + '/' + folderName + '/' + fileName));
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(SettingsMigrationTest, NoGlobalIniDoesNothing)
|
||||
|
|
@ -725,6 +748,168 @@ TEST_F(SettingsMigrationTest, RenameFailureWithExistingBackup)
|
|||
ASSERT_FALSE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
|
||||
}
|
||||
|
||||
// --- pre-migration backup of the settings folder ---
|
||||
|
||||
TEST_F(SettingsMigrationTest, BackupHoldsPreMigrationStateOfPerDomainFiles)
|
||||
{
|
||||
// A user already migrated once, changed a setting, then ran an older build that wrote to
|
||||
// global.ini again. Re-migrating clobbers the per-domain file, so the backup has to hold the
|
||||
// pre-migration value.
|
||||
{
|
||||
QSettings tabs(settingsPath + "tabs.ini", QSettings::IniFormat);
|
||||
tabs.setValue("tabs/visualDeckStorage", false);
|
||||
tabs.sync();
|
||||
}
|
||||
{
|
||||
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
|
||||
g.setValue("tabs/visualDeckStorage", true);
|
||||
g.sync();
|
||||
}
|
||||
|
||||
const QString backupPath = SettingsMigration::backupSettingsDirectory(settingsPath);
|
||||
ASSERT_FALSE(backupPath.isEmpty());
|
||||
ASSERT_TRUE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
|
||||
|
||||
// The migration overwrote the user's change ...
|
||||
ASSERT_EQ(readFromIni("tabs.ini", "tabs/visualDeckStorage"), QVariant(true));
|
||||
|
||||
// ... but the backup still holds it, so it can be restored by hand.
|
||||
const QStringList backups = backupFolderNames();
|
||||
ASSERT_EQ(backups.size(), 1);
|
||||
ASSERT_EQ(readFromBackup(backups.at(0), "tabs.ini", "tabs/visualDeckStorage"), QVariant(false));
|
||||
ASSERT_EQ(readFromBackup(backups.at(0), "global.ini", "tabs/visualDeckStorage"), QVariant(true));
|
||||
}
|
||||
|
||||
TEST_F(SettingsMigrationTest, BackupIsASiblingOfTheSettingsFolder)
|
||||
{
|
||||
{
|
||||
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
|
||||
g.setValue("sound/enabled", true);
|
||||
g.sync();
|
||||
}
|
||||
|
||||
const QString backupPath = SettingsMigration::backupSettingsDirectory(settingsPath);
|
||||
ASSERT_FALSE(backupPath.isEmpty());
|
||||
|
||||
const QDir backupDir(backupPath);
|
||||
ASSERT_TRUE(backupDir.dirName().startsWith("settings-backup-"));
|
||||
// Same parent as the settings folder, and not inside it, so a backup is never copied into
|
||||
// another backup and "Open settings folder" shows only live files.
|
||||
ASSERT_EQ(QDir::cleanPath(backupDir.absolutePath() + "/.."), QDir::cleanPath(tempDir.path()));
|
||||
ASSERT_FALSE(backupDir.absolutePath().startsWith(QDir::cleanPath(settingsPath) + '/'));
|
||||
ASSERT_TRUE(backupFileExists(backupDir.dirName(), "global.ini"));
|
||||
}
|
||||
|
||||
TEST_F(SettingsMigrationTest, BackupCopiesNestedFolders)
|
||||
{
|
||||
ASSERT_TRUE(QDir().mkpath(settingsPath + "nested"));
|
||||
{
|
||||
QFile f(settingsPath + "nested/deep.ini");
|
||||
ASSERT_TRUE(f.open(QIODevice::WriteOnly));
|
||||
f.write("[visualDeckStorage]\nfirstDeck=1234\n");
|
||||
}
|
||||
{
|
||||
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
|
||||
g.setValue("sound/enabled", true);
|
||||
g.sync();
|
||||
}
|
||||
|
||||
const QString backupPath = SettingsMigration::backupSettingsDirectory(settingsPath);
|
||||
ASSERT_FALSE(backupPath.isEmpty());
|
||||
|
||||
const QStringList backups = backupFolderNames();
|
||||
ASSERT_EQ(backups.size(), 1);
|
||||
ASSERT_TRUE(backupFileExists(backups.at(0), "nested/deep.ini"));
|
||||
ASSERT_EQ(readFromBackup(backups.at(0), "nested/deep.ini", "visualDeckStorage/firstDeck"), QVariant("1234"));
|
||||
}
|
||||
|
||||
TEST_F(SettingsMigrationTest, NoBackupWhenSettingsFolderIsEmpty)
|
||||
{
|
||||
ASSERT_TRUE(SettingsMigration::backupSettingsDirectory(settingsPath).isEmpty());
|
||||
ASSERT_TRUE(backupFolderNames().isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(SettingsMigrationTest, NoBackupWhenEverySettingsFileIsEmpty)
|
||||
{
|
||||
// A fresh profile: QSettings has created global.ini, but it holds nothing worth preserving.
|
||||
{
|
||||
QFile f(settingsPath + "global.ini");
|
||||
ASSERT_TRUE(f.open(QIODevice::WriteOnly));
|
||||
f.close();
|
||||
}
|
||||
|
||||
ASSERT_TRUE(SettingsMigration::backupSettingsDirectory(settingsPath).isEmpty());
|
||||
ASSERT_TRUE(backupFolderNames().isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(SettingsMigrationTest, BackupFailsGracefullyOnMissingSettingsFolder)
|
||||
{
|
||||
ASSERT_TRUE(SettingsMigration::backupSettingsDirectory(tempDir.path() + "/does-not-exist/").isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(SettingsMigrationTest, BackupKeepsEveryPreviousSnapshot)
|
||||
{
|
||||
{
|
||||
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
|
||||
g.setValue("sound/enabled", true);
|
||||
g.sync();
|
||||
}
|
||||
|
||||
ASSERT_FALSE(SettingsMigration::backupSettingsDirectory(settingsPath).isEmpty());
|
||||
ASSERT_FALSE(SettingsMigration::backupSettingsDirectory(settingsPath).isEmpty());
|
||||
|
||||
// A second snapshot in the same second must not merge into the first one.
|
||||
const QStringList backups = backupFolderNames();
|
||||
ASSERT_EQ(backups.size(), 2);
|
||||
ASSERT_NE(backups.at(0), backups.at(1));
|
||||
}
|
||||
|
||||
TEST_F(SettingsMigrationTest, IsMigrationPendingWhenGlobalIniHasKeys)
|
||||
{
|
||||
{
|
||||
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
|
||||
g.setValue("sound/enabled", true);
|
||||
g.sync();
|
||||
}
|
||||
|
||||
ASSERT_TRUE(SettingsMigration::isMigrationPending(settingsPath));
|
||||
}
|
||||
|
||||
TEST_F(SettingsMigrationTest, IsNotMigrationPendingAfterSentinelIsSet)
|
||||
{
|
||||
{
|
||||
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
|
||||
g.setValue("sound/enabled", true);
|
||||
g.sync();
|
||||
}
|
||||
ASSERT_TRUE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
|
||||
|
||||
// The sentinel-only global.ini and the legacy sentinel leave nothing to migrate.
|
||||
{
|
||||
QSettings personal(settingsPath + "personal.ini", QSettings::IniFormat);
|
||||
personal.setValue("migration/legacy_complete", true);
|
||||
personal.sync();
|
||||
}
|
||||
ASSERT_FALSE(SettingsMigration::isMigrationPending(settingsPath));
|
||||
}
|
||||
|
||||
TEST_F(SettingsMigrationTest, IsMigrationPendingWhenOnlyLegacySettingsRemain)
|
||||
{
|
||||
QSettings().setValue("server/host", "example.com");
|
||||
QSettings().sync();
|
||||
|
||||
ASSERT_TRUE(SettingsMigration::isMigrationPending(settingsPath));
|
||||
}
|
||||
|
||||
TEST_F(SettingsMigrationTest, IsNotMigrationPendingWithoutLegacyData)
|
||||
{
|
||||
// Nothing to migrate: a fresh profile must not leave a backup folder behind on every start.
|
||||
QSettings().clear();
|
||||
QSettings().sync();
|
||||
|
||||
ASSERT_FALSE(SettingsMigration::isMigrationPending(settingsPath));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
|
|
|
|||
262
tests/translation_loader_test.cpp
Normal file
262
tests/translation_loader_test.cpp
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
#include "gtest/gtest.h"
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QLibraryInfo>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QTemporaryDir>
|
||||
#include <QTranslator>
|
||||
#include <libcockatrice/utility/translation_loader.h>
|
||||
|
||||
// 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
|
||||
|
||||
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, 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();
|
||||
}
|
||||
15
tests/translation_loader_test.ts
Normal file
15
tests/translation_loader_test.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1" language="de">
|
||||
<context>
|
||||
<name>TranslationLoaderTest</name>
|
||||
<message>
|
||||
<source>Load into the first translator</source>
|
||||
<translation>In den ersten Übersetzer laden</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Load into the second translator</source>
|
||||
<translation>In den zweiten Übersetzer laden</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
Loading…
Add table
Add a link
Reference in a new issue