mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Translation] Name languages without reading the translation files The language selector derived every label from the language's own .qm file: it loaded the file and translated the literal "English" in the "i18n" context. lupdate could never extract that literal from the callers, which pass the DEFAULT_LANG_NAME macro, so a dummy QT_TRANSLATE_NOOP in cache_settings.cpp existed to give it something to extract. #7050 deleted that dummy, the next lupdate push dropped the string from Transifex, and the following pull rewrote the .ts files without it. lrelease only emits translated messages, so 22 of 25 .qm files returned an empty name. The names are data, not translations: a selector has to be readable before any translation is loaded, and a label must not depend on the translation of the language it names. TranslationLoader::languageDisplayName now holds them next to availableLanguages, falling back to Qt's native language name and then to the code, so a language added later still gets a label. Also select the configured language by item data instead of by matching the translated "English" string, which pointed at the wrong row whenever the lookup came back empty, and restore that English entry's own name: en_US is "English (United States)" so it can no longer be confused with the untranslated default. Oracle shared the code path, so its wizard language page is fixed too. * [Translation] Name the default language selection once Review follow-up: log the unresolvable language through the page's own category, and move the en_US entry both selectors fall back to for an unset setting into TranslationLoader::defaultLanguageCode. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
232 lines
9.2 KiB
C++
232 lines
9.2 KiB
C++
#include "translation_loader.h"
|
|
|
|
#include <QChar>
|
|
#include <QCoreApplication>
|
|
#include <QDebug>
|
|
#include <QDir>
|
|
#include <QFileInfo>
|
|
#include <QHash>
|
|
#include <QLatin1Char>
|
|
#include <QLibraryInfo>
|
|
#include <QLocale>
|
|
#include <QMessageLogger>
|
|
#include <QRegularExpression>
|
|
#include <QRegularExpressionMatch>
|
|
#include <QSet>
|
|
#include <QSharedPointer>
|
|
#include <QStringList>
|
|
#include <QStringLiteral>
|
|
#include <QTranslator>
|
|
#include <QtGlobal>
|
|
#include <algorithm>
|
|
#include <initializer_list>
|
|
|
|
// 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;
|
|
}
|
|
|
|
QString TranslationLoader::languageDisplayName(const QString &_lang)
|
|
{
|
|
if (_lang.isEmpty()) {
|
|
return QString();
|
|
}
|
|
|
|
// The native name first, so a speaker finds their own language, then the English one, so everyone
|
|
// else does. A language shipped with a .qm file belongs in here: the names used to come from the
|
|
// translation files, where a missing translation left the selector blank.
|
|
static const QHash<QString, QString> displayNames = {
|
|
{QStringLiteral("ca"), QStringLiteral("Català (Catalan)")},
|
|
{QStringLiteral("cs"), QStringLiteral("Česky (Czech)")},
|
|
{QStringLiteral("de"), QStringLiteral("Deutsch (German)")},
|
|
{QStringLiteral("el"), QStringLiteral("Ελληνικά (Greek)")},
|
|
{QStringLiteral("en@pirate"), QStringLiteral("English, arr! (Pirate English)")},
|
|
{QStringLiteral("en_US"), QStringLiteral("English (United States)")},
|
|
{QStringLiteral("es"), QStringLiteral("Español (Spanish)")},
|
|
{QStringLiteral("et"), QStringLiteral("Eesti Keel (Estonian)")},
|
|
{QStringLiteral("fi"), QStringLiteral("Suomi (Finnish)")},
|
|
{QStringLiteral("fr"), QStringLiteral("Français (French)")},
|
|
{QStringLiteral("hu"), QStringLiteral("Magyar (Hungarian)")},
|
|
{QStringLiteral("it"), QStringLiteral("Italiano (Italian)")},
|
|
{QStringLiteral("ja"), QStringLiteral("日本語 (Japanese)")},
|
|
{QStringLiteral("ko"), QStringLiteral("한국어 (Korean)")},
|
|
{QStringLiteral("nb"), QStringLiteral("Norsk Bokmål (Norwegian Bokmål)")},
|
|
{QStringLiteral("nl"), QStringLiteral("Nederlands (Dutch)")},
|
|
{QStringLiteral("pl"), QStringLiteral("Polski (Polish)")},
|
|
{QStringLiteral("pt"), QStringLiteral("Português (Portuguese)")},
|
|
{QStringLiteral("pt_BR"), QStringLiteral("Português do Brasil (Brazilian Portuguese)")},
|
|
{QStringLiteral("ru"), QStringLiteral("Русский (Russian)")},
|
|
{QStringLiteral("sr"), QStringLiteral("Srpski (Serbian)")},
|
|
{QStringLiteral("sv"), QStringLiteral("Svenska (Swedish)")},
|
|
{QStringLiteral("tr"), QStringLiteral("Türkçe (Turkish)")},
|
|
{QStringLiteral("yue"), QStringLiteral("粵語 (Cantonese)")},
|
|
{QStringLiteral("zh-Hans"), QStringLiteral("简体中文 (Chinese Simplified)")},
|
|
{QStringLiteral("zh-Hant"), QStringLiteral("繁體中文 (Chinese Traditional)")},
|
|
};
|
|
|
|
const QString displayName = displayNames.value(_lang);
|
|
if (!displayName.isEmpty()) {
|
|
return displayName;
|
|
}
|
|
|
|
// A language added after this table still gets a label instead of the blank one this function exists
|
|
// to prevent. Qt knows no name for an unknown code and answers with an empty string.
|
|
const QString nativeName = QLocale(_lang).nativeLanguageName();
|
|
return nativeName.isEmpty() ? _lang : nativeName;
|
|
}
|
|
|
|
QString TranslationLoader::defaultLanguageCode()
|
|
{
|
|
return QStringLiteral("en_US");
|
|
}
|
|
|
|
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;
|
|
}
|