Lingue

Ogni stringa che un utente può leggere vive in src/locales/, un file JSON per lingua. Pubblicare in una lingua diversa dall'inglese significa riempire un file, non modificare i componenti.

Costruito su next-intl. Nessun account, nessun servizio di traduzione, nessun fornitore: i file sono semplice JSON nella disposizione standard di quella libreria, quindi puntarci Crowdin o qualcos'altro più avanti è una tua scelta e oggi non costa niente.

Aggiungere una lingua

Due modifiche e un file.

// src/i18n/routing.ts
export const routing = defineRouting({
  locales: ["en", "it", "de"],
  defaultLocale: "en",
  localePrefix: "as-needed",
  localeDetection: false,
})
cp src/locales/en.json src/locales/de.json   # poi traduci i valori

La configurazione è tutta qui. Il selettore della lingua legge l'elenco da routing, quindi la nuova voce compare da sola, scritta nella propria lingua.

Non sei obbligato a tradurre tutto. Qualsiasi chiave lasciata fuori ricade sull'inglese invece di rendere un buco o il nome della chiave, e la fusione arriva fino in fondo: tradurre docs.title non svuota le sue sorelle. Pubblica un file con dieci chiavi dentro e le altre novecento restano in inglese.

Pubblicare in una lingua sola

Il kit arriva con due lingue, inglese e italiano, ed è voluto: uno scaffold che non ha mai girato con una seconda lingua è codice non testato, e i bug li troverebbe la prima persona che ne aggiunge una. Se al tuo prodotto ne serve una sola, togliere la seconda sono tre cancellazioni:

# 1. togli "it" da routing.locales in src/i18n/routing.ts
rm src/locales/it.json
rm content/docs/*.it.md

Il selettore della lingua sparisce da solo quando resta un locale soltanto, e gli indirizzi non si spostano: la lingua predefinita non ha mai avuto un prefisso.

Indirizzi

localePrefix: "as-needed" lascia la lingua predefinita esattamente dov'è: /pricing, non /en/pricing. Solo le altre lingue portano un prefisso, quindi aggiungere una lingua non invalida mai un indirizzo che hai già indicizzato.

localeDetection: false significa nessun reindirizzamento automatico da Accept-Language. Chi apre un link inglese legge l'inglese, e un link che incolli in una chat si risolve allo stesso modo per tutti quelli che lo aprono. Il cambio di lingua è un comando visibile, invece.

Una cosa da sapere se localizzi altre parti dell'app: il provider deve stare dentro [locale], con locale e messaggi passati esplicitamente. C'è un secondo NextIntlClientProvider nel layout radice, per le pagine che vivono fuori dal prefisso di lingua, e da solo non basta: un layout radice non viene ri-renderizzato quando cambia un segmento sotto di lui, quindi spostarsi tra le lingue lascia ogni componente client con quella che aveva al caricamento. La metà server della pagina cambia e la metà client no, il che sembra esattamente una traduzione che si è fermata a metà ed è invisibile a un ricaricamento.

Cosa non è localizzato, e perché

La dashboard, il pannello di amministrazione e le pagine di accesso stanno fuori da src/app/[locale]/. /it/dashboard non esiste. È una semplificazione e insieme una proprietà di sicurezza: src/proxy.ts riconosce le route private dal prefisso, e un segmento di lingua davanti a /dashboard impedirebbe a quei prefissi di corrispondere, quindi ogni route privata dovrebbe ricordarsi di toglierlo.

Le loro stringhe stanno comunque nei file dei messaggi, quindi localizzarle è lavoro di routing e non una riscrittura: sposta quei gruppi di route sotto [locale] e insegna a proxy.ts a togliere la lingua prima di confrontare.

Le pagine legali segnaposto tengono il testo dentro al codice. Tanto le sostituirai con le tue policy, e un documento legalmente vincolante assemblato da un file che sostituisce in silenzio un paragrafo mancante con un'altra lingua è peggio di uno scritto in una lingua sola di proposito.

I contenuti nel tuo database non sono coperti nemmeno loro: nomi e descrizioni dei piani arrivano dalle righe Plan, quindi rendono nella lingua con cui li hai inseriti. Se ti servono tradotti, leggili attraverso una chiave tipo plans.<slug>.description col valore del database come ripiego. Nessuna modifica allo schema, e ti resta fuori dai piedi finché non lo vuoi.

Copy che cambia tra un deploy e l'altro

Alcune sezioni si leggono diversamente sul sito di marketing del kit rispetto all'app che il kit diventa. Quelle portano entrambe le varianti sotto $kit e $product:

"docs": {
  "title": "Documentation",
  "$kit": { "intro": "Everything you need to go from git clone to production." },
  "$product": { "intro": "Guides for getting the most out of your app." }
}

KIT_SITE è fissato al momento del build, quindi solo un ramo può rendere. Il build butta via l'altro del tutto invece di spedirlo: senza, ogni pagina serializzerebbe copy che non può usare, e un clone si porterebbe dentro l'HTML le parole di marketing del kit, dove chiunque potrebbe leggerle. Le voci sorelle dei due marcatori sono condivise e restano.

Non ti servirà, a meno che tu non faccia girare due deploy da un solo codice. Se lo fai, i marcatori hanno il prefisso $ perché una chiave di messaggio non può iniziare per $, quindi non possono mai scontrarsi con una tua chiave vera.

Tradurre le guide in Markdown

Una guida tradotta è un file accanto al suo sorgente con la lingua nel nome:

content/docs/getting-started.md      # il sorgente
content/docs/getting-started.it.md   # la versione italiana

Non serve registrarla da nessun'altra parte. Una pagina senza variante per la lingua richiesta viene servita in inglese con una nota che lo dice, quindi tradurre una guida e lasciare le altre sei è uno stato previsto e non un lavoro a metà.

Le traduzioni portano il frontmatter anche dove il loro sorgente non ce l'ha, perché devono dichiarare due cose:

---
title: Primi passi
description: Da git clone all'app che gira, in una decina di minuti.
translated_from: getting-started.md
source_checksum: 3954fb6a1cdc
---

source_checksum è un hash breve del file inglese da cui la traduzione è stata scritta. Non devi calcolarlo: lancia il controllo qui sotto e ti stampa il valore da incollare.

Poi, prima di ogni rilascio:

npm run check:translations

Esce con un codice diverso da zero quando il file inglese è cambiato dopo che una traduzione l'ha registrato, e stampa il checksum aggiornato per ognuna che va sistemata.

Il marcatore è un hash del contenuto e non una revisione git, per una ragione precisa. Un hash di commit significa qualcosa solo dentro la storia in cui è nato, quindi smetterebbe di risolversi nel momento in cui cloni questo kit e cominci la tua. L'hash del file resta valido in qualsiasi repository e non richiede git, per questo funziona anche su un archivio dei sorgenti scaricato da una release. I fine riga sono normalizzati prima del calcolo, così lo stesso file dà lo stesso checksum su Windows e su Linux. Questo è il controllo che vale la pena tenere. Una traduzione vecchia non si rompe mai: continua a rendere, continua a sembrare finita, e diventa in silenzio le istruzioni di una versione precedente del tuo prodotto. L'unico modo per accorgersene leggendo è aprire i due file e confrontarli, quindi non lo fa nessuno.

Date, valuta e plurali

Tre cose che sembrano formattazione e si comportano come traduzione:

  • Le date passano da Intl.DateTimeFormat con la lingua attiva, mai da un "en-US" scritto a mano. Una lingua che scrive prima il giorno mostra altrimenti la data sbagliata, ed è sbagliata in un modo che si legge come giusto.
  • La valuta passa da Intl.NumberFormat, per lo stesso motivo.
  • I plurali usano ICU ({count, plural, one {# post} other {# posts}}), non un ternario. Il ternario è giusto per l'inglese e sbagliato per quasi tutte le altre lingue, diverse delle quali hanno tre o quattro forme.

Controlli che girano da soli

npm test fallisce quando una chiave letta dal codice manca da en.json, quando una chiave in en.json non è letta da nessuno, e quando un'altra lingua dichiara una chiave che l'inglese non ha. Tutti e tre sono fallimenti silenziosi altrimenti: una chiave mancante rende come il nome della chiave, il copy morto viene tradotto con cura, e un errore di battitura in un file di traduzione non fa assolutamente niente pur sembrando a posto.

Star on GitHub

Salvalo nei tuoi preferiti con una

Serve aiuto?