Indietro

i18n senza file di traduzione

Jackie Chen avatarJackie Chen
guideinternationalizationnextjsi18ngt-nexttranslation-filesdeveloper-experience

Chiunque abbia internazionalizzato un'app JavaScript conosce il flusso di lavoro. Installi una libreria i18n, crei un file en.json, estrai dai componenti tutte le stringhe visibili agli utenti, assegni a ciascuna una chiave e usi quella chiave al posto della stringa originale. Poi duplichi quel file JSON per ogni lingua supportata. es.json, fr.json, ja.json.

All'inizio va bene. Trenta stringhe, tre lingue, 90 voci.

Poi la tua app cresce. Sei mesi dopo hai 400 stringhe e 12 lingue. 4.800 voci distribuite su una dozzina di file. Uno sviluppatore aggiunge una nuova funzionalità, scrive cinque nuove stringhe e si dimentica di aggiornare tre file di traduzione. Nessuno se ne accorge finché un utente a Tokyo non si ritrova testi di fallback in inglese in un'interfaccia giapponese. Qualcuno propone di acquistare un sistema di gestione delle traduzioni.

Il costo della gestione dei file

I file di traduzione creano un carico di manutenzione che aumenta con il numero di lingue supportate. Modifichi una stringa e devi aggiornare una dozzina di file. Quei file finiscono per andare fuori sync. Tenerli allineati richiede strumenti, automazione o disciplina.

L'ecosistema dei workaround racconta bene la situazione: estensioni di VS Code che generano automaticamente chiavi di traduzione, generatori di tipi che validano il tuo JSON rispetto alle interfacce TypeScript, linter per le chiavi inutilizzate, controlli CI che verificano che tutte le impostazioni regionali siano sync. Non costruisci questo tipo di strumenti per un flusso di lavoro che funziona bene.

La tua UI dice una cosa, i tuoi file di traduzione ne dicono un'altra e una mappatura basata su chiavi tiene insieme le due parti:

function CheckoutSummary({ itemCount, discount }) {
  const { t } = useTranslation('checkout');

  return (
    <div>
      <h2>{t('summary.title')}</h2>
      <p>{t('summary.item_count', { count: itemCount })}</p>
      {discount && <p>{t('summary.discount_applied', { percent: discount })}</p>}
      <p>{t('summary.total_label')}</p>
      <small>{t('summary.tax_notice')}</small>
    </div>
  );
}

Per capire cosa visualizza questo componente, dovresti aprire checkout.json, trovare il namespace summary e incrociare cinque chiavi. Su centinaia di componenti, la code review diventa un esercizio di continuo passaggio da un file all'altro.

La sintassi dei messaggi ICU peggiora ulteriormente la situazione. Il formato che librerie come i18next, react-intl e next-intl usano per plurali, genere e interpolazione è un mini-linguaggio a sé stante incorporato nelle stringhe JSON. Una parentesi graffa fuori posto in {count, plural, one {# item} other {# items}} non verrà rilevata fino al runtime. Esistono linter per questo, ma la maggior parte dei team non li usa. Il codice inline, al contrario, viene controllato dal compilatore TypeScript e dall'IDE nel momento stesso in cui lo digiti.

Tradurre direttamente nel codice

E se non estraessi affatto le stringhe?

import { T } from 'gt-react';

function CheckoutSummary({ itemCount, discount }) {
  return (
    <T>
      <div>
        <h2>Order Summary</h2>
        <p>You have {itemCount} items in your cart.</p>
        {discount && <p>{discount}% discount applied!</p>}
        <p>Total (before tax):</p>
        <small>Tax calculated at checkout.</small>
      </div>
    </T>
  );
}

Il componente <T> in gt-react contrassegna un blocco di JSX da tradurre. L'inglese rimane nel tuo componente. Quando un utente visita il sito in spagnolo, il contenuto all'interno di <T> viene sostituito con l'equivalente in spagnolo. Struttura e formattazione vengono mantenute.

Non c'è t('checkout.summary.title'), né en.json, né file JSON separati per ogni impostazione regionale da tenere sincronizzati.

Traduzioni come output di build

Vengono generate in fase di deploy. La GT CLI analizza la tua base di codice alla ricerca di tutto ciò che si trova all'interno dei componenti <T> e genera traduzioni per ogni lingua di destinazione. L'output finisce in una directory inclusa in .gitignore, come il CSS compilato o il JavaScript incluso nel bundle.

{
  "defaultLocale": "en",
  "locales": ["es", "fr", "ja", "de", "ko", "zh"],
  "files": {
    "gt": {
      "output": "public/_gt/[locale].json"
    }
  }
}
public/_gt/

In sviluppo, le traduzioni avvengono on-demand. Modifica una stringa, aggiorna la pagina e la vedrai subito in giapponese. In produzione, tutto viene pre-generato e distribuito tramite una CDN. I file di traduzione esistono ancora sul disco, ma sono artefatti generati in output, non qualcosa che scrivi o mantieni.

Cosa cambia in pratica

La differenza si nota più nel lavoro quotidiano che nei diagrammi architetturali.

Una PR che aggiunge una nuova sezione a una pagina si legge esattamente per quello che è: una PR che aggiunge una nuova sezione a una pagina. Chi la revisiona vede le parole vere, non checkout.summary.discount_applied_notice. La code review non richiede più di affiancare un file JSON.

Il refactoring è meno doloroso. Rinomina un componente, spostalo, dividilo in più parti. Le traduzioni seguono il contenuto perché non ci sono chiavi da rimappare. Aggiungere una lingua richiede una modifica di configurazione di una sola riga. Eliminare un componente significa che le sue traduzioni smettono semplicemente di essere generate, invece di lasciare chiavi orfane sparse in 15 file che nessuno ha voglia di ripulire.

Dove stanno andando le librerie i18n

La tendenza in tutto l'ecosistema va verso le stringhe inline. Le prime librerie i18n come i18next e react-intl sono nate quando la traduzione automatica non era ancora fattibile e ogni stringa doveva essere affidata a un traduttore umano. I dizionari avevano senso come formato di interscambio. Quel vincolo non esiste più, e il costo in termini di developer experience di mantenere file di stringhe paralleli è sempre più difficile da giustificare.

next-intl ha aggiunto chiamate t() non basate su dizionari accanto alla sua modalità con dizionario. Il Compiler di Lingui estrae i messaggi in fase di build da tagged template literal inline. Paraglide segue una strada diversa, compilando i file di messaggi in funzioni tree-shakeable per ciascuna impostazione regionale. Gli approcci sono diversi, ma in tutti i casi il contenuto si sta avvicinando al componente. GT porta questa idea alle sue estreme conseguenze: il tuo JSX è la fonte autorevole e la traduzione avviene in fase di compilazione.

Il compromesso

Quando scrivi contenuti inline, stai scrivendo nella tua lingua madre. La struttura dei componenti, l'andamento delle frasi, il flusso della UI riflettono tutti il modo in cui pensi in inglese (o in qualsiasi altra lingua di partenza). Un approccio basato su dizionario come next-intl è, per sua natura, più indipendente dalla lingua, perché il componente non contiene mai una frase reale in nessuna lingua, ma solo una chiave che rimanda altrove.

Ma la maggior parte degli sviluppatori pensa già in una lingua quando realizza un'interfaccia. Il layout, i testi, le etichette dei pulsanti vengono tutti concepiti prima in inglese. Questa impostazione è insita nel design, indipendentemente dal fatto che le stringhe siano inline o in un file JSON. Pensiamo che il framework di i18n debba adattarsi al tuo modo reale di lavorare. Costruisci l'app in modo naturale e lascia che il framework si occupi della traduzione, invece di astrarre i contenuti in chiavi per il solo gusto della neutralità linguistica.

Guida introduttiva

npx gt@latest init

La procedura guidata di configurazione configura il progetto, installa le dipendenze e imposta l'hot reload delle traduzioni per lo sviluppo. Per una panoramica completa, consulta la guida Quickstart.

gt-react è open source. Per Next.js App Router, usa gt-next. Per React Native, usa gt-react-native.