mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 09:05:10 -07:00
[Client] Show localized card names, texts and pictures
Localization wiring now runs end to end: the oracle importer collects
foreignData for the configured language and the client renders it.
- [Oracle] Import localized names and rules texts for the selected cardLang
- single-face cards store their foreignData name and full text
- multi-face (split/adventure/aftermath/prepare) cards collect the joined
name once and join each face's translated text with the same separator
as the English merge; an incomplete translation falls back to English;
the joined text follows the same highest-priority-set policy as the
single-face path and is only collected when localization is enabled
- the wizard switching languages re-imports the card database
- [Client] Display localized card info throughout the client
- card info text/picture widgets and the game board re-render on language
change
- pictures resolve cardLang art through Scryfall's named endpoint using the
localized name, falling back to id-based art when no match exists
- deck editor keeps canonical English names as card identity (EditRole)
while showing localized names (DisplayRole), so decks and wire names
stay stable
- [Card] Add CardLocalization-backed name/text lookup and cards.xml v4
localization elements with a bounded-size translation cache
- [Tests] Cover oracle foreignData import (incl. multi-face joins, priority
and fallback paths), XML v4 localization parsing, deck model localized
display and the language-aware settings default
Existing installations need to re-run Oracle to see translations: localized
data only lands in cards.xml when the Oracle app is started with the
preferred language selected — launch the separate "Oracle" program that
ships with Cockatrice, pick the language in the wizard and let it re-import
the card database.
The client's database cache (cards.xml.cache) is invalidated by the cache
format bump and the source-hash checks, but a cache written before the
re-import can still hold English-only entries (the hash uses file size and
mtime, so a same-size/same-timestamp rewrite may be served as-is); delete
cards.xml.cache and relaunch if no localized names/texts show up after
re-importing.
This commit is contained in:
parent
fd82b140a8
commit
8bb2337117
35 changed files with 1138 additions and 25 deletions
|
|
@ -1,6 +1,7 @@
|
|||
#include "abstract_card_item.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../../interface/card_localization.h"
|
||||
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
||||
#include "../game_scene.h"
|
||||
#include "../z_values.h"
|
||||
|
|
@ -26,6 +27,8 @@ AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef
|
|||
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::displayCardNamesChanged, this,
|
||||
[this] { update(); });
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
|
||||
[this] { update(); });
|
||||
refreshCardInfo();
|
||||
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
|
||||
|
|
@ -171,7 +174,7 @@ void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedS
|
|||
if (SettingsCache::instance().debug().getShowCardId()) {
|
||||
prefix = "#" + QString::number(id) + " ";
|
||||
}
|
||||
nameStr = prefix + cardRef.name;
|
||||
nameStr = prefix + LocalizedCard::displayName(getCardInfo());
|
||||
}
|
||||
painter->drawText(QRectF(3 * scaleFactor, 3 * scaleFactor, translatedSize.width() - 6 * scaleFactor,
|
||||
translatedSize.height() - 6 * scaleFactor),
|
||||
|
|
|
|||
59
cockatrice/src/interface/card_localization.h
Normal file
59
cockatrice/src/interface/card_localization.h
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
#ifndef COCKATRICE_CARD_LOCALIZATION_H
|
||||
#define COCKATRICE_CARD_LOCALIZATION_H
|
||||
|
||||
#include "../client/settings/cache_settings.h"
|
||||
|
||||
#include <QString>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
|
||||
namespace LocalizedCard
|
||||
{
|
||||
/**
|
||||
* @brief The language code selected for localized card text and images.
|
||||
*/
|
||||
inline QString displayLang()
|
||||
{
|
||||
return SettingsCache::instance().cardsDisplay().getCardLang();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Card name in the configured display language, falling back to English.
|
||||
* @param card The card to display.
|
||||
* @return The localized name, or an empty string for a null card.
|
||||
*/
|
||||
inline QString displayName(const CardInfoPtr &card)
|
||||
{
|
||||
return card.isNull() ? QString() : card->getLocalizedName(displayLang());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Card rules text in the configured display language, falling back to English.
|
||||
* @param card The card to display.
|
||||
* @return The localized text, or an empty string for a null card.
|
||||
*/
|
||||
inline QString displayText(const CardInfoPtr &card)
|
||||
{
|
||||
return card.isNull() ? QString() : card->getLocalizedText(displayLang());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Card name in the configured display language, falling back to English.
|
||||
* @param card The card to display.
|
||||
*/
|
||||
inline QString displayName(const CardInfo &card)
|
||||
{
|
||||
return card.getLocalizedName(displayLang());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Card rules text in the configured display language, falling back to English.
|
||||
* @param card The card to display.
|
||||
*/
|
||||
inline QString displayText(const CardInfo &card)
|
||||
{
|
||||
return card.getLocalizedText(displayLang());
|
||||
}
|
||||
} // namespace LocalizedCard
|
||||
|
||||
#endif // COCKATRICE_CARD_LOCALIZATION_H
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
#include <QThread>
|
||||
#include <algorithm>
|
||||
#include <libcockatrice/settings/cache_storage_settings.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <libcockatrice/settings/download_settings.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <utility>
|
||||
|
|
@ -37,6 +38,8 @@ CardPictureLoader::CardPictureLoader() : QObject(nullptr)
|
|||
&CardPictureLoader::picsPathChanged);
|
||||
connect(&SettingsCache::instance().downloads(), &DownloadSettings::picDownloadChanged, this,
|
||||
&CardPictureLoader::picDownloadChanged);
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
|
||||
&CardPictureLoader::cardLangChanged);
|
||||
|
||||
qRegisterMetaType<ExactCard>();
|
||||
connect(worker, &CardPictureLoaderWorker::imageLoaded, this, &CardPictureLoader::imageLoaded);
|
||||
|
|
@ -327,6 +330,15 @@ void CardPictureLoader::picsPathChanged()
|
|||
QPixmapCache::clear();
|
||||
}
|
||||
|
||||
void CardPictureLoader::cardLangChanged()
|
||||
{
|
||||
// Localized images are fetched via a different URL, but the in-memory
|
||||
// pixmap cache is keyed by card name/uuid, so drop everything cached
|
||||
// (including failure timestamps) to force a reload in the new language.
|
||||
QPixmapCache::clear();
|
||||
failedAt.clear();
|
||||
}
|
||||
|
||||
bool CardPictureLoader::hasCustomArt()
|
||||
{
|
||||
auto picsPath = SettingsCache::instance().paths().getPicsPath();
|
||||
|
|
|
|||
|
|
@ -134,6 +134,12 @@ private slots:
|
|||
* Clears the QPixmap cache to reload images.
|
||||
*/
|
||||
void picsPathChanged();
|
||||
|
||||
/**
|
||||
* @brief Triggered when the card language setting changes.
|
||||
* Clears the in-memory picture caches so images reload in the new language.
|
||||
*/
|
||||
void cardLangChanged();
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -94,7 +94,20 @@ void CardPictureToLoad::populateSetUrls()
|
|||
}
|
||||
}
|
||||
|
||||
for (const QString &urlTemplate : urlTemplates) {
|
||||
QStringList orderedTemplates = urlTemplates;
|
||||
if (SettingsCache::instance().cardsDisplay().getCardLang() != "en") {
|
||||
// Scryfall serves localized art from per-language printings, which have
|
||||
// their own ids. The ids stored in the card database belong to the
|
||||
// English prints, so id-based templates (cards.scryfall.io,
|
||||
// api.scryfall.com/cards/!set:uuid!) can only ever resolve English
|
||||
// scans; the `lang=` parameter is ignored on them. Resolve the localized
|
||||
// printing by its translated name (and the language code) ahead of the
|
||||
// id-based templates instead.
|
||||
orderedTemplates.prepend("https://api.scryfall.com/cards/named?fuzzy=!localizedName!&lang=!sflang!"
|
||||
"&format=image&face=!prop:side!");
|
||||
}
|
||||
|
||||
for (const QString &urlTemplate : orderedTemplates) {
|
||||
QString transformedUrl = transformUrl(urlTemplate);
|
||||
|
||||
if (!transformedUrl.isEmpty()) {
|
||||
|
|
@ -282,8 +295,15 @@ QString CardPictureToLoad::transformUrl(const QString &urlTemplate) const
|
|||
}
|
||||
|
||||
// language setting
|
||||
transformMap["!sflang!"] = QString(QCoreApplication::translate(
|
||||
"PictureLoader", "en", "code for scryfall's language property, not available for all languages"));
|
||||
const QString cardLang = SettingsCache::instance().cardsDisplay().getCardLang();
|
||||
transformMap["!sflang!"] = cardLang;
|
||||
|
||||
// The localized printing's own id is unknown, so Scryfall must resolve it by
|
||||
// its translated name (see populateSetUrls); expose that name for the
|
||||
// `/cards/named` template.
|
||||
if (cardLang != "en") {
|
||||
transformMap["!localizedName!"] = card.getInfo().getLocalizedName(cardLang);
|
||||
}
|
||||
|
||||
QString transformedUrl = urlTemplate;
|
||||
for (const QString &prop : transformMap.keys()) {
|
||||
|
|
|
|||
|
|
@ -74,6 +74,8 @@ CardInfoPictureWidget::CardInfoPictureWidget(QWidget *parent, const bool _hoverT
|
|||
|
||||
update();
|
||||
});
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
|
||||
&CardInfoPictureWidget::updatePixmap);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "card_info_text_widget.h"
|
||||
|
||||
#include "../../../game_graphics/board/card_item.h"
|
||||
#include "../../card_localization.h"
|
||||
|
||||
#include <QGridLayout>
|
||||
#include <QLabel>
|
||||
|
|
@ -39,6 +40,12 @@ CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent), info(n
|
|||
grid->setRowStretch(1, 1);
|
||||
|
||||
retranslateUi();
|
||||
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this, [this] {
|
||||
if (currentCard) {
|
||||
setCard(currentCard);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void CardInfoTextWidget::setTexts(const QString &propsText, const QString &textText)
|
||||
|
|
@ -60,7 +67,7 @@ void CardInfoTextWidget::setCard(const ExactCard &exactCard)
|
|||
|
||||
QString text = "<table width=\"100%\" border=0 cellspacing=0 cellpadding=0>";
|
||||
text += QString("<tr><td>%1</td><td width=\"5\"></td><td>%2</td></tr>")
|
||||
.arg(tr("Name:"), card->getName().toHtmlEscaped());
|
||||
.arg(tr("Name:"), LocalizedCard::displayName(card).toHtmlEscaped());
|
||||
|
||||
if (!exactCard.getPrinting().isEmpty()) {
|
||||
QString setShort = exactCard.getPrinting().getSet()->getShortName().toHtmlEscaped();
|
||||
|
|
@ -94,7 +101,8 @@ void CardInfoTextWidget::setCard(const ExactCard &exactCard)
|
|||
}
|
||||
|
||||
text += "</table>";
|
||||
setTexts(text, card->getText());
|
||||
setTexts(text, LocalizedCard::displayText(card));
|
||||
currentCard = exactCard;
|
||||
}
|
||||
|
||||
void CardInfoTextWidget::setInvalidCardName(const QString &cardName)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ private:
|
|||
QScrollArea *propsScroll;
|
||||
QTextEdit *textLabel;
|
||||
CardInfoPtr info;
|
||||
ExactCard currentCard; ///< Last card set, re-rendered when the card language changes.
|
||||
void setTexts(const QString &propsText, const QString &textText);
|
||||
|
||||
public:
|
||||
|
|
|
|||
|
|
@ -345,7 +345,9 @@ ExactCard DeckEditorDeckDockWidget::getCurrentCard()
|
|||
if (!current.isValid()) {
|
||||
return {};
|
||||
}
|
||||
const QString cardName = current.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString();
|
||||
// The display role holds the localized card name; the edit role always carries the
|
||||
// canonical English name needed to look the card up in the database.
|
||||
const QString cardName = current.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
const QString cardProviderID = current.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString();
|
||||
const QModelIndex gparent = current.parent().parent();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,20 @@
|
|||
#include "deck_state_manager.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/deck_list_history_manager.h>
|
||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
|
||||
DeckStateManager::DeckStateManager(QObject *parent)
|
||||
: QObject(parent), deckList(QSharedPointer<DeckList>(new DeckList)),
|
||||
deckListModel(new DeckListModel(this, deckList)), historyManager(new DeckListHistoryManager(this))
|
||||
{
|
||||
deckListModel->setDisplayLanguage(SettingsCache::instance().cardsDisplay().getCardLang());
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, deckListModel,
|
||||
[this](const QString &lang) { deckListModel->setDisplayLanguage(lang); });
|
||||
|
||||
connect(historyManager, &DeckListHistoryManager::undoRedoStateChanged, this, [this] {
|
||||
setModified(true);
|
||||
emit historyChanged();
|
||||
|
|
@ -260,7 +267,10 @@ bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx)
|
|||
return false;
|
||||
}
|
||||
|
||||
QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString();
|
||||
// The display role holds the localized card name; the edit role always carries the
|
||||
// canonical English name needed to look the card up in the database.
|
||||
QString displayCardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString();
|
||||
QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
QString providerId = idx.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString();
|
||||
QModelIndex gparent = idx.parent().parent();
|
||||
|
||||
|
|
@ -277,7 +287,7 @@ bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx)
|
|||
|
||||
QString reason = tr("Moved to %1 1 × \"%2\" (%3)") //
|
||||
.arg(otherZoneName)
|
||||
.arg(cardName)
|
||||
.arg(displayCardName)
|
||||
.arg(providerId);
|
||||
|
||||
return modifyDeck(reason, [&idx, &cardName, &providerId, &otherZoneName](auto model) {
|
||||
|
|
@ -291,9 +301,8 @@ bool DeckStateManager::removeCardAtIndex(const QModelIndex &idx)
|
|||
return false;
|
||||
}
|
||||
|
||||
QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString();
|
||||
|
||||
QString reason = tr("Removed \"%1\" (all copies)").arg(cardName);
|
||||
QString reason =
|
||||
tr("Removed \"%1\" (all copies)").arg(idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString());
|
||||
|
||||
return modifyDeck(reason, [&idx](auto model) { return model->removeRow(idx.row(), idx.parent()); });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -416,6 +416,11 @@ void DlgSettings::setTab(int index)
|
|||
}
|
||||
}
|
||||
|
||||
AbstractSettingsPage *DlgSettings::page(SettingsPage which) const
|
||||
{
|
||||
return pages.value(static_cast<int>(which));
|
||||
}
|
||||
|
||||
void DlgSettings::updateLanguage()
|
||||
{
|
||||
qApp->removeTranslator(translator); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ public:
|
|||
|
||||
explicit DlgSettings(QWidget *parent = nullptr);
|
||||
void setTab(int index);
|
||||
AbstractSettingsPage *page(SettingsPage which) const;
|
||||
|
||||
private slots:
|
||||
void onTabClicked(int index);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
#include "general_settings_page.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../interface/card_picture_loader/card_picture_loader.h"
|
||||
#include "../main.h"
|
||||
#include "../server/user/user_info_connection.h"
|
||||
#include "update/client/release_channel.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QFile>
|
||||
#include <QFileDialog>
|
||||
#include <QGridLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QTranslator>
|
||||
#include <libcockatrice/card/card_localization.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
#include <libcockatrice/settings/tabs_settings.h>
|
||||
|
|
@ -46,10 +51,27 @@ GeneralSettingsPage::GeneralSettingsPage()
|
|||
connect(&languageBox, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||||
&GeneralSettingsPage::languageBoxChanged);
|
||||
|
||||
// card text & images language, independent of the UI language
|
||||
cardLanguageBox.addItem(tr("English"), "en");
|
||||
for (const QString &code : CardLocalization::supportedLanguages()) {
|
||||
cardLanguageBox.addItem(CardLocalization::languageDisplayName(code), code);
|
||||
}
|
||||
const int cardLangIndex = cardLanguageBox.findData(SettingsCache::instance().cardsDisplay().getCardLang());
|
||||
cardLanguageBox.setCurrentIndex(cardLangIndex < 0 ? 0 : cardLangIndex);
|
||||
|
||||
connect(&cardLanguageBox, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||||
&GeneralSettingsPage::cardLanguageBoxChanged);
|
||||
|
||||
auto *languageGrid = new QGridLayout;
|
||||
languageGrid->addWidget(&languageLabel, 0, 0);
|
||||
languageGrid->addWidget(&languageBox, 0, 1);
|
||||
languageGrid->addWidget(&advertiseTranslationPageLabel, 1, 1, Qt::AlignRight);
|
||||
languageGrid->addWidget(&cardLanguageLabel, 1, 0);
|
||||
languageGrid->addWidget(&cardLanguageBox, 1, 1);
|
||||
languageGrid->addWidget(&cardLanguageNoteLabel, 2, 1);
|
||||
languageGrid->addWidget(&advertiseTranslationPageLabel, 3, 1, Qt::AlignRight);
|
||||
|
||||
cardLanguageNoteLabel.setWordWrap(true);
|
||||
cardLanguageNoteLabel.setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||
|
||||
languageGroupBox = new QGroupBox;
|
||||
languageGroupBox->setLayout(languageGrid);
|
||||
|
|
@ -412,6 +434,43 @@ void GeneralSettingsPage::languageBoxChanged(int index)
|
|||
SettingsCache::instance().personal().setLang(languageBox.itemData(index).toString());
|
||||
}
|
||||
|
||||
void GeneralSettingsPage::cardLanguageBoxChanged(int index)
|
||||
{
|
||||
const QString lang = cardLanguageBox.itemData(index).toString();
|
||||
SettingsCache::instance().cardsDisplay().setCardLang(lang);
|
||||
|
||||
// Switching to a non-default language only takes effect after the card
|
||||
// database is re-imported with that language selected; English data is always
|
||||
// present, so switching back to English needs no prompt.
|
||||
if (lang == "en") {
|
||||
return;
|
||||
}
|
||||
|
||||
// The binary cache does not track the language its entries were imported in,
|
||||
// and the downloaded pictures were fetched with English art names, so both are
|
||||
// stale until Oracle re-imports the database in the new language: drop them.
|
||||
QFile::remove(SettingsCache::instance().getCardDatabasePath() + ".cache");
|
||||
CardPictureLoader::clearNetworkCache();
|
||||
CardPictureLoader::clearPixmapCache();
|
||||
|
||||
const QMessageBox::StandardButton answer =
|
||||
QMessageBox::question(this, tr("Card text & images language changed"),
|
||||
tr("<p>The card database only contains English card data. To see cards in <b>%1</b>, "
|
||||
"<b>Oracle</b> must run once with this language selected and re-import the card "
|
||||
"database.</p>"
|
||||
"<p>The cached database and the downloaded card pictures have been cleared, so a "
|
||||
"re-import is picked up without stale entries.</p>"
|
||||
"<p>Run Oracle now?</p>")
|
||||
.arg(cardLanguageBox.itemText(index)),
|
||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
|
||||
|
||||
// The answer only controls whether Oracle starts right away; the caches stay
|
||||
// cleared so the next import or launch rebuilds them in the new language.
|
||||
if (answer == QMessageBox::Yes) {
|
||||
emit cardDatabaseUpdateRequested();
|
||||
}
|
||||
}
|
||||
|
||||
void GeneralSettingsPage::updateStartupServerControlsVisibility()
|
||||
{
|
||||
const int index = startupTabSelector.currentIndex();
|
||||
|
|
@ -429,6 +488,10 @@ void GeneralSettingsPage::retranslateUi()
|
|||
|
||||
languageGroupBox->setTitle(tr("Language settings"));
|
||||
languageLabel.setText(tr("Language:"));
|
||||
cardLanguageBox.setItemText(0, tr("English"));
|
||||
cardLanguageLabel.setText(tr("Card text & images language:"));
|
||||
cardLanguageNoteLabel.setText(
|
||||
tr("Foreign card names, text and art apply after you update the card database (Oracle)."));
|
||||
advertiseTranslationPageLabel.setText(
|
||||
QString("<a href='%1'>%2</a>").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations")));
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ public:
|
|||
static QStringList findQmFiles();
|
||||
static QString languageName(const QString &lang);
|
||||
|
||||
signals:
|
||||
/// Request to re-import the card database with the newly selected card language
|
||||
void cardDatabaseUpdateRequested();
|
||||
|
||||
private slots:
|
||||
void deckPathButtonClicked();
|
||||
void filtersPathButtonClicked();
|
||||
|
|
@ -33,6 +37,7 @@ private slots:
|
|||
void tokenDatabasePathButtonClicked();
|
||||
void resetAllPathsClicked();
|
||||
void languageBoxChanged(int index);
|
||||
void cardLanguageBoxChanged(int index);
|
||||
void updateStartupServerControlsVisibility();
|
||||
|
||||
private:
|
||||
|
|
@ -46,6 +51,10 @@ private:
|
|||
QComboBox languageBox;
|
||||
QLabel advertiseTranslationPageLabel;
|
||||
|
||||
QLabel cardLanguageLabel;
|
||||
QComboBox cardLanguageBox;
|
||||
QLabel cardLanguageNoteLabel;
|
||||
|
||||
QLabel updateReleaseChannelLabel;
|
||||
QComboBox updateReleaseChannelBox;
|
||||
QCheckBox startupUpdateCheckCheckBox;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "archidekt_api_response_deck_display_widget.h"
|
||||
|
||||
#include "../../../../../../client/settings/cache_settings.h"
|
||||
#include "../../../../../deck_loader/card_node_function.h"
|
||||
#include "../../../../../deck_loader/deck_loader.h"
|
||||
#include "../../../../cards/card_size_widget.h"
|
||||
|
|
@ -10,6 +11,7 @@
|
|||
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <libcockatrice/card/import/card_name_normalizer.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
|
||||
ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWidget *parent,
|
||||
ArchidektApiResponseDeck _response,
|
||||
|
|
@ -120,6 +122,9 @@ ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWi
|
|||
}
|
||||
|
||||
model = new DeckListModel(this);
|
||||
model->setDisplayLanguage(SettingsCache::instance().cardsDisplay().getCardLang());
|
||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, model,
|
||||
[this](const QString &lang) { model->setDisplayLanguage(lang); });
|
||||
connect(model, &DeckListModel::modelReset, this, &ArchidektApiResponseDeckDisplayWidget::decklistModelReset);
|
||||
|
||||
auto decklist = QSharedPointer<DeckList>(new DeckList);
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
#include "../interface/widgets/dialogs/dlg_update.h"
|
||||
#include "../interface/widgets/dialogs/dlg_view_log.h"
|
||||
#include "../interface/widgets/onboarding/first_run_wizard.h"
|
||||
#include "../interface/widgets/settings_page/general_settings_page.h"
|
||||
#include "../interface/widgets/tabs/tab_game.h"
|
||||
#include "../interface/widgets/tabs/tab_server.h"
|
||||
#include "../interface/widgets/tabs/tab_supervisor.h"
|
||||
|
|
@ -246,6 +247,8 @@ void MainWindow::actFullScreen(bool checked)
|
|||
void MainWindow::actSettings()
|
||||
{
|
||||
DlgSettings dlg(this);
|
||||
auto *generalPage = qobject_cast<GeneralSettingsPage *>(dlg.page(DlgSettings::GeneralPage));
|
||||
connect(generalPage, &GeneralSettingsPage::cardDatabaseUpdateRequested, this, &MainWindow::actCheckCardUpdates);
|
||||
dlg.exec();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,13 @@
|
|||
</xs:extension>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="localizationType">
|
||||
<xs:sequence>
|
||||
<xs:element type="xs:string" name="name" minOccurs="0" maxOccurs="1" />
|
||||
<xs:element type="xs:string" name="text" minOccurs="0" maxOccurs="1" />
|
||||
</xs:sequence>
|
||||
<xs:attribute type="xs:string" name="lang" use="required" />
|
||||
</xs:complexType>
|
||||
<xs:group name="cardPropertyGroup">
|
||||
<xs:sequence>
|
||||
<xs:any processContents="skip" minOccurs="0" maxOccurs="unbounded" />
|
||||
|
|
@ -49,6 +56,13 @@
|
|||
<xs:group ref="cardPropertyGroup"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="localizations" minOccurs="0" maxOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element type="localizationType" name="localization" minOccurs="0" maxOccurs="unbounded" />
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element type="cardInSetType" name="set" minOccurs="1" maxOccurs="unbounded" />
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element type="relatedType" name="related" minOccurs="0" maxOccurs="unbounded" />
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ set(CMAKE_AUTORCC ON)
|
|||
set(HEADERS
|
||||
libcockatrice/card/card_info.h
|
||||
libcockatrice/card/card_info_comparator.h
|
||||
libcockatrice/card/card_localization.h
|
||||
libcockatrice/card/lazy_properties_hash.h
|
||||
libcockatrice/card/database/card_database.h
|
||||
libcockatrice/card/database/card_database_loader.h
|
||||
|
|
@ -27,6 +28,7 @@ add_library(
|
|||
${MOC_SOURCES}
|
||||
libcockatrice/card/card_info.cpp
|
||||
libcockatrice/card/card_info_comparator.cpp
|
||||
libcockatrice/card/card_localization.cpp
|
||||
libcockatrice/card/lazy_properties_hash.cpp
|
||||
libcockatrice/card/database/card_database.cpp
|
||||
libcockatrice/card/database/card_database_cache.cpp
|
||||
|
|
|
|||
|
|
@ -77,6 +77,9 @@ private:
|
|||
QString text; ///< Text description or rules text of the card.
|
||||
bool isToken; ///< Whether this card is a token or not.
|
||||
|
||||
QMap<QString, QString> localizedNames; ///< Localized card names, keyed by language code.
|
||||
QMap<QString, QString> localizedTexts; ///< Localized rules text, keyed by language code.
|
||||
|
||||
LazyPropertiesHash properties; ///< Key-value store of dynamic card properties.
|
||||
|
||||
QList<CardRelation *> relatedCards; ///< Forward references to related cards.
|
||||
|
|
@ -151,7 +154,8 @@ public:
|
|||
*/
|
||||
CardInfo(const CardInfo &other)
|
||||
: QObject(other.parent()), name(other.name), simpleName(other.simpleName), text(other.text),
|
||||
isToken(other.isToken), properties(other.properties), relatedCards(other.relatedCards),
|
||||
isToken(other.isToken), localizedNames(other.localizedNames), localizedTexts(other.localizedTexts),
|
||||
properties(other.properties), relatedCards(other.relatedCards),
|
||||
reverseRelatedCards(other.reverseRelatedCards), reverseRelatedCardsToMe(other.reverseRelatedCardsToMe),
|
||||
setsToPrintings(other.setsToPrintings), uiAttributes(other.uiAttributes), setsNames(other.setsNames),
|
||||
altNames(other.altNames)
|
||||
|
|
@ -270,6 +274,96 @@ public:
|
|||
text = _text;
|
||||
emit cardInfoChanged(smartThis);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the card name in the given language, falling back to the
|
||||
* English name when no localization is available.
|
||||
*
|
||||
* @param lang Language code (e.g. "de", "ja", "zhs").
|
||||
* @return The localized name, or the English name as fallback.
|
||||
*/
|
||||
[[nodiscard]] const QString &getLocalizedName(const QString &lang) const
|
||||
{
|
||||
const auto it = localizedNames.constFind(lang);
|
||||
return it != localizedNames.constEnd() ? it.value() : name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the rules text in the given language, falling back to the
|
||||
* English text when no localization is available.
|
||||
*
|
||||
* @param lang Language code (e.g. "de", "ja", "zhs").
|
||||
* @return The localized text, or the English text as fallback.
|
||||
*/
|
||||
[[nodiscard]] const QString &getLocalizedText(const QString &lang) const
|
||||
{
|
||||
const auto it = localizedTexts.constFind(lang);
|
||||
return it != localizedTexts.constEnd() ? it.value() : text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the localized card names keyed by language code.
|
||||
*
|
||||
* Only languages that have an entry are present; there is no English
|
||||
* fallback in this map.
|
||||
*/
|
||||
[[nodiscard]] const QMap<QString, QString> &getLocalizedNames() const
|
||||
{
|
||||
return localizedNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the localized rules text keyed by language code.
|
||||
*
|
||||
* Only languages that have an entry are present; there is no English
|
||||
* fallback in this map.
|
||||
*/
|
||||
[[nodiscard]] const QMap<QString, QString> &getLocalizedTexts() const
|
||||
{
|
||||
return localizedTexts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the card name for the given language.
|
||||
*
|
||||
* @param lang Language code.
|
||||
* @param _localizedName The localized card name.
|
||||
*/
|
||||
void setLocalizedName(const QString &lang, const QString &_localizedName)
|
||||
{
|
||||
if (localizedNames.value(lang) == _localizedName) {
|
||||
return;
|
||||
}
|
||||
localizedNames.insert(lang, _localizedName);
|
||||
emit cardInfoChanged(smartThis);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the rules text for the given language.
|
||||
*
|
||||
* @param lang Language code.
|
||||
* @param _localizedText The localized rules text.
|
||||
*/
|
||||
void setLocalizedText(const QString &lang, const QString &_localizedText)
|
||||
{
|
||||
if (localizedTexts.value(lang) == _localizedText) {
|
||||
return;
|
||||
}
|
||||
localizedTexts.insert(lang, _localizedText);
|
||||
emit cardInfoChanged(smartThis);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the language codes for which this card has a localized
|
||||
* name or rules text.
|
||||
*/
|
||||
[[nodiscard]] QStringList localizationLanguages() const
|
||||
{
|
||||
QStringList languages = localizedNames.keys();
|
||||
languages.append(localizedTexts.keys());
|
||||
languages.removeDuplicates();
|
||||
return languages;
|
||||
}
|
||||
[[nodiscard]] bool getIsToken() const
|
||||
{
|
||||
return isToken;
|
||||
|
|
|
|||
25
libcockatrice_card/libcockatrice/card/card_localization.cpp
Normal file
25
libcockatrice_card/libcockatrice/card/card_localization.cpp
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#include "card_localization.h"
|
||||
|
||||
#include <QLocale>
|
||||
#include <QStringList>
|
||||
|
||||
namespace CardLocalization
|
||||
{
|
||||
const QStringList &supportedLanguages()
|
||||
{
|
||||
static const QStringList languages = {"cs", "de", "es", "fr", "it", "ja", "ko", "pt", "ru", "zhs", "zht", "he"};
|
||||
return languages;
|
||||
}
|
||||
|
||||
QString languageDisplayName(const QString &lang)
|
||||
{
|
||||
if (lang == "zhs") {
|
||||
return QStringLiteral("简体中文");
|
||||
}
|
||||
if (lang == "zht") {
|
||||
return QStringLiteral("繁體中文");
|
||||
}
|
||||
const QString nativeName = QLocale(lang).nativeLanguageName();
|
||||
return nativeName.isEmpty() ? lang : nativeName;
|
||||
}
|
||||
} // namespace CardLocalization
|
||||
40
libcockatrice_card/libcockatrice/card/card_localization.h
Normal file
40
libcockatrice_card/libcockatrice/card/card_localization.h
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
#ifndef CARD_LOCALIZATION_H
|
||||
#define CARD_LOCALIZATION_H
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
/**
|
||||
* @namespace CardLocalization
|
||||
* @ingroup Cards
|
||||
*
|
||||
* @brief Shared language metadata for localized card text and images.
|
||||
*
|
||||
* Lists the language codes Cockatrice can display localized card data for and
|
||||
* provides human-readable names. The list is shared between Oracle (which
|
||||
* imports the selected language's card data) and the client settings UI (which
|
||||
* offers the language choice).
|
||||
*/
|
||||
namespace CardLocalization
|
||||
{
|
||||
/**
|
||||
* @brief Language codes for which localized card data can be imported/displayed.
|
||||
*
|
||||
* Matches the languages Scryfall can serve localized card images for. "en" is
|
||||
* always available as the default/fallback and is not listed here.
|
||||
*
|
||||
* @return The list of supported language codes.
|
||||
*/
|
||||
[[nodiscard]] const QStringList &supportedLanguages();
|
||||
|
||||
/**
|
||||
* @brief Human-readable native name for a language code.
|
||||
*
|
||||
* @param lang Language code (e.g. "de", "ja", "zhs").
|
||||
* @return The language's name in its own language, or the code itself if
|
||||
* it cannot be resolved.
|
||||
*/
|
||||
[[nodiscard]] QString languageDisplayName(const QString &lang);
|
||||
} // namespace CardLocalization
|
||||
|
||||
#endif // CARD_LOCALIZATION_H
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
namespace
|
||||
{
|
||||
constexpr quint32 CACHE_MAGIC = 0x43445243; // "CDRC"
|
||||
constexpr quint32 CACHE_VERSION = 2;
|
||||
constexpr quint32 CACHE_VERSION = 3;
|
||||
|
||||
// ---- Primitives -----------------------------------------------------------
|
||||
|
||||
|
|
@ -71,6 +71,35 @@ QDate readDate(QDataStream &in)
|
|||
return d;
|
||||
}
|
||||
|
||||
void writeStringMap(QDataStream &out, const QMap<QString, QString> &map)
|
||||
{
|
||||
out << static_cast<quint32>(map.size());
|
||||
for (auto it = map.constBegin(); it != map.constEnd(); ++it) {
|
||||
writeString(out, it.key());
|
||||
writeString(out, it.value());
|
||||
}
|
||||
}
|
||||
|
||||
QMap<QString, QString> readStringMap(QDataStream &in)
|
||||
{
|
||||
QMap<QString, QString> map;
|
||||
quint32 count = 0;
|
||||
in >> count;
|
||||
if (in.status() != QDataStream::Ok) {
|
||||
return map;
|
||||
}
|
||||
for (quint32 i = 0; i < count; ++i) {
|
||||
QString key = readString(in);
|
||||
QString value = readString(in);
|
||||
if (in.status() != QDataStream::Ok) {
|
||||
map.clear();
|
||||
return map;
|
||||
}
|
||||
map.insert(key, value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// ---- CardRelation ----------------------------------------------------------
|
||||
|
||||
void writeRelation(QDataStream &out, const CardRelation *rel)
|
||||
|
|
@ -195,6 +224,10 @@ void writeCard(QDataStream &out, const CardInfoPtr &card)
|
|||
for (const CardRelation *rel : reverse) {
|
||||
writeRelation(out, rel);
|
||||
}
|
||||
|
||||
// localized card data
|
||||
writeStringMap(out, card->getLocalizedNames());
|
||||
writeStringMap(out, card->getLocalizedTexts());
|
||||
}
|
||||
|
||||
CardInfoPtr readCard(QDataStream &in, const SetNameMap &sets)
|
||||
|
|
@ -268,8 +301,25 @@ CardInfoPtr readCard(QDataStream &in, const SetNameMap &sets)
|
|||
reverse.append(readRelation(in));
|
||||
}
|
||||
|
||||
return CardInfo::newInstance(name, text, isToken, propertiesBlob, related, reverse, cardSets, ui, simpleName,
|
||||
altNames, false);
|
||||
CardInfoPtr card = CardInfo::newInstance(name, text, isToken, propertiesBlob, related, reverse, cardSets, ui,
|
||||
simpleName, altNames, false);
|
||||
|
||||
const QMap<QString, QString> localizedNames = readStringMap(in);
|
||||
if (in.status() != QDataStream::Ok) {
|
||||
return nullptr;
|
||||
}
|
||||
const QMap<QString, QString> localizedTexts = readStringMap(in);
|
||||
if (in.status() != QDataStream::Ok) {
|
||||
return nullptr;
|
||||
}
|
||||
for (auto it = localizedNames.constBegin(); it != localizedNames.constEnd(); ++it) {
|
||||
card->setLocalizedName(it.key(), it.value());
|
||||
}
|
||||
for (auto it = localizedTexts.constBegin(); it != localizedTexts.constEnd(); ++it) {
|
||||
card->setLocalizedText(it.key(), it.value());
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
// ---- FormatRules -----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -273,6 +273,8 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml)
|
|||
QString name = QString("");
|
||||
QString text = QString("");
|
||||
QHash<QString, QString> properties;
|
||||
QMap<QString, QString> localizedNames;
|
||||
QMap<QString, QString> localizedTexts;
|
||||
QList<CardRelation *> relatedCards, reverseRelatedCards;
|
||||
auto _sets = SetToPrintingsMap();
|
||||
int tableRow = 0;
|
||||
|
|
@ -298,6 +300,44 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml)
|
|||
// generic properties
|
||||
} else if (xmlName == "prop") {
|
||||
properties = loadCardPropertiesFromXml(xml);
|
||||
// localized card data
|
||||
} else if (xmlName == "localizations") {
|
||||
while (!xml.atEnd()) {
|
||||
if (xml.readNextStartElement()) {
|
||||
const QString elementName = xml.name().toString();
|
||||
if (elementName == "localization") {
|
||||
const QString lang = xml.attributes().value("lang").toString();
|
||||
QString localizedName;
|
||||
QString localizedText;
|
||||
while (!xml.atEnd()) {
|
||||
if (xml.readNext() == QXmlStreamReader::EndElement) {
|
||||
break;
|
||||
}
|
||||
if (xml.isStartElement()) {
|
||||
const QString childName = xml.name().toString();
|
||||
QString value = xml.readElementText(QXmlStreamReader::IncludeChildElements);
|
||||
if (childName == "name") {
|
||||
localizedName = value;
|
||||
} else if (childName == "text") {
|
||||
localizedText = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!lang.isEmpty()) {
|
||||
if (!localizedName.isEmpty()) {
|
||||
localizedNames.insert(lang, localizedName);
|
||||
}
|
||||
if (!localizedText.isEmpty()) {
|
||||
localizedTexts.insert(lang, localizedText);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
xml.skipCurrentElement();
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// positioning info
|
||||
} else if (xmlName == "tablerow") {
|
||||
tableRow = xml.readElementText(QXmlStreamReader::IncludeChildElements).toInt();
|
||||
|
|
@ -401,6 +441,12 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml)
|
|||
.upsideDownArt = upsideDown};
|
||||
CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards,
|
||||
reverseRelatedCards, _sets, attributes);
|
||||
for (auto it = localizedNames.constBegin(); it != localizedNames.constEnd(); ++it) {
|
||||
newCard->setLocalizedName(it.key(), it.value());
|
||||
}
|
||||
for (auto it = localizedTexts.constBegin(); it != localizedTexts.constEnd(); ++it) {
|
||||
newCard->setLocalizedText(it.key(), it.value());
|
||||
}
|
||||
if (targetData) {
|
||||
// Mirror CardDatabase::addCard: if a card with this name already
|
||||
// exists, merge the new printings into it instead of replacing.
|
||||
|
|
@ -517,6 +563,26 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in
|
|||
}
|
||||
xml.writeEndElement();
|
||||
|
||||
// localized card data
|
||||
const QStringList localizedLanguages = info->localizationLanguages();
|
||||
if (!localizedLanguages.isEmpty()) {
|
||||
xml.writeStartElement("localizations");
|
||||
const QMap<QString, QString> &localizedNames = info->getLocalizedNames();
|
||||
const QMap<QString, QString> &localizedTexts = info->getLocalizedTexts();
|
||||
for (const QString &lang : localizedLanguages) {
|
||||
xml.writeStartElement("localization");
|
||||
xml.writeAttribute("lang", lang);
|
||||
if (localizedNames.contains(lang)) {
|
||||
xml.writeTextElement("name", localizedNames.value(lang));
|
||||
}
|
||||
if (localizedTexts.contains(lang)) {
|
||||
xml.writeTextElement("text", localizedTexts.value(lang));
|
||||
}
|
||||
xml.writeEndElement();
|
||||
}
|
||||
xml.writeEndElement();
|
||||
}
|
||||
|
||||
// sets
|
||||
for (const auto &printings : info->getSets()) {
|
||||
for (const PrintingInfo &set : printings) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
#ifndef COCKATRICE_INTERFACE_CARDS_DISPLAY_SETTINGS_PROVIDER_H
|
||||
#define COCKATRICE_INTERFACE_CARDS_DISPLAY_SETTINGS_PROVIDER_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
class ICardsDisplaySettingsProvider
|
||||
{
|
||||
public:
|
||||
|
|
@ -26,6 +28,7 @@ public:
|
|||
[[nodiscard]] virtual int getEDHRecCardSize() const = 0;
|
||||
[[nodiscard]] virtual int getArchidektPreviewSize() const = 0;
|
||||
[[nodiscard]] virtual int getSampleHandSize() const = 0;
|
||||
[[nodiscard]] virtual QString getCardLang() const = 0;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_INTERFACE_CARDS_DISPLAY_SETTINGS_PROVIDER_H
|
||||
|
|
|
|||
|
|
@ -28,6 +28,16 @@ DeckListModel::~DeckListModel()
|
|||
delete root;
|
||||
}
|
||||
|
||||
void DeckListModel::setDisplayLanguage(const QString &lang)
|
||||
{
|
||||
if (displayLang == lang) {
|
||||
return;
|
||||
}
|
||||
displayLang = lang;
|
||||
emit layoutAboutToBeChanged();
|
||||
emit layoutChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Extract the value from the card that is used for the group criteria.
|
||||
* @param info Pointer to card information.
|
||||
|
|
@ -181,8 +191,15 @@ QVariant DeckListModel::data(const QModelIndex &index, int role) const
|
|||
switch (index.column()) {
|
||||
case DeckListModelColumns::CARD_AMOUNT:
|
||||
return card->getNumber();
|
||||
case DeckListModelColumns::CARD_NAME:
|
||||
case DeckListModelColumns::CARD_NAME: {
|
||||
if (role == Qt::DisplayRole) {
|
||||
CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(card->getName());
|
||||
if (info) {
|
||||
return info->getLocalizedName(displayLang);
|
||||
}
|
||||
}
|
||||
return card->getName();
|
||||
}
|
||||
case DeckListModelColumns::CARD_SET:
|
||||
return card->getCardSetShortName();
|
||||
case DeckListModelColumns::CARD_COLLECTOR_NUMBER:
|
||||
|
|
|
|||
|
|
@ -287,6 +287,16 @@ public:
|
|||
explicit DeckListModel(QObject *parent, const QSharedPointer<DeckList> &deckList);
|
||||
~DeckListModel() override;
|
||||
|
||||
/**
|
||||
* @brief Selects the language code for localized card names in the display role.
|
||||
*
|
||||
* The model never reads global settings itself; callers wire this to the card
|
||||
* language setting (including reacting to its changes) rather than the model
|
||||
* querying it.
|
||||
* @param lang Language code; "en" shows the canonical English names.
|
||||
*/
|
||||
void setDisplayLanguage(const QString &lang);
|
||||
|
||||
/**
|
||||
* @brief Returns the root index of the model.
|
||||
* @return QModelIndex representing the root node.
|
||||
|
|
@ -408,6 +418,7 @@ private:
|
|||
DeckListModelGroupCriteria::Type activeGroupCriteria = DeckListModelGroupCriteria::MAIN_TYPE;
|
||||
int lastKnownColumn; /**< Last column used for sorting. */
|
||||
Qt::SortOrder lastKnownOrder; /**< Last known sort order. */
|
||||
QString displayLang = "en"; /**< Language code for localized card names in the display role. */
|
||||
|
||||
InnerDecklistNode *createNodeIfNeeded(const QString &name, InnerDecklistNode *parent);
|
||||
QModelIndex nodeToIndex(AbstractDecklistNode *node) const;
|
||||
|
|
|
|||
|
|
@ -105,6 +105,11 @@ int CardsDisplaySettings::getSampleHandSize() const
|
|||
return getValue("sampleHandSize", "cards", "cardSize", 7).toInt();
|
||||
}
|
||||
|
||||
QString CardsDisplaySettings::getCardLang() const
|
||||
{
|
||||
return getValue("cardLang", QString(), QString(), "en").toString();
|
||||
}
|
||||
|
||||
void CardsDisplaySettings::setDisplayCardNames(bool _displayCardNames)
|
||||
{
|
||||
setValue(_displayCardNames, "displayCardNames");
|
||||
|
|
@ -224,3 +229,16 @@ void CardsDisplaySettings::setSampleHandSize(int _sampleHandSize)
|
|||
setValue(_sampleHandSize, "sampleHandSize", "cards", "cardSize");
|
||||
emit sampleHandSizeChanged(_sampleHandSize);
|
||||
}
|
||||
|
||||
void CardsDisplaySettings::setCardLang(const QString &_cardLang)
|
||||
{
|
||||
if (_cardLang == getCardLang()) {
|
||||
return;
|
||||
}
|
||||
setValue(_cardLang, "cardLang");
|
||||
// Flush to disk immediately: the Oracle tool is a separate process that
|
||||
// reads this value to decide which foreignData to import, so it must not
|
||||
// observe a stale (pre-change) value.
|
||||
sync();
|
||||
emit cardLangChanged(_cardLang);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ public:
|
|||
[[nodiscard]] int getEDHRecCardSize() const override;
|
||||
[[nodiscard]] int getArchidektPreviewSize() const override;
|
||||
[[nodiscard]] int getSampleHandSize() const override;
|
||||
[[nodiscard]] QString getCardLang() const override;
|
||||
|
||||
void setDisplayCardNames(bool _displayCardNames);
|
||||
void setRoundCardCorners(bool _roundCardCorners);
|
||||
|
|
@ -52,6 +53,7 @@ public:
|
|||
void setEDHRecCardSize(int _edhrecCardSize);
|
||||
void setArchidektPreviewCardSize(int _archidektPreviewCardSize);
|
||||
void setSampleHandSize(int _sampleHandSize);
|
||||
void setCardLang(const QString &_cardLang);
|
||||
|
||||
signals:
|
||||
void displayCardNamesChanged();
|
||||
|
|
@ -68,6 +70,7 @@ signals:
|
|||
void edhRecCardSizeChanged();
|
||||
void archidektPreviewSizeChanged();
|
||||
void sampleHandSizeChanged(int amount);
|
||||
void cardLangChanged(const QString &lang);
|
||||
|
||||
public:
|
||||
explicit CardsDisplaySettings(const QString &settingPath, QObject *parent = nullptr);
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
const QStringList DownloadSettings::DEFAULT_DOWNLOAD_URLS = {
|
||||
"https://cards.scryfall.io/large/!prop:side!/!set:uuid_substr_0_1!/!set:uuid_substr_1_1!/!set:uuid!.jpg",
|
||||
"https://api.scryfall.com/cards/!set:uuid!?format=image&face=!prop:side!",
|
||||
"https://api.scryfall.com/cards/multiverse/!set:muid!?format=image",
|
||||
"https://api.scryfall.com/cards/!set:uuid!?format=image&face=!prop:side!&lang=!sflang!",
|
||||
"https://api.scryfall.com/cards/multiverse/!set:muid!?format=image&lang=!sflang!",
|
||||
"https://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!set:muid!&type=card",
|
||||
"https://gatherer.wizards.com/Handlers/Image.ashx?name=!name!&type=card"};
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
#include <QSet>
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <libcockatrice/card/card_localization.h>
|
||||
#include <libcockatrice/card/database/parser/cockatrice_xml_4.h>
|
||||
#include <libcockatrice/card/relation/card_relation.h>
|
||||
|
||||
|
|
@ -22,8 +23,9 @@ static const QList<AllowedCount> kSingletonCounts = {{1, "legal"}, {0, "banned"}
|
|||
SplitCardPart::SplitCardPart(const QString &_name,
|
||||
const QString &_text,
|
||||
const QHash<QString, QString> &_properties,
|
||||
const PrintingInfo &_printingInfo)
|
||||
: name(_name), text(_text), properties(_properties), printingInfo(_printingInfo)
|
||||
const PrintingInfo &_printingInfo,
|
||||
const QString &_localizedText)
|
||||
: name(_name), text(_text), localizedText(_localizedText), properties(_properties), printingInfo(_printingInfo)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -33,6 +35,42 @@ OracleImporter::OracleImporter(QObject *parent) : QObject(parent)
|
|||
{
|
||||
}
|
||||
|
||||
void OracleImporter::setCardLang(const QString &lang)
|
||||
{
|
||||
cardLang = lang.trimmed().toLower();
|
||||
localizationEnabled = cardLang != "en" && CardLocalization::supportedLanguages().contains(cardLang);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Maps the MTGJSON foreignData language names to the short codes used by
|
||||
* Scryfall and stored in cards.xml (e.g. "German" -> "de").
|
||||
* @param language The language name found in the MTGJSON foreignData entries.
|
||||
* @return The short language code, or an empty string if unknown.
|
||||
*/
|
||||
static QString mtgjsonLanguageToCode(const QString &language)
|
||||
{
|
||||
static const QHash<QString, QString> map = {
|
||||
{"Chinese Simplified", "zhs"},
|
||||
{"Chinese Traditional", "zht"},
|
||||
{"English", "en"},
|
||||
{"French", "fr"},
|
||||
{"German", "de"},
|
||||
{"Greek", "grc"},
|
||||
{"Ancient Greek", "grc"},
|
||||
{"Hebrew", "he"},
|
||||
{"Italian", "it"},
|
||||
{"Japanese", "ja"},
|
||||
{"Korean", "ko"},
|
||||
{"Latin", "la"},
|
||||
{"Phyrexian", "ph"},
|
||||
{"Portuguese (Brazil)", "pt"},
|
||||
{"Russian", "ru"},
|
||||
{"Sanskrit", "sa"},
|
||||
{"Spanish", "es"},
|
||||
};
|
||||
return map.value(language);
|
||||
}
|
||||
|
||||
static CardSet::Priority getSetPriority(const QString &setType, const QString &shortName)
|
||||
{
|
||||
if (!setTypePriorities.contains(setType.toLower())) {
|
||||
|
|
@ -254,6 +292,101 @@ static QString getJsonString(const QJsonObject &obj, const QString &key)
|
|||
return obj.value(key).toVariant().toString();
|
||||
}
|
||||
|
||||
static QString normalizeCardName(QString name)
|
||||
{
|
||||
// Mirror of the name cleanup applied in addCard(), so collected localization
|
||||
// keys line up with the card map keys (Æ → AE, curly apostrophe → straight).
|
||||
name = name.replace("Æ", "AE");
|
||||
name = name.replace("’", "'");
|
||||
return name;
|
||||
}
|
||||
|
||||
static QString matchingForeignEntryText(const QJsonObject &card, const QString &cardLang)
|
||||
{
|
||||
// Multi-face cards (split/aftermath/adventure/prepare) expose each face as a
|
||||
// separate card object, each with its own foreignData entry carrying that
|
||||
// face's rules text; single-face cards carry the full text in one entry.
|
||||
const QJsonArray foreignData = card.value("foreignData").toArray();
|
||||
for (const QJsonValue &entryValue : foreignData) {
|
||||
const QJsonObject entry = entryValue.toObject();
|
||||
// MTGJSON reports languages by long-form name ("German"); match the
|
||||
// short code ("de") that Scryfall and cards.xml use.
|
||||
if (mtgjsonLanguageToCode(getJsonString(entry, "language")) == cardLang) {
|
||||
return getJsonString(entry, "text");
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
void OracleImporter::collectForeignData(const QString &cardKey,
|
||||
const CardSetPtr ¤tSet,
|
||||
const QJsonObject &card,
|
||||
bool collectText)
|
||||
{
|
||||
if (!localizationEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
LocalizedCardEntry incoming;
|
||||
bool found = false;
|
||||
const QJsonArray foreignData = card.value("foreignData").toArray();
|
||||
for (const QJsonValue &entryValue : foreignData) {
|
||||
const QJsonObject entry = entryValue.toObject();
|
||||
// MTGJSON reports languages by long-form name ("German"); match the
|
||||
// short code ("de") that Scryfall and cards.xml use.
|
||||
if (mtgjsonLanguageToCode(getJsonString(entry, "language")) != cardLang) {
|
||||
continue;
|
||||
}
|
||||
incoming.name = getJsonString(entry, "name");
|
||||
incoming.text = collectText ? getJsonString(entry, "text") : QString();
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
if (!found) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer the entry from the highest-priority set (lower enum value = more
|
||||
// authoritative); printings of equal priority keep the first one seen.
|
||||
incoming.priority = currentSet->getPriority();
|
||||
const auto existing = localizedEntries.constFind(cardKey);
|
||||
if (existing == localizedEntries.constEnd() || incoming.priority < existing->priority) {
|
||||
localizedEntries.insert(cardKey, incoming);
|
||||
}
|
||||
}
|
||||
|
||||
void OracleImporter::applyLocalizedData()
|
||||
{
|
||||
if (!localizationEnabled) {
|
||||
return;
|
||||
}
|
||||
for (auto it = localizedEntries.constBegin(); it != localizedEntries.constEnd(); ++it) {
|
||||
CardInfoPtr card = cards.value(it.key());
|
||||
if (card.isNull()) {
|
||||
continue;
|
||||
}
|
||||
const LocalizedCardEntry &entry = it.value();
|
||||
if (!entry.name.isEmpty()) {
|
||||
card->setLocalizedName(cardLang, entry.name);
|
||||
}
|
||||
if (!entry.text.isEmpty()) {
|
||||
card->setLocalizedText(cardLang, entry.text);
|
||||
}
|
||||
}
|
||||
localizedEntries.clear();
|
||||
|
||||
for (auto it = splitLocalizedTexts.constBegin(); it != splitLocalizedTexts.constEnd(); ++it) {
|
||||
CardInfoPtr card = cards.value(it.key());
|
||||
if (card.isNull()) {
|
||||
continue;
|
||||
}
|
||||
if (!it.value().text.isEmpty()) {
|
||||
card->setLocalizedText(cardLang, it.value().text);
|
||||
}
|
||||
}
|
||||
splitLocalizedTexts.clear();
|
||||
}
|
||||
|
||||
int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJsonArray &cardsList)
|
||||
{
|
||||
// mtgjson name => xml name
|
||||
|
|
@ -397,13 +530,21 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson
|
|||
// split cards are considered a single card, enqueue for later merging
|
||||
if (layout == "split" || layout == "aftermath" || layout == "adventure" || layout == "prepare") {
|
||||
auto _faceName = getJsonString(card, "faceName");
|
||||
SplitCardPart split(_faceName, text, properties, printingInfo);
|
||||
// MTGJSON exposes each face as a separate card object, each with its
|
||||
// own foreignData entry holding that face's rules text; collect it so
|
||||
// the per-face texts can be joined the same way as the English text.
|
||||
const QString faceLocalizedText =
|
||||
localizationEnabled ? matchingForeignEntryText(card, cardLang) : QString();
|
||||
SplitCardPart split(_faceName, text, properties, printingInfo, faceLocalizedText);
|
||||
auto found_iter = splitCards.find(name + numProperty);
|
||||
if (found_iter == splitCards.end()) {
|
||||
splitCards.insert(name + numProperty, {{split}, name});
|
||||
} else {
|
||||
found_iter->first.append(split);
|
||||
}
|
||||
// MTGJSON's foreignData name is the joined name present on every
|
||||
// face, so collect the name once.
|
||||
collectForeignData(normalizeCardName(name), currentSet, card, false);
|
||||
} else {
|
||||
// relations
|
||||
QList<CardRelation *> relatedCards;
|
||||
|
|
@ -446,6 +587,8 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson
|
|||
}
|
||||
}
|
||||
|
||||
collectForeignData(normalizeCardName(name + numComponent), currentSet, card);
|
||||
|
||||
CardInfoPtr newCard =
|
||||
addCard(name + numComponent, text, isToken, std::move(properties), relatedCards, printingInfo);
|
||||
numCards++;
|
||||
|
|
@ -459,6 +602,8 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson
|
|||
QList<QPair<QList<SplitCardPart>, QString>> partsAndNames = splitCards.values();
|
||||
for (auto [splitCardParts, name] : partsAndNames) {
|
||||
QString text;
|
||||
QString localizedText;
|
||||
bool localizedTextComplete = true;
|
||||
QHash<QString, QString> properties;
|
||||
PrintingInfo printingInfo;
|
||||
|
||||
|
|
@ -468,6 +613,21 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson
|
|||
}
|
||||
text.append(tmp.getText());
|
||||
|
||||
// Build the cardLang text by joining each face's translated text with
|
||||
// the same separator as the English text. Any face missing a complete
|
||||
// translation abandons the whole join, falling back to the English text.
|
||||
if (localizedTextComplete) {
|
||||
const QString partLocalizedText = tmp.getLocalizedText();
|
||||
if (partLocalizedText.isEmpty()) {
|
||||
localizedTextComplete = false;
|
||||
} else {
|
||||
if (!localizedText.isEmpty()) {
|
||||
localizedText.append(splitCardTextSeparator);
|
||||
}
|
||||
localizedText.append(partLocalizedText);
|
||||
}
|
||||
}
|
||||
|
||||
if (properties.isEmpty()) {
|
||||
properties = tmp.getProperties();
|
||||
printingInfo = tmp.getPrintingInfo();
|
||||
|
|
@ -500,6 +660,18 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson
|
|||
}
|
||||
}
|
||||
CardInfoPtr newCard = addCard(name, text, isToken, std::move(properties), {}, printingInfo);
|
||||
if (localizationEnabled && localizedTextComplete && !localizedText.isEmpty()) {
|
||||
// Same priority policy as collectForeignData(): the joined text from the
|
||||
// highest-priority set seen so far wins, applied once all printings are in.
|
||||
LocalizedCardEntry entry;
|
||||
entry.text = localizedText;
|
||||
entry.priority = currentSet->getPriority();
|
||||
const QString entryKey = normalizeCardName(name);
|
||||
const auto existing = splitLocalizedTexts.constFind(entryKey);
|
||||
if (existing == splitLocalizedTexts.constEnd() || entry.priority < existing->priority) {
|
||||
splitLocalizedTexts.insert(entryKey, entry);
|
||||
}
|
||||
}
|
||||
numCards++;
|
||||
}
|
||||
|
||||
|
|
@ -654,6 +826,8 @@ int OracleImporter::startImport()
|
|||
emit setIndexChanged(numCardsInSet, setIndex, curSetToParse.getLongName());
|
||||
}
|
||||
|
||||
applyLocalizedData();
|
||||
|
||||
emit setIndexChanged(0, setIndex, QString());
|
||||
|
||||
// total number of sets
|
||||
|
|
@ -679,4 +853,6 @@ void OracleImporter::clear()
|
|||
cards.clear();
|
||||
allSets.clear();
|
||||
rawSetsData.clear();
|
||||
localizedEntries.clear();
|
||||
splitLocalizedTexts.clear();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,7 +107,8 @@ public:
|
|||
SplitCardPart(const QString &_name,
|
||||
const QString &_text,
|
||||
const QHash<QString, QString> &_properties,
|
||||
const PrintingInfo &_printingInfo);
|
||||
const PrintingInfo &_printingInfo,
|
||||
const QString &_localizedText = QString());
|
||||
inline const QString &getName() const
|
||||
{
|
||||
return name;
|
||||
|
|
@ -116,6 +117,13 @@ public:
|
|||
{
|
||||
return text;
|
||||
}
|
||||
/**
|
||||
* @brief The cardLang rules text of this face's foreignData entry, if any.
|
||||
*/
|
||||
inline const QString &getLocalizedText() const
|
||||
{
|
||||
return localizedText;
|
||||
}
|
||||
inline const QHash<QString, QString> &getProperties() const
|
||||
{
|
||||
return properties;
|
||||
|
|
@ -128,10 +136,18 @@ public:
|
|||
private:
|
||||
QString name;
|
||||
QString text;
|
||||
QString localizedText;
|
||||
QHash<QString, QString> properties;
|
||||
PrintingInfo printingInfo;
|
||||
};
|
||||
|
||||
struct LocalizedCardEntry
|
||||
{
|
||||
QString name;
|
||||
QString text;
|
||||
CardSet::Priority priority = CardSet::PriorityLowest;
|
||||
};
|
||||
|
||||
class OracleImporter : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
|
@ -171,12 +187,53 @@ private:
|
|||
*/
|
||||
QAtomicInt importCancelled;
|
||||
|
||||
/**
|
||||
* The ISO-639 language code whose foreignData is imported; "en" by default.
|
||||
*/
|
||||
QString cardLang = "en";
|
||||
|
||||
/**
|
||||
* Whether cardLang is a supported language other than English, so per-card
|
||||
* foreignData scanning can be skipped entirely when disabled.
|
||||
*/
|
||||
bool localizationEnabled = false;
|
||||
|
||||
/**
|
||||
* Localized name/text collected per imported card key while parsing sets,
|
||||
* applied to the CardInfo objects by applyLocalizedData() once all
|
||||
* printings have been seen so the best-priority one wins.
|
||||
*/
|
||||
QMap<QString, LocalizedCardEntry> localizedEntries;
|
||||
|
||||
/**
|
||||
* cardLang rules text collected for split-card names while parsing sets,
|
||||
* applied by applyLocalizedData(). Kept apart from localizedEntries because
|
||||
* MTGJSON emits each split face as its own card object with the joined name
|
||||
* on every foreignData entry: names and the per-face text join have different
|
||||
* completeness and must not overwrite each other under the same key.
|
||||
*/
|
||||
QMap<QString, LocalizedCardEntry> splitLocalizedTexts;
|
||||
|
||||
CardInfoPtr addCard(QString name,
|
||||
const QString &text,
|
||||
bool isToken,
|
||||
QHash<QString, QString> properties,
|
||||
const QList<CardRelation *> &relatedCards,
|
||||
const PrintingInfo &printingInfo);
|
||||
|
||||
/**
|
||||
* Records the first foreignData entry matching cardLang for the given card
|
||||
* key, keeping the entry from the highest-priority set seen so far.
|
||||
*
|
||||
* Multi-face cards (split, adventure, aftermath, prepare) pass collectText =
|
||||
* false: MTGJSON emits one foreignData entry per face with the same joined
|
||||
* name but only that face's text, so the name is collected here while the
|
||||
* per-face texts are joined during the split-card merge.
|
||||
*/
|
||||
void collectForeignData(const QString &cardKey,
|
||||
const CardSetPtr ¤tSet,
|
||||
const QJsonObject &card,
|
||||
bool collectText = true);
|
||||
signals:
|
||||
void setIndexChanged(int cardsImported, int setIndex, const QString &setName);
|
||||
void dataReadProgress(int bytesRead, int totalBytes);
|
||||
|
|
@ -195,6 +252,15 @@ public:
|
|||
{
|
||||
progressReporting = enabled;
|
||||
}
|
||||
/**
|
||||
* Selects the ISO-639 language code whose foreignData is imported.
|
||||
* English (the default) and unsupported codes disable localization.
|
||||
*/
|
||||
void setCardLang(const QString &lang);
|
||||
const QString &getCardLang() const
|
||||
{
|
||||
return cardLang;
|
||||
}
|
||||
/**
|
||||
* Scans the given JSON document for set metadata. Takes the data by value so
|
||||
* the wizard can hand over its decompressed buffer without copying it.
|
||||
|
|
@ -212,6 +278,12 @@ public:
|
|||
{
|
||||
importCancelled.storeRelease(1);
|
||||
}
|
||||
/**
|
||||
* Applies the collected localized names/texts to the imported cards.
|
||||
* Called automatically at the end of startImport(); exposed separately so
|
||||
* tests can drive it after importing sets directly.
|
||||
*/
|
||||
void applyLocalizedData();
|
||||
bool saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion);
|
||||
int importCardsFromSet(const CardSetPtr ¤tSet, const QJsonArray &cardsList);
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
#include <QScrollBar>
|
||||
#include <QtConcurrent>
|
||||
#include <QtGui>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
|
||||
OracleWizard::OracleWizard(QWidget *parent) : QWizard(parent)
|
||||
|
|
@ -37,6 +38,9 @@ OracleWizard::OracleWizard(QWidget *parent) : QWizard(parent)
|
|||
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &OracleWizard::updateLanguage);
|
||||
|
||||
importer = new OracleImporter(this);
|
||||
// Import card text in the language the client displays, if supported:
|
||||
// foreignData for any other language is never imported.
|
||||
importer->setCardLang(SettingsCache::instance().cardsDisplay().getCardLang());
|
||||
|
||||
nam = new QNetworkAccessManager(this);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,14 @@
|
|||
#include "test_card_database_path_provider.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <QTemporaryDir>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
#include <libcockatrice/card/database/card_database_data.h>
|
||||
#include <libcockatrice/card/database/parser/cockatrice_xml_4.h>
|
||||
#include <libcockatrice/card/lazy_properties_hash.h>
|
||||
#include <libcockatrice/card/printing/printing_info.h>
|
||||
#include <libcockatrice/interfaces/noop_card_preference_provider.h>
|
||||
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
|
|
@ -33,6 +38,49 @@ TEST(CardDatabaseTest, LoadXml)
|
|||
ASSERT_EQ(0, db->query()->getAllMainCardTypes().size()) << "Types not empty after clear";
|
||||
ASSERT_EQ(NotLoaded, db->getLoadStatus()) << "Incorrect status after clear";
|
||||
}
|
||||
|
||||
TEST(CardDatabaseTest, Xml4LocalizedDataRoundTrip)
|
||||
{
|
||||
NoopCardSetPriorityController controller;
|
||||
CardSetPtr set =
|
||||
CardSet::newInstance(&controller, "TST", "Test Set", "expansion", QDate(), CardSet::PriorityPrimary);
|
||||
|
||||
QHash<QString, QString> props;
|
||||
props["manacost"] = "1R";
|
||||
PrintingInfo printing(set, LazyPropertiesHash(props));
|
||||
SetToPrintingsMap setsInfo;
|
||||
setsInfo["TST"].append(printing);
|
||||
|
||||
CardInfo::UiAttributes attributes = {.tableRow = 1};
|
||||
CardInfoPtr card =
|
||||
CardInfo::newInstance("Lightning Bolt", "Deal 3 damage.", false, {}, {}, {}, setsInfo, attributes);
|
||||
card->setLocalizedName("de", "Blitzschlag");
|
||||
card->setLocalizedText("de", "Blitzschlag fügt 3 Schadenspunkte zu.");
|
||||
|
||||
SetNameMap sets;
|
||||
sets.insert("TST", set);
|
||||
CardNameMap cards;
|
||||
cards.insert("Lightning Bolt", card);
|
||||
|
||||
QTemporaryDir tempDir;
|
||||
const QString fileName = tempDir.filePath("cards.xml");
|
||||
NoopCardPreferenceProvider prefProvider;
|
||||
CockatriceXml4Parser writer(&prefProvider, &controller);
|
||||
ASSERT_TRUE(writer.saveToFile({}, sets, cards, fileName));
|
||||
|
||||
CardDatabaseData data;
|
||||
CockatriceXml4Parser parser(&prefProvider, &controller);
|
||||
QFile file(fileName);
|
||||
ASSERT_TRUE(file.open(QIODevice::ReadOnly));
|
||||
parser.parseFileInto(file, data);
|
||||
|
||||
CardInfoPtr loaded = data.cards.value("Lightning Bolt");
|
||||
ASSERT_FALSE(loaded.isNull());
|
||||
ASSERT_EQ(loaded->getName(), "Lightning Bolt");
|
||||
ASSERT_EQ(loaded->getLocalizedName("de"), "Blitzschlag");
|
||||
ASSERT_EQ(loaded->getLocalizedText("de"), "Blitzschlag fügt 3 Schadenspunkte zu.");
|
||||
ASSERT_EQ(loaded->getLocalizedText("fr"), "Deal 3 damage.");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
|
|
|
|||
|
|
@ -72,6 +72,18 @@ protected:
|
|||
return card;
|
||||
}
|
||||
|
||||
// Helper: build a single MTGJSON foreignData entry
|
||||
QJsonObject makeForeignEntry(const QString &language, const QString &name, const QString &text)
|
||||
{
|
||||
QJsonObject entry;
|
||||
entry["language"] = language;
|
||||
entry["name"] = name;
|
||||
if (!text.isEmpty()) {
|
||||
entry["text"] = text;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
NoopCardSetPriorityController *controller;
|
||||
OracleImporter *importer;
|
||||
CardSetPtr set;
|
||||
|
|
@ -872,6 +884,243 @@ TEST_F(OracleImporterTest, DisablingProgressReportingSuppressesScanEmissions)
|
|||
ASSERT_GT(emissions, 0);
|
||||
}
|
||||
|
||||
// Localized card text tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(OracleImporterTest, ImportsLocalizedTextForRequestedLanguage)
|
||||
{
|
||||
QJsonObject card = makeCard("Lightning Bolt");
|
||||
card["foreignData"] =
|
||||
QJsonArray{makeForeignEntry("German", "Blitzschlag", "Blitzschlag fügt 3 Schadenspunkte zu.")};
|
||||
QJsonArray cards{card};
|
||||
|
||||
importer->setCardLang("de");
|
||||
importer->importCardsFromSet(set, cards);
|
||||
importer->applyLocalizedData();
|
||||
|
||||
auto result = importer->getCardList().value("Lightning Bolt");
|
||||
ASSERT_FALSE(result.isNull());
|
||||
ASSERT_EQ(result->getLocalizedName("de"), "Blitzschlag");
|
||||
ASSERT_EQ(result->getLocalizedText("de"), "Blitzschlag fügt 3 Schadenspunkte zu.");
|
||||
// English identity untouched
|
||||
ASSERT_EQ(result->getName(), "Lightning Bolt");
|
||||
ASSERT_EQ(result->getText(), "Rules text.");
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, ImportsLocalizedNameAndTextForMultiFaceCards)
|
||||
{
|
||||
// MTGJSON reports multi-face cards (adventure/split/aftermath/prepare) as one
|
||||
// card object per face; every face carries the joined name but only its own
|
||||
// face's rules text in foreignData. The importer joins the per-face texts with
|
||||
// the same separator as the English merge.
|
||||
QJsonObject front = makeCard("Disruptive Stormbrood // Petty Revenge");
|
||||
front["layout"] = "adventure";
|
||||
front["faceName"] = "Disruptive Stormbrood";
|
||||
front["side"] = "a";
|
||||
front["foreignData"] = QJsonArray{
|
||||
makeForeignEntry("German", "Disruptive Stormbrood // Kleinliche Rache",
|
||||
"Fliegend\nWenn diese Kreatur ins Spiel kommt, zerstöre bis zu ein Artefakt oder eine "
|
||||
"Verzauberung deiner Wahl.")};
|
||||
QJsonObject back = makeCard("Disruptive Stormbrood // Petty Revenge");
|
||||
back["layout"] = "adventure";
|
||||
back["faceName"] = "Petty Revenge";
|
||||
back["side"] = "b";
|
||||
back["text"] = "Destroy target creature.";
|
||||
back["foreignData"] = QJsonArray{makeForeignEntry("German", "Disruptive Stormbrood // Kleinliche Rache",
|
||||
"Zerstöre eine Kreatur deiner Wahl mit Stärke 3 oder weniger.")};
|
||||
QJsonArray cards{front, back};
|
||||
|
||||
importer->setCardLang("de");
|
||||
importer->importCardsFromSet(set, cards);
|
||||
importer->applyLocalizedData();
|
||||
|
||||
auto result = importer->getCardList().value("Disruptive Stormbrood // Petty Revenge");
|
||||
ASSERT_FALSE(result.isNull());
|
||||
ASSERT_EQ(result->getLocalizedName("de"), "Disruptive Stormbrood // Kleinliche Rache");
|
||||
ASSERT_EQ(result->getLocalizedText("de"),
|
||||
"Fliegend\nWenn diese Kreatur ins Spiel kommt, zerstöre bis zu ein Artefakt oder eine Verzauberung "
|
||||
"deiner Wahl.\n\n---\n\nZerstöre eine Kreatur deiner Wahl mit Stärke 3 oder weniger.");
|
||||
// English identity untouched
|
||||
ASSERT_EQ(result->getName(), "Disruptive Stormbrood // Petty Revenge");
|
||||
ASSERT_EQ(result->getText(), "Rules text.\n\n---\n\nDestroy target creature.");
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, MultiFaceCardsWithoutCompleteForeignTextKeepEnglishText)
|
||||
{
|
||||
// Both faces must carry a foreignData text for the joined text; otherwise the
|
||||
// rules text stays English while the localized name (from a later complete
|
||||
// printing) is still applied.
|
||||
QJsonObject front = makeCard("Wear // Tear");
|
||||
front["layout"] = "split";
|
||||
front["faceName"] = "Wear";
|
||||
front["side"] = "a";
|
||||
front["foreignData"] = QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "Verschleiß-Text.")};
|
||||
QJsonObject back = makeCard("Wear // Tear");
|
||||
back["layout"] = "split";
|
||||
back["faceName"] = "Tear";
|
||||
back["side"] = "b";
|
||||
back["text"] = "Tear rules text.";
|
||||
back["foreignData"] = QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "")};
|
||||
QJsonArray cards{front, back};
|
||||
|
||||
importer->setCardLang("de");
|
||||
importer->importCardsFromSet(set, cards);
|
||||
importer->applyLocalizedData();
|
||||
|
||||
auto result = importer->getCardList().value("Wear // Tear");
|
||||
ASSERT_FALSE(result.isNull());
|
||||
// The joined name is still applied.
|
||||
ASSERT_EQ(result->getLocalizedName("de"), "Verschleiß // Zerrreißung");
|
||||
// The incomplete text must not become the card's localized text.
|
||||
ASSERT_TRUE(result->getLocalizedTexts().isEmpty());
|
||||
ASSERT_EQ(result->getText(), "Rules text.\n\n---\n\nTear rules text.");
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, DefaultLanguageSkipsForeignData)
|
||||
{
|
||||
QJsonObject card = makeCard("Lightning Bolt");
|
||||
card["foreignData"] =
|
||||
QJsonArray{makeForeignEntry("German", "Blitzschlag", "Blitzschlag fügt 3 Schadenspunkte zu.")};
|
||||
QJsonArray cards{card};
|
||||
|
||||
// cardLang defaults to "en" — foreignData must never be imported
|
||||
importer->importCardsFromSet(set, cards);
|
||||
importer->applyLocalizedData();
|
||||
|
||||
auto result = importer->getCardList().value("Lightning Bolt");
|
||||
ASSERT_FALSE(result.isNull());
|
||||
ASSERT_TRUE(result->getLocalizedNames().isEmpty());
|
||||
ASSERT_TRUE(result->getLocalizedTexts().isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, UnsupportedLanguageSkipsForeignData)
|
||||
{
|
||||
QJsonObject card = makeCard("Lightning Bolt");
|
||||
card["foreignData"] = QJsonArray{makeForeignEntry("xx", "Kochanie", "Grzmot uderza.")};
|
||||
QJsonArray cards{card};
|
||||
|
||||
importer->setCardLang("xx");
|
||||
importer->importCardsFromSet(set, cards);
|
||||
importer->applyLocalizedData();
|
||||
|
||||
auto result = importer->getCardList().value("Lightning Bolt");
|
||||
ASSERT_FALSE(result.isNull());
|
||||
ASSERT_TRUE(result->getLocalizedNames().isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, NonMatchingLanguageNotCollected)
|
||||
{
|
||||
QJsonObject card = makeCard("Lightning Bolt");
|
||||
card["foreignData"] = QJsonArray{makeForeignEntry("French", "Éclair", "L'Éclair inflige 3 blessures.")};
|
||||
QJsonArray cards{card};
|
||||
|
||||
importer->setCardLang("de");
|
||||
importer->importCardsFromSet(set, cards);
|
||||
importer->applyLocalizedData();
|
||||
|
||||
auto result = importer->getCardList().value("Lightning Bolt");
|
||||
ASSERT_FALSE(result.isNull());
|
||||
ASSERT_TRUE(result->getLocalizedNames().isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, HigherPrioritySetWinsForReprint)
|
||||
{
|
||||
// First printing in a reprint set, then another in a (more authoritative)
|
||||
// core set: the core set's German text must win even though it was seen later.
|
||||
QJsonObject reprintCard = makeCard("Lightning Bolt");
|
||||
reprintCard["foreignData"] = QJsonArray{makeForeignEntry("German", "Blitzschlag", "Älterer deutscher Text.")};
|
||||
CardSetPtr reprintSet =
|
||||
CardSet::newInstance(controller, "TS2", "Second Set", QString(), QDate(), CardSet::PriorityReprint);
|
||||
importer->setCardLang("de");
|
||||
importer->importCardsFromSet(reprintSet, QJsonArray{reprintCard});
|
||||
|
||||
QJsonObject primaryCard = makeCard("Lightning Bolt");
|
||||
primaryCard["foreignData"] =
|
||||
QJsonArray{makeForeignEntry("German", "Blitzschlag", "Blitzschlag fügt 3 Schadenspunkte zu.")};
|
||||
CardSetPtr primarySet =
|
||||
CardSet::newInstance(controller, "TS3", "Third Set", QString(), QDate(), CardSet::PriorityPrimary);
|
||||
importer->importCardsFromSet(primarySet, QJsonArray{primaryCard});
|
||||
importer->applyLocalizedData();
|
||||
|
||||
auto result = importer->getCardList().value("Lightning Bolt");
|
||||
ASSERT_FALSE(result.isNull());
|
||||
ASSERT_EQ(result->getLocalizedName("de"), "Blitzschlag");
|
||||
ASSERT_EQ(result->getLocalizedText("de"), "Blitzschlag fügt 3 Schadenspunkte zu.");
|
||||
ASSERT_EQ(importer->getCardList().size(), 1);
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, HigherPrioritySplitSetWinsForReprint)
|
||||
{
|
||||
// Split cards print each face as its own card object; the joined text is
|
||||
// collected per set with the same priority policy as single-face cards, so a
|
||||
// reprint set's German text must yield to the core set's even when reprints
|
||||
// are imported first.
|
||||
QJsonObject reprintFront = makeCard("Wear // Tear");
|
||||
reprintFront["layout"] = "split";
|
||||
reprintFront["faceName"] = "Wear";
|
||||
reprintFront["side"] = "a";
|
||||
reprintFront["foreignData"] =
|
||||
QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "Wear alter Text.")};
|
||||
QJsonObject reprintBack = makeCard("Wear // Tear");
|
||||
reprintBack["layout"] = "split";
|
||||
reprintBack["faceName"] = "Tear";
|
||||
reprintBack["side"] = "b";
|
||||
reprintBack["foreignData"] =
|
||||
QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "Tear alter Text.")};
|
||||
CardSetPtr reprintSet =
|
||||
CardSet::newInstance(controller, "TS2", "Second Set", QString(), QDate(), CardSet::PriorityReprint);
|
||||
importer->setCardLang("de");
|
||||
importer->importCardsFromSet(reprintSet, QJsonArray{reprintFront, reprintBack});
|
||||
|
||||
QJsonObject primaryFront = makeCard("Wear // Tear");
|
||||
primaryFront["layout"] = "split";
|
||||
primaryFront["faceName"] = "Wear";
|
||||
primaryFront["side"] = "a";
|
||||
primaryFront["foreignData"] =
|
||||
QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "Wear neuer Text.")};
|
||||
QJsonObject primaryBack = makeCard("Wear // Tear");
|
||||
primaryBack["layout"] = "split";
|
||||
primaryBack["faceName"] = "Tear";
|
||||
primaryBack["side"] = "b";
|
||||
primaryBack["foreignData"] =
|
||||
QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "Tear neuer Text.")};
|
||||
CardSetPtr primarySet =
|
||||
CardSet::newInstance(controller, "TS3", "Third Set", QString(), QDate(), CardSet::PriorityPrimary);
|
||||
importer->importCardsFromSet(primarySet, QJsonArray{primaryFront, primaryBack});
|
||||
importer->applyLocalizedData();
|
||||
|
||||
auto result = importer->getCardList().value("Wear // Tear");
|
||||
ASSERT_FALSE(result.isNull());
|
||||
ASSERT_EQ(result->getLocalizedName("de"), "Verschleiß // Zerrreißung");
|
||||
ASSERT_EQ(result->getLocalizedText("de"), "Wear neuer Text.\n\n---\n\nTear neuer Text.");
|
||||
ASSERT_EQ(importer->getCardList().size(), 1);
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, StartImportAppliesLocalizedData)
|
||||
{
|
||||
QJsonObject card = makeCard("Lightning Bolt");
|
||||
card["foreignData"] =
|
||||
QJsonArray{makeForeignEntry("Portuguese (Brazil)", "Raio", "Raio causa 3 de dano a qualquer alvo.")};
|
||||
QJsonObject dataSet;
|
||||
dataSet["code"] = "tst";
|
||||
dataSet["name"] = "Test Set";
|
||||
dataSet["type"] = "expansion";
|
||||
dataSet["releaseDate"] = "2024-01-01";
|
||||
dataSet["cards"] = QJsonArray{card};
|
||||
|
||||
QJsonObject root;
|
||||
root["data"] = QJsonObject{{"TST", dataSet}};
|
||||
|
||||
importer->setCardLang("pt");
|
||||
ASSERT_TRUE(importer->readSetsFromByteArray(QJsonDocument(root).toJson(QJsonDocument::Compact)));
|
||||
ASSERT_EQ(importer->startImport(), 1);
|
||||
|
||||
auto result = importer->getCardList().value("Lightning Bolt");
|
||||
ASSERT_FALSE(result.isNull());
|
||||
ASSERT_EQ(result->getLocalizedName("pt"), "Raio");
|
||||
ASSERT_EQ(result->getLocalizedText("pt"), "Raio causa 3 de dano a qualquer alvo.");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
|
|
|
|||
|
|
@ -549,6 +549,19 @@ TEST_F(SettingsDefaultsTest, CardsDisplay_ArrowDrawAnimation_Default)
|
|||
ASSERT_EQ(s.getArrowDrawAnimation(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, CardsDisplay_CardLang_Default)
|
||||
{
|
||||
CardsDisplaySettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getCardLang(), QString("en"));
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, CardsDisplay_CardLang_SetAndGet)
|
||||
{
|
||||
CardsDisplaySettings s(settingsPath, nullptr);
|
||||
s.setCardLang("de");
|
||||
ASSERT_EQ(s.getCardLang(), QString("de"));
|
||||
}
|
||||
|
||||
// --- VisualDeckStorageSettings ---
|
||||
|
||||
TEST_F(SettingsDefaultsTest, VisualDeckStorage_SortingOrder_Default)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue