Aggiornare le traduzioni senza rifare il deploy: caricamento a runtime, cache, e perché gli utenti vedono testi vecchi
Per aggiornare le traduzioni senza rifare il deploy, portatele fuori dal build: l'app carica i suoi file di traduzione a runtime da un URL (il vostro server, un bucket di object storage o una CDN per le traduzioni), quindi cambiare un testo significa cambiare un file su quell'URL invece di rilasciare una nuova versione. Con i18next si tratta di un plugin backend al posto delle resources incluse nel bundle. La velocità con cui gli utenti vedono una modifica dipende poi dalla cache: ogni cache del browser, service worker, livello localStorage e CDN lungo il percorso può continuare a servire il vecchio file fino alla propria scadenza.
Questo articolo descrive i tre modi in cui i team gestiscono oggi gli aggiornamenti delle traduzioni, le cache dietro "ho fatto il deploy e il testo non è cambiato", i casi in cui il bundling resta la scelta migliore, e come funziona in Locize.
- Le traduzioni nel bundle si comportano come codice: se il JSON delle traduzioni viene importato nel bundle o passato a i18next come resources, ogni modifica al testo richiede un build e un deploy, oppure un rilascio sullo store.
- Il caricamento a runtime separa i testi dai rilasci: i18next carica i namespace tramite un plugin backend, e i18next-http-backend li recupera da qualsiasi URL configuriate.
- I testi vecchi sono quasi sempre colpa di una cache: la cache HTTP del browser (Cache-Control max-age), le cache dei service worker (che non scadono mai da sole), i backend di cache su localStorage (7 giorni di default in i18next-localstorage-backend) e i nodi della CDN.
- Le risorse nel bundle disattivano il backend: i18next carica tramite un backend solo se all'inizializzazione non viene passata l'opzione resources, oppure se partialBundledLanguages è attivo.
- Per alcune app il bundling resta giusto: app offline-first, pagine pre-renderizzate, binari di app native e testi riservati.
Perché cambiare un testo di solito significa un rilascio
La maggior parte delle app parte con le traduzioni nel repository, accanto al codice, importate nel bundle e passate a i18next all'inizializzazione:
import i18next from 'i18next'
import en from './locales/en/common.json'
import de from './locales/de/common.json'
i18next.init({
lng: 'en',
fallbackLng: 'en',
defaultNS: 'common',
resources: {
en: { common: en },
de: { common: de }
}
})È semplice, veloce e funziona offline. Ma significa anche che il bundler tratta le traduzioni esattamente come codice: finiscono in un file JavaScript, e l'unico modo per cambiare una sola parola è un nuovo build. Una traduttrice corregge un refuso, uno sviluppatore recupera il file, apre una pull request, aspetta la CI e fa il deploy. Sul mobile nativo si aggiunge la revisione dello store delle app. Il ciclo di rilascio dei vostri testi è saldato a quello del vostro codice, cioè l'opposto di ciò che serve alla localizzazione continua.
La stessa configurazione nasconde una seconda trappola. Secondo le opzioni di configurazione di i18next, resources è pensato per configurazioni senza backend, ed è partialBundledLanguages: true a consentire risorse nel bundle e backend insieme. I team che aggiungono un plugin backend in un secondo momento lasciando al loro posto le risorse nel bundle ottengono un backend che non viene mai chiamato: uno dei motivi classici delle "traduzioni che non si aggiornano dopo il deploy". Anche con partialBundledLanguages, una lingua e un namespace già presenti nello store grazie al bundle non vengono ricaricati, a meno che non lo chiediate esplicitamente con i18next.reloadResources().
Come gestiscono la cosa i team oggi
1. File nel bundle più un nuovo deploy
L'approccio predefinito. I file di traduzione vivono nel repository, uno script o un job di CI recupera le nuove traduzioni da dove lavorano i traduttori, e il deploy successivo le rilascia. I vantaggi: nessuna richiesta a runtime, supporto offline dal primo avvio e testi versionati insieme al codice. Il costo: ogni correzione aspetta un rilascio, e il ritmo di rilascio della vostra piattaforma più lenta diventa il ritmo di aggiornamento dei vostri testi. Come questo passaggio di consegne si scontra con il deploy continuo è descritto in come i workflow di traduzione a lotti rompono il deploy continuo.
2. Caricamento a runtime dal vostro server o da un object storage
L'app recupera i file di traduzione all'avvio da un URL che controllate voi: una cartella statica sul vostro web server, un bucket di storage, qualsiasi host. Con i18next si usa i18next-http-backend:
import i18next from 'i18next'
import HttpBackend from 'i18next-http-backend'
i18next
.use(HttpBackend)
.init({
fallbackLng: 'en',
ns: ['common'],
defaultNS: 'common',
backend: {
// any URL: your web server, a storage bucket, a CDN
loadPath: 'https://translations.example.com/{{lng}}/{{ns}}.json'
}
})I componenti React, Vue o React Native continuano a chiamare t() come prima; cambia solo la provenienza dei testi. Una modifica al testo ora è un upload, senza alcun build. Cosa vi prendete in carico in cambio: la pipeline che copia le traduzioni dallo strumento dei traduttori a quell'host, gli header cross-origin, la disponibilità di quell'host e gli header di cache di quei file, di cui parla la prossima sezione.
3. Una CDN alimentata dal vostro sistema di gestione delle traduzioni
Il sistema di gestione delle traduzioni (TMS) pubblica direttamente i file su una CDN, e l'app li carica da lì. Download, commit e upload spariscono dalla vostra pipeline: una traduzione salvata e pubblicata è il file in produzione. Scegliete comunque un plugin backend e convivete comunque con la cache, ma hosting, distribuzione globale e passo di pubblicazione non sono più compito vostro. Come questo si inserisce in un progetto i18next che cresce è descritto in i18next in production.
| Bundle + nuovo deploy | Vostro server o storage | CDN alimentata dal TMS | |
|---|---|---|---|
| Una modifica al testo richiede | Build e deploy (o rilascio sullo store) | L'upload di un file | Una pubblicazione nel TMS |
| Visualizzazione offline al primo avvio | Sì | Solo con un fallback nel bundle | Solo con un fallback nel bundle |
| Voi gestite | Nulla in più | Hosting, pipeline di upload, header di cache | Configurazione del backend e impostazioni di cache |
| Da dove arrivano i testi vecchi | Build vecchi ancora in esecuzione | Cache tra host e utente | Cache tra CDN e utente |
Come la cache decide quando gli utenti vedono la modifica
Quando le traduzioni vengono caricate a runtime, la domanda non è più "abbiamo fatto il deploy?" ma "quale cache tiene ancora il vecchio file?". La cache HTTP è controllata dall'header di risposta Cache-Control:
max-age=N: la risposta resta fresca per N secondi dalla sua generazione; finché è fresca, una cache può riutilizzarla senza interpellare il server.no-cache: le cache possono memorizzare la risposta, ma devono convalidarla con il server prima di ogni riutilizzo. Se il file non è cambiato, si tratta di una richiesta condizionale economica.no-store: nessuna cache deve memorizzare la risposta.s-maxage: una durata di freschezza separata per le cache condivise come le CDN; lì sostituiscemax-agee viene ignorata dal browser.immutable: la risposta non cambierà finché è fresca, quindi il browser salta la riconvalida.
Nomi di file con hash: perfetti per i bundle, scomodi per i testi a runtime
Per le risorse statiche, MDN descrive lo schema di cache-busting: inserire una versione o un hash nell'URL, non modificare mai il file, metterlo in cache per un anno e marcarlo immutable. È così che i bundler rilasciano il JavaScript, ed è il motivo per cui le traduzioni nel bundle non sono mai vecchie una volta caricato il nuovo bundle.
Per le traduzioni caricate a runtime vale il contrario. Se il nome del file contiene un hash, l'app deve conoscere il nuovo nome, e il punto da cui lo apprende è di solito il deploy successivo. Lo stesso vale per l'opzione queryStringParams di i18next-http-backend (per esempio { v: '1.3.5' }): la stringa di versione vive nel vostro codice. Le traduzioni a runtime stanno quindi di solito su URL stabili con un max-age breve, oppure con no-cache e riconvalida, scambiando qualche richiesta in più con l'aggiornamento. Se volete comunque una cache lunga, fate caricare all'app prima un piccolo manifest non in cache che indichi la versione attuale delle traduzioni.
I livelli tra una modifica e l'utente
| Livello | Cosa trattiene il vecchio testo | Come far passare il nuovo testo |
|---|---|---|
| Cache HTTP del browser | La risposta JSON, per il suo max-age | Un max-age breve, oppure no-cache con riconvalida. i18next-http-backend passa requestOptions a fetch, e cache: 'no-cache' fa sì che il browser riconvalidi anche una voce fresca (una CDN davanti può comunque rispondere dalla propria cache) |
| Service worker | Le voci della Cache API, aggiornate solo se lo fa il vostro codice, e che non scadono mai da sole | Escludere gli URL delle traduzioni dal precaching, oppure caricarli in modalità network-first |
| Cache localStorage | i18next-localstorage-backend serve un namespace in cache fino a expirationTime (default: 7 giorni) | Una scadenza più breve, un nuovo valore di defaultVersion o versions, oppure la modalità di aggiornamento del chained backend (sotto) |
| Nodo della CDN | La copia sul nodo, per il suo max-age, s-maxage o il TTL proprio della CDN | Un TTL breve, oppure un purge alla pubblicazione |
| Server di lunga durata | L'istanza di i18next che ha caricato i file all'avvio | reloadInterval di i18next-http-backend: un'ora di default fuori dal browser, disattivato nel browser. Riutilizzare una sola istanza per processo |
| HTML pre-renderizzato | Il testo renderizzato nell'HTML al momento del build | Un nuovo build, oppure una riconvalida come l'ISR di Next.js |
| Rendering React | Niente: lo store ha il nuovo testo, ma nessun componente si è ri-renderizzato | react-i18next ri-renderizza di default su languageChanged; impostate bindI18nStore: 'added' per ri-renderizzare quando vengono aggiunte risorse |
Una configurazione che si visualizza subito da una cache locale e recepisce comunque le modifiche nella stessa visita combina i18next-chained-backend con la sua modalità refreshAndUpdateStore: serve la copia in cache, recupera quella aggiornata dal backend successivo, aggiorna la cache e la aggiunge allo store:
import i18next from 'i18next'
import { initReactI18next } from 'react-i18next'
import ChainedBackend from 'i18next-chained-backend'
import LocalStorageBackend from 'i18next-localstorage-backend'
import HttpBackend from 'i18next-http-backend'
i18next
.use(ChainedBackend)
.use(initReactI18next)
.init({
fallbackLng: 'en',
backend: {
backends: [LocalStorageBackend, HttpBackend],
backendOptions: [
{ expirationTime: 7 * 24 * 60 * 60 * 1000 }, // the default: 7 days
{ loadPath: 'https://translations.example.com/{{lng}}/{{ns}}.json' }
],
// serve the cached copy first, then fetch the fresh one and update the store
cacheHitMode: 'refreshAndUpdateStore'
},
react: {
bindI18nStore: 'added' // re-render when refreshed resources land in the store
}
})Per le app che restano aperte per ore (dashboard, strumenti di amministrazione), chiamate i18next.reloadResources() a intervalli regolari o quando la scheda torna visibile; con bindI18nStore: 'added' il testo ricaricato compare senza ricaricare la pagina.
Debug: "le traduzioni non si aggiornano"
- Il file di traduzione viene richiesto? Controllate la scheda di rete. Nessuna richiesta significa che i testi arrivano dal bundle (cercate
resourcesall'inizializzazione), da un service worker o dal localStorage. - Viene servito da una cache? "Memory cache" o "disk cache" significa che il browser ha servito la propria copia senza chiedere nulla. Una risposta 304 significa che il browser ha riconvalidato e la CDN o il server di origine ha risposto "invariato": un testo vecchio dopo un 304 si trova quindi a monte. Gli header di risposta
Cache-ControleAgemostrano cosa sta facendo la CDN. - Il file stesso è aggiornato? Aprite direttamente l'URL del file e confrontatelo con la sorgente. Se è già vecchio lì, il problema è nel passo di pubblicazione o di upload, non nell'app.
- Cosa memorizza il browser? Negli strumenti per sviluppatori, guardate le voci di localStorage con il prefisso
i18next_res_(il default di i18next-localstorage-backend) e la Cache Storage. - Sul server: il processo è stato riavviato, e l'istanza di i18next si ricarica a intervalli? Una nuova istanza per richiesta scarica le traduzioni a ogni richiesta, e ogni istanza mantiene attivo il proprio timer di ricaricamento (
reloadIntervalvale un'ora di default fuori dal browser): create quindi l'istanza una sola volta per processo e riutilizzatela. - Lo store ha il testo, lo schermo no: è un problema di binding del rendering, non di cache. Vedete
bindI18nStoresopra.
Se vedete chiavi o testi di fallback invece di testi vecchi, la causa è una traduzione mancante, non vecchia: vedete missing translations.
Quando il bundling è la scelta giusta
Il caricamento a runtime non è automaticamente migliore. Includete le traduzioni nel bundle quando:
- l'app è offline-first. Una traduzione caricata a runtime richiede una rete al primo avvio. Se l'app deve visualizzarsi prima di essere mai stata online, includete nel bundle almeno la lingua predefinita. L'approccio ibrido è un chained backend con la rete per prima e le risorse nel bundle come fallback.
- le pagine sono pre-renderizzate (SSG, export statico). Il testo renderizzato al build fa parte dell'HTML. Sostituire un file su una CDN non cambia un HTML già generato; la pagina ha bisogno di un nuovo build o di una riconvalida (l'ISR di Next.js, che l'export statico non supporta). Caricare testi più recenti nel client dopo l'idratazione significa che gli utenti possono vedere il testo cambiare dopo la comparsa della pagina. Per pagine di contenuto che vengono comunque ricostruite a ogni modifica, il bundling è più semplice.
- i testi viaggiano nel binario di un'app nativa. Le risorse di stringhe di iOS e Android vengono compilate nell'app, quindi modificarle richiede un nuovo build. Le App Review Guidelines di Apple vietano di scaricare codice che cambia le funzionalità di un'app (2.5.2). Le traduzioni sono dati, non codice, ed è per questo che le app React Native possono caricarle a runtime. Ma la regola 2.3.1 chiede anche che le funzionalità dell'app siano chiare all'App Review; se i vostri testi descrivono funzionalità, prezzi o condizioni, rilasciarli con il build revisionato mantiene le due cose identiche.
- i testi sono riservati. Un file su un URL pubblico può essere letto da chiunque lo trovi: nomi di prodotti non ancora annunciati, strumenti interni, formulazioni specifiche per un cliente. Includeteli nel bundle, oppure ospitateli voi dietro un'autenticazione. Una chiave distribuita dentro un'app per browser è visibile a ogni utente; i download protetti da chiave proteggono quindi i recuperi lato server e al build, non un frontend pubblico.
- i testi devono cambiare insieme al codice. Una nuova chiave usata da nuovo codice deve esistere prima che quel codice raggiunga gli utenti. Con il caricamento a runtime, pubblicate le nuove chiavi prima del deploy o insieme a esso, altrimenti gli utenti vedono i fallback.
Come funziona in Locize
Locize, creato dal team dietro i18next, è la terza opzione descritta sopra:
- Pubblicato su una CDN di default. Le traduzioni vengono pubblicate sulla CDN di Locize a meno che non lo disattiviate, e le app web e React Native le caricano a runtime tramite i18next; una modifica pubblicata non richiede alcun nuovo deploy:
import i18next from 'i18next'
import LocizeBackend from 'i18next-locize-backend'
i18next
.use(LocizeBackend)
.init({
fallbackLng: 'en',
backend: {
projectId: '[PROJECTID]',
version: 'latest', // a dedicated production version needs a plan with several versions
cdnType: 'standard' // or 'pro', matching your project's CDN type
}
})- Due tipi di CDN, due modelli di cache. La CDN Standard (BunnyCDN, l'impostazione predefinita per i nuovi progetti) è solo pubblica, con una durata di cache fissa di un'ora. La CDN Pro (Amazon CloudFront, dal piano Growth) permette di impostare il Cache-Control max-age per versione, supporta la pubblicazione privata che richiede una chiave API e conserva i backup dei namespace pubblicati. Vedete CDN types: Standard vs Pro e caching.
- Sviluppo e produzione restano separati. Sulla CDN Pro, la documentazione sulla cache raccomanda di disattivare la cache sulle versioni di sviluppo, e una versione di produzione dedicata senza pubblicazione automatica, pubblicata manualmente o tramite API o CLI, con un max-age basso ma diverso da zero. Durante lo sviluppo sulla CDN Standard,
noCache: truein i18next-locize-backend (oppure?cache=nonell'URL) aggira la cache. - Il bundling resta un'opzione. Scaricate le traduzioni con la CLI, l'API o la GitHub Action locize/download e includetele nel bundle, oppure combinate le due cose con un fallback nel bundle. Le app native iOS e Android seguono questa strada: i loro file di stringhe vengono sincronizzati al build, perché Locize non ha un SDK nativo over-the-air. Per i testi riservati, disattivate la pubblicazione automatica e includete le traduzioni nel bundle, oppure usate la pubblicazione privata.
Per vedere il ciclo completo: create un progetto, puntateci i18next-locize-backend, modificate un testo nell'editor, pubblicatelo e ricaricate la vostra app. Il quadro più ampio, cioè rilasciare i testi indipendentemente dal codice, è sulla pagina continuous localization.
Domande frequenti
Come si aggiornano le traduzioni senza rifare il deploy dell'app? Caricate le traduzioni a runtime da un URL (vostro server, bucket di storage o CDN alimentata dal TMS) invece di compilarle nel build. Con i18next usate un plugin backend come i18next-http-backend e non passate resources nel bundle all'inizializzazione. Una modifica al testo diventa una modifica a un file, al netto delle cache intermedie.
Perché le mie traduzioni i18next non si aggiornano dopo un deploy? Di solito per risorse nel bundle che disattivano il backend, per una cache che serve ancora il vecchio file (browser, service worker, backend localStorage, nodo della CDN), per un'istanza server di lunga durata che ha caricato i file all'avvio, o per componenti React che non si sono ri-renderizzati dopo l'aggiornamento dello store.
Come si caricano le traduzioni da una CDN a runtime? Puntate un backend di i18next sullo schema di URL della CDN, mantenete l'URL stabile, scegliete un max-age adatto a quanto devono essere aggiornati i testi, e includete nel bundle una lingua di fallback se l'app deve visualizzarsi offline.
Quanto tempo passa prima che gli utenti vedano una traduzione modificata? Tanto quanto consente la cache più lenta lungo il percorso: il max-age del browser, la durata nella CDN, una scadenza di localStorage o una voce di service worker che il vostro codice non ha sostituito. Le app aperte vedono la modifica al prossimo caricamento del namespace.
Quando conviene includere le traduzioni nel bundle invece di caricarle a runtime? Per app offline-first, pagine pre-renderizzate, binari di app native soggetti alla revisione dello store e testi riservati. Un fallback nel bundle combinato con il caricamento a runtime copre la maggior parte dei casi intermedi.
Posso cambiare i testi di un'app mobile senza un nuovo rilascio sullo store? Con React Native e i18next sì: le traduzioni si caricano a runtime come sul web. Le risorse di stringhe native di iOS e Android vengono compilate nel binario; senza un SDK over-the-air (Locize non ne ha) modificarle richiede quindi un nuovo build e un nuovo rilascio sullo store.
Stanco di gestire le traduzioni a mano?
Locize è il backend di gestione delle traduzioni creato dal team di i18next: distribuzione via CDN, traduzione con AI, editing in-context, senza nuovi deploy.
Inizia la prova gratuita di 14 giorni