Skip to content

F6.16 — Japanese cards as preview-then-promote (pre-international releases) #602

@jbourdin

Description

@jbourdin

Feature ID: F6.16
Section: F6 — Card Data & Validation
Priority: Medium
Depends on: F6.2 (TCGdex enrichment), F6.10 (Card identity & printing model), F6.13 (Incremental TCGdex sync)


Context

Players who want to register a Japanese deck list today (e.g. cards from SV10 / Mega Brave / Mega Symphonia) can't, because the TCGdex sync, deck-list parser, and card enrichment are hardwired to English (/v2/en). The goal is to allow JP cards as previews in deck lists while they wait for their international release, then auto-promote them to first-class cards (with a CardIdentity and full Expanded legality) when the EN counterpart syncs in.

This is feasibility-confirmed: TCGdex's Japanese coverage is broad enough (172 ja sets vs. 208 en sets, full card data including attacks/abilities/regulation marks/images) and the local schema already has the right slots (TcgdexCard.name is a multilingual JSON, DeckCard.cardLocale exists, Deck.languages exists).

TCGdex Reality Check (verified against live API)

Aspect Finding Source
Language code for Japanese ja (ISO 639-1), not jp API error from /v2/jp/sets lists supported langs: en, fr, es, es-mx, it, pt, pt-br, pt-pt, de, nl, pl, ru, ja, ko, zh-tw, id, th, zh-cn
Number of JA sets 172 GET /v2/ja/sets
Card payload shape Single-locale flat strings (e.g. name: "クヌギダマ"), not a {en, ja} object GET /v2/ja/cards/SV10-001
Available JP fields name, hp, types, stage, attacks[{cost,name,damage}], weaknesses, retreat, regulationMark, legal.{standard,expanded}, image, illustrator, rarity, dexId, description same
legal.expanded on fresh JP releases false (e.g. SV10-001) same
releaseDate on JP sets often missing same
Pricing on JP cards usually null same

Implication for our TcgdexCard.name = {"en": "...", "fr": "..."} JSON column: we must convert flat JA responses into the same shape ourselves ({"ja": "クヌギダマ"}), since the API doesn't deliver multilingual objects.

Approach — Preview Now, Auto-Promote on EN Release

Two-phase lifecycle for a JP-released card:

Phase A — Preview (international release does not exist yet)

  • Sync /v2/ja/... weekly. Upsert TcgdexCard rows keyed by their JP ID (e.g. SV10-001).
  • Store JA fields in the existing JSON columns: name['ja'], abilities[*].name['ja'], attacks[*].name['ja'], effect['ja'], etc.
  • Set isExpandedLegal = true ("anticipated Expanded" — overrides TCGdex's legal.expanded: false).
  • Do not create CardIdentity / CardPrinting for these rows. A new boolean TcgdexCard.isPreview distinguishes them.
  • Deck-list parser accepts JP set codes; deck-list validator allows preview cards but flags the deck as hasPreviewCards = true (so we can render a banner: "this deck contains cards awaiting international release").

Phase B — Promotion (EN sync sees the card)

When SyncTcgdexCardHandler ingests the EN payload for a card whose TCGdex ID already exists with isPreview = true:

  1. Merge name['en'] (and fr, etc.) into the existing JSON columns alongside name['ja'].
  2. Refresh isExpandedLegal from legal.expanded (TCGdex's official value now becomes the source of truth).
  3. Flip isPreview = false and run the normal CardEnricher flow to create CardIdentity + CardPrinting.
  4. Re-enrich every DeckCard that referenced this card (they previously had no identity link); existing decks now have full validation and Cardmarket export.

Promotion fires from a TcgdexCardPromotedEvent so deck enrichment and notifications stay decoupled.

Critical Files to Modify

1. API client — make locale a parameter

  • src/Service/Tcgdex/TcgdexApiClient.php (line 32, line 224, 299, 330, 464): remove const BASE_URL = 'https://api.tcgdex.net/v2/en', replace with private const string BASE_URL_TEMPLATE = 'https://api.tcgdex.net/v2/%s', and pass a string $locale = 'en' argument through findCard(), findCardByName*, fetchSet(). Default stays 'en' so existing call sites don't change.

  • Mirror same change in every sync handler with a hardcoded URL:

    • src/MessageHandler/SyncTcgdexSeriesHandler.php:39
    • src/MessageHandler/SyncTcgdexSerieHandler.php
    • src/MessageHandler/SyncTcgdexSetHandler.php
    • src/MessageHandler/SyncTcgdexCardHandler.php:40
    • src/MessageHandler/BuildSetMappingsHandler.php (if any)

    (grep -rn "api.tcgdex.net/v2/en" src/ finds them all)

2. Sync messages — add locale

  • src/Message/SyncTcgdexSeriesMessage.php, SyncTcgdexSerieMessage.php, SyncTcgdexSetMessage.php, SyncTcgdexCardMessage.php — add an optional string $locale = 'en' constructor argument. Handlers route to the right /v2/{locale}/....
  • src/Command/SyncTcgdexCommand.php — accept --locale=ja (repeatable) and dispatch the cascade once per locale.
  • src/Message/SyncTcgdexCompleteMessage.php — orchestrate the cascade per locale.

3. TcgdexCard — additive changes

  • src/Entity/TcgdexCard.php:
    • New generated column nameJa (mirror lines 58–61 for nameFr). Migration: ALTER TABLE tcgdex_card ADD COLUMN name_ja VARCHAR(100) GENERATED ALWAYS AS (name->>'$.ja') STORED, ADD INDEX idx_tcgdex_card_name_ja (name_ja);
    • New column bool $isPreview = false with index. Stores whether this row has been seen in EN yet.
  • src/Service/Tcgdex/TcgdexCardHydrator.php: the hydrator currently assumes flat multilingual objects from EN. When the source payload is a flat string (because we queried /v2/ja/), wrap it as [$locale => $value] and merge with any existing JSON to preserve other locales already stored.

4. Promotion logic — new service

  • src/Service/Tcgdex/CardPromotionService.php (new):
    • promote(TcgdexCard $card): void — flips isPreview to false, calls CardEnricher::enrich() to create the CardIdentity + CardPrinting, then dispatches TcgdexCardPromotedEvent.
  • src/EventListener/ReEnrichDeckCardsOnPromotionListener.php (new): subscribed via #[AsEventListener], finds every DeckCard where cardLocale = 'ja' and tcgdexCardId = <promoted id>, points them at the now-existing CardPrinting, recomputes deck validation flags.
  • SyncTcgdexCardHandler — at the end of an EN sync, if the just-upserted TcgdexCard has isPreview = true, call CardPromotionService::promote().

5. Deck list parser & validator

  • src/Service/DeckListParser.php:
    • Set-code parsing is already locale-agnostic (it grabs SV10 001 regardless of language). No change to the regex, but add JA name lookups to the basic-energy table around lines 105–113 (already partially there) and Japanese set-code prefix recognition if PTCG's JP export uses different codes.
    • When a line resolves only via the JA TcgdexCard, set DeckCard.cardLocale = 'ja'.
  • src/Service/DeckListValidator.php:
    • Skip the strict format-legality check (isExpandedLegal) when DeckCard.cardLocale = 'ja' and the matched TcgdexCard.isPreview = true — the "anticipated Expanded" override.
    • Emit a new validation info message (not error): app.deck.validation.contains_preview_cards.
  • src/Entity/Deck.php: derived flag hasPreviewCards (computed at save time, like existing aggregates) so the UI can render a banner without re-scanning DeckCards.

6. UI surfacing

  • Add a Twig partial templates/deck/_preview_banner.html.twig that renders when deck.hasPreviewCards is true — explains the deck contains JP-only cards awaiting international release.
  • Card-tile component (likely under assets/components/): when cardLocale = 'ja', badge with a small "JP" chip and the image from getImageUrl(...) automatically picks the ja/ CDN path (already supported by imageBaseUrl).
  • Translations: add app.deck.preview.banner.title/body and app.card.locale.ja to translations/messages.en.xlf and messages.fr.xlf (no card-name translation — card names stay in their native locale).

7. Format-legality nuance

  • src/Repository/CardPrintingRepository.php — the EXPANDED_ERA_START filter still applies, but JA-only previews never reach this code path (no CardPrinting exists for them). No change needed.
  • TcgdexCard.isExpandedLegal for JA previews: set to true on import (override the API). On promotion, the EN payload's legal.expanded becomes authoritative — if it turns out the card was banned in Expanded, the deck banner switches to "no longer Expanded-legal" via the existing banned-card pipeline.

8. Reused functions / utilities — do not reinvent

  • TcgdexCard::getLocalizedName(string $locale) (line 181) already handles fallback chains — reuse it everywhere instead of new locale-specific helpers.
  • Archetype::getLocalizedName(), MenuCategory::getLocalizedName() follow the same pattern — they're the reference style for locale-aware accessors.
  • TcgdexSetAlias (already in repo) is the right home for any JP-specific set-code translation if PTCGL exports use codes that differ from TCGdex's SV10. Don't create a new alias mechanism.
  • CardEnricher::enrich() should be called by CardPromotionService rather than duplicating its logic.

Verification

End-to-end manual test (after make migrations and make assets):

  1. Sync a JP-only set: symfony console app:tcgdex:sync --locale=ja --set=SV10. Confirm rows appear in tcgdex_card with name->>'$.ja' populated, is_preview = 1, and is_expanded_legal = 1.
  2. Confirm no identity created: SELECT COUNT(*) FROM card_identity ci JOIN card_printing cp ON cp.card_identity_id = ci.id WHERE cp.tcgdex_card_id LIKE 'SV10-%'; → 0.
  3. Paste a JP deck: register a new deck, paste a list referencing SV10 001 (using its JP card name from the API). Expect: deck saves; banner "contains preview cards" appears; deck export disabled or marked "preview".
  4. Simulate EN release: symfony console app:tcgdex:sync --locale=en --set=SV10 (or whatever EN code shadows it, once mapped via TcgdexSetAlias). Expect: same TCGdex IDs get name['en'] merged in, is_preview flips to 0, CardIdentity rows are created, and the preview banner disappears from the deck view.
  5. Re-run the deck-list validator on the existing deck — should now report full Expanded legality and Cardmarket export should work.

Tests to add

  • tests/Service/Tcgdex/TcgdexCardHydratorTest.php — flat JA payload is wrapped into {"ja": ...} and merged with existing {"en": ...} without overwriting.
  • tests/Service/Tcgdex/CardPromotionServiceTest.phpisPreview flip + identity creation + event dispatch.
  • tests/Service/DeckListValidatorTest.php — JP preview card bypasses Expanded check; deck flagged with hasPreviewCards.
  • tests/Functional/SyncTcgdexCardHandlerTest.php — EN sync over an existing JA preview triggers promotion.

What This Plan Deliberately Does Not Do

  • No new "Japanese" format: JP cards live in "anticipated Expanded" rather than getting their own format.
  • No JP card-name translation: card names stay in their native locale. UI shows getLocalizedName($userLocale) with a fallback chain.
  • No TcgdexSet releaseDate patching: TCGdex's JA releaseDate is sparse but we don't need it — JP-only cards are identified by their absence from the EN dataset, not by date.
  • No fresh JA pricing scraping: Cardmarket has no JP listings; cardmarketProductId stays null on previews and Cardmarket export is disabled for them.

Open Questions for Follow-up (do not block this feature)

  • Some recent JP sets use codes that differ from the eventual EN code (e.g. JP "SV6a" → EN "TWM"). Promotion-on-sync needs a robust mapping; the existing TcgdexSetAlias table is the right home, but the seed data may need expanding. Worth a small spike after Phase A ships, since previews work without it (each card is keyed by its full TCGdex ID).
  • TCGdex's coverage of brand new JP sets (within days of release) varies. Status of any given set should be checked at https://tcgdex.dev/status before relying on it.

Acceptance Criteria

  • TcgdexApiClient accepts a locale parameter; default behaviour unchanged for existing en callers.
  • app:tcgdex:sync --locale=ja --set=<id> syncs a JP set into tcgdex_card with is_preview = 1 and no CardIdentity rows created.
  • Deck-list parser resolves a pasted line 4 クヌギダマ SV10 001 to the JA TcgdexCard, storing DeckCard.cardLocale = 'ja'.
  • Deck-list validator allows preview cards under Expanded, sets Deck.hasPreviewCards = true, and surfaces an info-level banner.
  • When the EN counterpart syncs, is_preview flips to 0, CardIdentity + CardPrinting are created, and existing DeckCard rows are re-linked automatically (banner disappears, Cardmarket export becomes available).
  • Unit + functional tests listed above pass.
  • docs/technicalities/tcgdex_sync.md and docs/technicalities/enrichment.md updated; new doc entry in features.md for F6.16.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions