I pulsanti, la navigazione e gli stati vuoti sono in tedesco. I nomi dei prodotti, le categorie e le descrizioni sono ancora in inglese. Non è un bug della configurazione i18n: è il suo confine. globalize.now è un’infrastruttura di localizzazione basata su IA: estrae le stringhe hardcoded dalla codebase, genera le chiavi e i cataloghi di traduzione e li sincronizza a ogni push su Git. Legge il codice, non le righe, mentre il contenuto che interessa agli utenti vive nelle righe.

Risolvere il problema è una decisione di schema, non di strumenti. Ecco il modello che regge nel tempo.

Perché l’interfaccia della mia app Lovable è tradotta, ma il contenuto del database è ancora in inglese?

Perché un estrattore di stringhe può vedere solo ciò che è scritto nei file sorgente.

Quando localizzi un’app Lovable, uno scanner attraversa i componenti ed estrae ogni stringa letterale che trova: "Add to cart", "No results", "Sign in". Queste diventano message ID in un catalogo, che viene tradotto e sottoposto a commit. La pipeline è deterministica e copre completamente l’interfaccia.

Non può coprire una riga. products.name = 'Trail Runner' non è mai stata in un file .tsx. È arrivata tramite un form, uno script di seed o un'importazione, e vive in Postgres: il backend integrato di Lovable Cloud, che gira su Supabase, oppure un progetto Supabase che hai collegato tu stesso. Nessuno scanner del codice la leggerà mai, e rieseguire l'estrazione all'infinito non cambierà nulla.

Ti ritrovi così con un guscio tradotto attorno a contenuti in inglese. La navigazione dice Warenkorb, mentre il prodotto al suo interno dice Trail Runner, lightweight trail shoe for wet conditions. Gli utenti lo notano subito e il risultato sembra peggiore di un’app non tradotta, perché dà l’impressione di essere stata abbandonata a metà.

Cosa rientra nei contenuti del database di un’app Lovable?

Tutto ciò che un utente o un amministratore può modificare senza un deploy. In pratica, in una build Lovable tipica, include:

  • Nomi e descrizioni di prodotti, piani o annunci
  • Etichette di categorie, tag e stati generate da una tabella di lookup
  • Copy di marketing memorizzata in una tabella pages o sections
  • Template di email e notifiche
  • Valori simili a enum visualizzati direttamente, come status = 'pending_review'
  • Contenuti generati dagli utenti, che di solito vengono lasciati intenzionalmente nella lingua originale

L’ultimo punto è importante. Non tradurre automaticamente i contenuti generati dagli utenti. Una recensione scritta in spagnolo deve restare in spagnolo. Il contenuto da localizzare è quello che scrivi e rilasci tu.

Tutto il resto — l’interfaccia attorno a quei contenuti — resta nei tuoi cataloghi. I due insiemi non devono mai sovrapporsi. Se un’etichetta compare sia in una tabella di lookup sia come stringa hardcoded, scegli una sola fonte e rimuovi l’altra, altrimenti rilascerai due parole tedesche diverse per lo stesso concetto.

Come si archiviano le traduzioni dei contenuti del database in Supabase?

Usa una tabella di traduzione separata, indicizzata sull’ID della riga e su una colonna locale.

create table product_translations (
  product_id  uuid not null references products(id) on delete cascade,
  locale      text not null,
  name        text not null,
  description text,
  primary key (product_id, locale)
);

create index product_translations_locale_idx on product_translations (locale);

Aggiungere il giapponese diventa un insert, non una migration. Le policy di row-level security si applicano a una sola tabella invece che a un insieme crescente di colonne, mentre una traduzione mancante corrisponde a una riga mancante: facile da interrogare e da segnalare.

L’alternativa è una colonna JSONB per ogni campo traducibile:

alter table products
  add column name_i18n jsonb not null default '{}'::jsonb;

-- { "en": "Trail Runner", "de": "Trail Runner", "fr": "Coureur de sentier" }

È più veloce da prototipare e va bene per contenuti piccoli e quasi statici. Ti impone vincoli per lingua, rende difficile verificare le traduzioni parziali e diventa costosa da indicizzare quando devi filtrare o ordinare per il valore localizzato.

Il terzo pattern, una colonna per lingua — name_en, name_de, name_fr — è quello da evitare. Ogni nuova lingua richiede una migration dello schema su ogni tabella e, in un progetto Lovable, significa chiedere ripetutamente all’agente di modificare le tabelle di produzione. Se vuoi approfondire questo pattern, l’estensione Postgres pubblica pg_i18n implementa l’approccio basato sulla tabella di traduzione tramite viste con fallback integrato. Vale la pena leggerla prima di fissare lo schema.

Quale pattern dovresti scegliere?

La tabella di traduzione, a meno che il contenuto non sia minimo e immutabile.

Scegli JSONB solo se hai meno di una manciata di campi traducibili, non devi filtrarli o ordinarli e non prevedi di farli modificare a nessuno oltre a te. In tutti gli altri casi — con una UI amministrativa, con nuove lingue da aggiungere o con più di qualche centinaio di righe — serve la tabella.

Come si interroga la lingua corretta al momento del rendering?

Con un fallback esplicito nel database, così l’app non deve occuparsene.

create or replace function products_for_locale(p_locale text)
returns table (id uuid, slug text, name text, description text)
language sql stable as $$
  select
    p.id,
    p.slug,
    coalesce(t.name, base.name)               as name,
    coalesce(t.description, base.description)  as description
  from products p
  left join product_translations base
    on base.product_id = p.id and base.locale = 'en'
  left join product_translations t
    on t.product_id = p.id and t.locale = p_locale;
$$;

A quel punto il call site passa una locale e nient’altro:

const { data: products } = await supabase
  .rpc('products_for_locale', { p_locale: locale });

Due regole fanno funzionare questo modello anche con dati reali. Primo: coalesce alla lingua sorgente, mai a una stringa vuota. Un catalogo tradotto a metà deve mostrare l’inglese, non schede vuote. Secondo: il valore locale passato deve essere lo stesso usato dal segmento della route e lo stesso su cui sono indicizzati i cataloghi UI. Un’unica lista di lingue, tre consumer: router, cataloghi e database.

È proprio su questo punto che la maggior parte dei progetti Lovable perde coerenza. Il router conosce de, i cataloghi sono stati creati per de-DE e il database contiene righe marcate german. Scegli l’insieme esatto di tag BCP 47 usati dalle route delle lingue e adegua tutto il resto. Se non hai ancora configurato queste route, le guide per stack coprono entrambi gli stack generati da Lovable: la build SPA con Vite e il default SSR con TanStack Start.

Perché non lasciare semplicemente che un widget runtime traduca il testo del database?

Perché funziona sulla pagina renderizzata, non sui dati, e la differenza si vede in tre punti.

Un widget di traduzione JavaScript legge il testo che finisce nel DOM e lo sostituisce dopo il rendering. Questo intercetta davvero le righe del database: è l’unica cosa che i widget fanno e i cataloghi no, ed è per questo che all’inizio sembrano una soluzione completa.

I costi sono strutturali. Le righe tradotte esistono solo nella sessione del browser, quindi i motori di ricerca che scansionano la pagina vedono la lingua sorgente; il centro assistenza di Weglot dichiara che la sua integrazione JavaScript non offre vantaggi SEO, perché le traduzioni vengono renderizzate lato client. Le correzioni vivono nella dashboard del fornitore invece che nel database, quindi il valore reale di un campo è diviso tra due sistemi. Inoltre, la sostituzione avviene dopo il paint: per questo le app tradotte in questo modo mostrano per un attimo l’inglese a ogni navigazione.

Nulla di tutto questo è un motivo per scartare del tutto l’approccio in uno strumento interno. È un motivo per non costruirci sopra un prodotto pubblico, indicizzabile e multilingue.

Come restano sincronizzati i due livelli?

Condividendo un’unica lista di lingue e un unico trigger.

Il livello del codice è automatico. Aggiungi la skill lovable-i18n al tuo workspace Lovable e chiedi a Lovable di configurare l’i18n, oppure installala localmente con:

npx skills add globalize-now/globalize-skills

Da quel momento, l’estrazione viene eseguita e a ogni push si apre una pull request per la traduzione: nessun passaggio nella dashboard, nessuna revisione manuale dei file di traduzione. È lo stesso loop descritto in perché le traduzioni Lovable si rompono a ogni push.

Il livello dei dati è tuo e richiede un hook intenzionale: ogni volta che aggiungi una lingua, la stessa lista che alimenta route e cataloghi deve avviare un backfill nella tabella di traduzione. In un progetto Lovable, la soluzione pratica è una funzione di seed che chiedi all’agente di scrivere una volta sola: legge la lista delle lingue e inserisce le righe mancanti usando i valori nella lingua sorgente. Le righe mancanti vengono così completate, non inventate, e coalesce copre il vuoto nel frattempo.

Anche qui il pricing non ti mette i bastoni tra le ruote. Il piano Starter costa 20 € al mese per workspace e include 20 € di credito per le traduzioni — circa 200.000 parole al mese — con l’utilizzo oltre questa soglia fatturato alla tariffa pubblicata per carattere. Nessun costo per seat e nessun costo per lingua: aggiungere la quinta lingua costa solo i caratteri utilizzati, nient’altro. Alla registrazione ricevi 5 € di credito per le traduzioni, senza carta. Scopri di più su come si integra con uno stack costruito da un agent nelle pagine per vibe coder e sviluppatori.

Cosa si rompe quando un vendor di localizzazione chiude?

Tutto ciò che era archiviato sui suoi server, e nulla di ciò che era archiviato sui tuoi.

Questo mese non è un'ipotesi astratta. Lovalingo, il layer di traduzione runtime adottato da molti builder che usano Lovable, chiude il 31 agosto 2026 alle 23:59 CEST, come confermato dalla sua stessa pagina dedicata alla chiusura del servizio, con i nuovi progetti e gli abbonamenti già sospesi e gli export per la conservazione dei dati in preparazione per i clienti attivi. L'indicazione più interessante arriva proprio da Lovalingo: implementare l'internazionalizzazione di proprietà del progetto insieme al servizio e rimuoverlo solo dopo che la sostituzione ha superato i controlli in produzione su un ambiente live.

È l’intera tesi in una frase, pronunciata dal vendor la cui uscita la dimostra. Un catalogo versionato e una tabella product_translations sono entrambi tuoi: non hanno uno stato del servizio da controllare. Un layer runtime gestito è una dipendenza che renderizza il testo del prodotto, e le dipendenze prima o poi finiscono.

Se stai migrando via da un widget proprio ora, parti dal data layer. Le stringhe della UI verranno riestratte dal codebase in pochi minuti. Le traduzioni nel database, se sono esistite solo nella dashboard di un vendor, non torneranno. La nostra comparazione con Lovalingo spiega cosa verificare prima di rimuovere qualsiasi cosa.

Il code layer è la parte che non dovresti gestire a mano. globalize.now estrae le stringhe, genera i cataloghi e apre una pull request di traduzione a ogni push, così puoi progettare il data layer una volta e poi dimenticartene.

globalize.now trasforma il testo hardcoded dell'app in file di localizzazione pronti e li mantiene aggiornati mentre rilasci.

Prova globalize.now gratis