mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
[Translations] Load Qt's own translations and build ours with the binary (#7384)
* [Translations] Load Qt's own translations and build ours with the binary Three separate bugs kept translations from working. installNewTranslator() loaded the application translation into qtTranslator. QTranslator discards its previous contents on every load, so that wiped the qt_<lang>.qm loaded one line earlier and installed an empty translator instead. Qt's own strings were never translated on any platform. Give each file its own translator. QLibraryInfo::path(TranslationsPath) was the only directory searched for Qt's translations. A relocated Qt can report a path that does not exist: the KDE Flatpak runtime reports /usr/translations while its files live in /usr/share/qt6/translations. Probe the usual install layouts and only hand back the ones that exist. The .qm files were attached to the default build target only, so building the executable on its own left it without any translations and the language selector empty. Attach them to the target as well. Move the shared lookup into TranslationLoader so client and Oracle cannot drift again, and have it report a missing translation at debug level. An unset language is the default for English, which has no file to begin with, and warning about it only added noise to every fresh debug log. Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de> * [Tests] Define main() in translation_loader_test Without a GTest installation the fallback in tests/CMakeLists.txt links only gtest, not gtest_main, so the test has to bring its own main(). Every other test already does; only this one relied on the distribution package and failed to link on CI. Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de> * [Tests] Put the translation loader fixture next to the test executable A multi-config generator (MSBuild, Xcode) writes the executable to a per-config subdirectory while the .qm stays in the current binary directory, so applicationDirPath() never saw the fixture and ABuildTreeFindsTheTranslationsNextToTheExecutable failed on Windows. * [Translations] Depend on Qt's lrelease target and skip the loader test without lrelease Attaching the .qm custom commands to a second custom target races with Qt's own <Project>_lrelease target on MSBuild/Makefiles and is rejected by Xcode, so depend on Qt's target instead. Only build the translation loader test when Qt6::lrelease is available. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de> Co-authored-by: ZeldaZach <zahalpern+github@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3aba1ef929
commit
0934380a5c
13 changed files with 627 additions and 56 deletions
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue