---
name: sito-lingue
description: "Il sito in cinque lingue dal 19/9: come funziona il selettore, dove vivono le traduzioni, la regola sugli URL dei tag, e il passo in più ad ogni pubblica"
metadata:
  node_type: memory
  type: project
  originSessionId: d9cb587f-e008-4bf3-b0fb-b6182f56fea4
  modified: 2026-09-19T08:08:20.637Z
---

Deciso con Paolo il 19/9/2026: il sito [[sito-bg-perspective]] parla anche inglese, francese,
tedesco e spagnolo, oltre all'italiano. Selettore nel footer, icona a bandierina che apre un
menù con le altre quattro. Lingua scelta in automatico dal browser di chi visita, default
inglese se non c'è una corrispondenza.

**Gli URL**: l'italiano resta senza prefisso, cioè `bg-perspective.it/gioco/calimala` mostra la
lingua rilevata (o quella scelta col selettore, salvata in un cookie). Le altre quattro vivono
su `/it`, `/en`, `/fr`, `/de`, `/es` espliciti — **sì, anche l'italiano ha un suo prefisso
esplicito**, per chi lo vuole linkare diretto. Scelto così per non rompere i link già in giro
(commenti BGG, bio Instagram) che puntano al percorso senza prefisso.

## Come è fatto

- `src/params/lang.js` + route sotto `src/routes/[[lang=lang]]/...`: parametro opzionale,
  `params.lang` è il codice quando c'è il prefisso, `undefined` altrimenti.
- `src/hooks.server.js` calcola la lingua vera a ogni richiesta: prefisso esplicito nell'URL →
  cookie `lingua` → `Accept-Language` del browser → default inglese. Il prefisso esplicito
  aggiorna anche il cookie (un anno).
- `src/routes/+layout.server.js` la mette in `data.lingua` per tutte le pagine. **Importante**:
  il load legge `url.pathname` (anche se non gli serve) solo per farsi ricalcolare da SvelteKit
  a ogni cambio pagina — altrimenti la navigazione client-side fra `/it` e `/fr` non aggiorna
  niente, perché SvelteKit non sa che `locals.lingua` dipende dall'URL. **Trovato con un test
  vero nel browser**: cliccando la bandiera l'URL cambiava ma il testo restava nella lingua di
  prima, finché non ho aggiunto quella riga.
- `src/lib/i18n.js`: le cinque lingue, il dizionario `T` dei testi fissi dell'interfaccia (circa
  90 chiavi, alcune funzioni tipo `pesoDi(numero)` o `inN(giocatori)` per i plurali/formati che
  cambiano da lingua a lingua), `dataLocale()` per le date, `senzaPrefisso()` e `alternates()`
  per costruire i link fra lingue e gli hreflang.
- `src/lib/Lingua.svelte`: il selettore nel footer, stesso pattern a tendina di `Ordina.svelte`.

## Il contenuto dei giochi: tabella `traduzioni`

L'italiano resta la lingua sorgente nella tabella `giochi` (schema in `sito/schema.sql`). Le
altre quattro stanno in **`traduzioni`** (slug, lingua, e gli undici campi traducibili:
`edizione_it`, `testo_nel_gioco`, `categorie`, `meccaniche`, `peso_etichetta`, `cos_e`,
`come_gira`, `apertura`, `chicca`, `domanda`, `domanda_sotto`, `alt`), una riga per gioco e
lingua. `src/lib/db.js` legge sempre l'italiano e ci sovrappone la traduzione se la lingua
richiesta non è `it` e la riga esiste. Titolo, autori, editore restano sempre quelli italiani
(nomi propri, non si traducono).

**Il glossario per categorie, meccaniche, peso, "testo nel gioco"** sta in
`_instagram/dati/glossario_lingue.json`: mappa ogni etichetta italiana già usata sul sito
(28 categorie, 64 meccaniche, i 4 pesi, i 4 valori di "testo nel gioco", più "Non esiste" e
"Ghenos, annunciata") alle sue quattro traduzioni. **Se un gioco nuovo porta una categoria o
una meccanica non ancora nel glossario, va aggiunta lì per prima cosa**, con la stessa
traduzione che si userebbe per qualsiasi altro gioco: altrimenti la stessa meccanica rischia di
finire tradotta in due modi diversi su due giochi, e le pagine tag smettono di raggrupparli
insieme.

## Gli URL dei tag non cambiano con la lingua — regola trovata da un bug vero

**Scoperto il 19/9 con un test nel browser**: la prima versione faceva slugificare
l'etichetta *tradotta* per costruire l'URL del tag (`/meccanica/mehrheiten` in tedesco). Risultato:
aprendo lo stesso URL con un'altra lingua attiva, o un link condiviso in italiano visitato da
un cookie tedesco, la pagina dava **404**, perché lo slug non esisteva più in quella lingua.

**Corretto così**: lo slug del tag (`/categoria/…`, `/meccanica/…`, `/peso/…`) è **sempre**
quello dell'etichetta italiana, qualunque sia la lingua mostrata. `perTag()` in `db.js` cerca i
giochi confrontando le etichette italiane, poi traduce solo il nome mostrato in pagina (trova
la posizione dell'etichetta italiana nell'array del primo gioco e legge la stessa posizione
nell'array tradotto — le traduzioni mantengono l'ordine dell'originale). Stesso principio per i
link **dalla** scheda di un gioco ai tag: `perSlug()` porta anche `categorieSlug`,
`meccanicheSlug`, `pesoSlug` (gli slug italiani, parallela all'array che si vede, anche quando
quello che si vede è tradotto), e il template li usa per gli `href`, mai `slugifica()` sul
testo mostrato. **How to apply**: qualunque nuovo link a una pagina tag deve passare per questi
slug paralleli, mai dallo slug dell'etichetta che si sta mostrando in quel momento.

## SEO: hreflang e sitemap

Ogni pagina ha in `<head>` un `<link rel="alternate" hreflang="…">` per ciascuna delle cinque
lingue più uno `hreflang="x-default"` (tutti e due puntano all'indirizzo senza prefisso, quello
inglese: è quello che Googlebot vede di default, dato che di solito non manda un
`Accept-Language` che combaci con una delle quattro). `alternates()` in `i18n.js` costruisce
questi cinque indirizzi da un pathname.

`sitemap.xml` elenca **ogni pagina cinque volte** (una per lingua, prefisso compreso per
l'italiano), ciascuna con i quattro fratelli come `<xhtml:link rel="alternate" hreflang="…">`
annidati (lo standard che Google raccomanda per i siti multilingua). Verificato che il numero
di voci sia `pagine × 5` e che i tag hreflang comparissero davvero, con `curl` sul sito in
locale prima del deploy.

## Il passo in più ad ogni pubblica

Deciso con Paolo: ogni nuovo post tradotto **subito**, non in un arretrato da smaltire dopo.
Quindi da ora, dopo `sito.py pubblica <chiave> ...` (vedi [[pipeline-pubblicazione]]), il passo
successivo è:

1. Traduco io (Claude) i campi in `CAMPI_TRADOTTI` nelle quattro lingue, usando il glossario di
   `_instagram/dati/glossario_lingue.json` per categorie/meccaniche/peso/testo-nel-gioco (mai
   inventarne di nuove se il termine già esiste nel glossario) e un tono naturale, non letterale,
   per i testi liberi (`cos_e`, `come_gira`, `apertura`, `chicca`, `domanda`, `domanda_sotto`,
   `edizione_it` quando non è un nome proprio). **Vale sempre la regola di
   [[mai-inventare-meccaniche-non-verificate]]: si traduce quello che c'è, non si aggiungono
   fatti nuovi.**
2. Per ciascuna lingua scrivo un JSON con gli undici campi (stesso schema di
   `CAMPI_TRADOTTI` in `codice/sito.py`) e lancio
   `python3 codice/sito.py traduci <slug> <lingua> <file.json>` — quattro volte, una per lingua.
3. Se il gioco porta una categoria o meccanica nuova, si aggiorna prima il glossario (vedi sopra).

**Il primo carico, il 19/9**: 18 giochi (gli 8 già usciti più i 10 in coda) tradotti in blocco,
usando quattro agenti in parallelo (uno per lingua) per non riempire il turno di testo da
rileggere, con lo stesso glossario dato a tutti e quattro per restare coerenti. Verificato
prima di caricare: dizionario coerente sui campi controllati, nessuna nota di dubbio dagli
agenti, un confronto a campione (Calimala) su tutte e quattro le lingue.

## Verificato dal vivo in Chrome il 19/9

Home in italiano (rilevata dal browser), passaggio a francese dal footer (client-side,
compreso il bug del layout server risolto), scheda di un gioco in francese e poi tedesco
(compresi i tag/meccaniche tradotti, i link "vicini"/"stessa famiglia" ancora funzionanti),
editore e autore in tedesco/spagnolo, pagina tag in tedesco con la correzione dello slug. Nessun
errore in console. `npm run build` passa (adapter Vercel, i soliti avvisi su moduli nativi di
libsql/ws non sono un problema, c'erano anche prima).
