Merge branch 'master' into tooomm-qt5

This commit is contained in:
tooomm 2026-10-04 12:27:58 +02:00 • committed by GitHub
commit 94b85b8940
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
266 changed files with 11392 additions and 930 deletions

View file

@ -6,8 +6,13 @@ set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
set(UTILITY_SOURCES
libcockatrice/utility/cryptoutil.cpp libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp
libcockatrice/utility/passwordhasher.cpp libcockatrice/utility/server_rate_limiter.cpp
libcockatrice/utility/cryptoutil.cpp
libcockatrice/utility/expression.cpp
libcockatrice/utility/levenshtein.cpp
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
)
@ -20,6 +25,7 @@ set(UTILITY_HEADERS
libcockatrice/utility/macros.h
libcockatrice/utility/passwordhasher.h
libcockatrice/utility/playmat_params.h
libcockatrice/utility/report_categories.h
libcockatrice/utility/string_limits.h
libcockatrice/utility/dice_limits.h
libcockatrice/utility/counter_limits.h
@ -27,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
)

View file

@ -0,0 +1,16 @@
#include "report_categories.h"
namespace ReportCategories
{
const QStringList &keys()
{
static const QStringList categories = {"cheating", "bug_abuse", "harassment", "verbal_abuse",
"hate_speech", "spam", "other"};
return categories;
}
bool isValid(const QString &key)
{
return keys().contains(key, Qt::CaseInsensitive);
}
} // namespace ReportCategories

View file

@ -0,0 +1,24 @@
#ifndef REPORT_CATEGORIES_H
#define REPORT_CATEGORIES_H
#include <QString>
#include <QStringList>
/**
* Canonical report categories, shared by the client and the server.
*
* This is the single source of truth for the category keys exchanged over the
* wire (Command_Report.category) and stored in the reports table. The server
* only accepts keys from this list and the client report dialog only offers
* keys from this list, so the two sides cannot drift apart again.
*/
namespace ReportCategories
{
/** @brief The complete list of valid category keys, in canonical order. */
const QStringList &keys();
/** @brief Whether @a key is a valid report category (case-insensitive). */
bool isValid(const QString &key);
} // namespace ReportCategories
#endif // REPORT_CATEGORIES_H

View file

@ -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;
}

View file

@ -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