Seus botões, a navegação e os estados vazios estão em alemão. Os nomes dos seus produtos, categorias e descrições continuam em inglês. Isso não é um bug na sua configuração de i18n, é o limite dela. O globalize.now é uma infraestrutura de localização baseada em IA: ele extrai strings hardcoded do seu código-fonte, gera as chaves e os catálogos de locale, e os sincroniza a cada push no Git. Ele lê código, não linhas, e o conteúdo que seus usuários vieram buscar vive nas linhas.
Resolver isso é uma decisão de schema, não uma decisão de ferramenta. Aqui está a versão que realmente se sustenta.
Por que a interface do meu app no Lovable está traduzida, mas o conteúdo do banco de dados continua em inglês?
Porque um extrator de strings só consegue enxergar o que está escrito nos seus arquivos de origem.
Quando você localiza um app feito no Lovable, um scanner percorre seus componentes e extrai cada literal que encontra: "Add to cart", "No results", "Sign in". Isso vira IDs de mensagem em um catálogo, e o catálogo é traduzido e commitado. Esse pipeline é determinístico e cobre a interface por completo.
Ele não consegue cobrir uma linha de banco de dados. products.name = 'Trail Runner' nunca esteve em um arquivo .tsx. Ela chegou por um formulário, um script de seed ou uma importação, e vive no Postgres: o backend embutido do Lovable Cloud, que roda sobre o Supabase, ou um projeto Supabase que você mesmo conectou. Nenhum scanner de código vai ler isso, e nenhuma quantidade de reexecução de extração vai mudar esse fato.
Aí você acaba com uma casca traduzida em volta de conteúdo em inglês. O menu diz Warenkorb, mas o produto dentro dele diz Trail Runner, tênis leve de trilha para condições úmidas. Os usuários percebem na hora, e o resultado fica pior do que um app não traduzido, porque parece abandonado pela metade.
O que conta como conteúdo de banco de dados em um app Lovable?
Qualquer coisa que um usuário ou administrador possa alterar sem precisar fazer deploy. Na prática, em um projeto Lovable típico, isso inclui:
- Nomes e descrições de produtos, planos ou anúncios
- Rótulos de categoria, tag e status renderizados a partir de uma tabela de referência
- Textos de marketing armazenados em uma tabela
pagesousections - Modelos de e-mail e notificação
- Valores tipo enum exibidos diretamente, como
status = 'pending_review' - Conteúdo gerado por usuários, que normalmente é mantido de propósito no idioma original
O último ponto é importante. Por padrão, não traduza conteúdo gerado por usuários. Uma avaliação escrita em espanhol deve permanecer em espanhol. O conjunto que você realmente localiza é o conteúdo que você cria e publica.
Tudo o mais, ou seja, a moldura em volta desse conteúdo, fica nos seus catálogos. Os dois conjuntos nunca devem se sobrepor. Se um rótulo aparece tanto em uma tabela de referência quanto em uma string fixa no código, escolha um único lugar para ele e remova o outro, senão você vai acabar publicando duas palavras diferentes em alemão para o mesmo conceito.
Como armazenar traduções de conteúdo de banco de dados no Supabase?
Use uma tabela de tradução separada, indexada pelo id da linha e por uma coluna de idioma.
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);
Adicionar japonês passa a ser um simples insert, não uma migração. As políticas de segurança em nível de linha ficam associadas a uma única tabela em vez de um conjunto crescente de colunas, e uma tradução ausente vira uma linha ausente, o que é fácil de consultar e fácil de monitorar com alertas.
A alternativa é uma coluna JSONB por campo traduzível:
alter table products
add column name_i18n jsonb not null default '{}'::jsonb;
-- { "en": "Trail Runner", "de": "Trail Runner", "fr": "Coureur de sentier" }
Isso é mais rápido de prototipar e funciona bem para conteúdo pequeno e majoritariamente estático. Em troca, você fica preso a restrições por idioma, torna traduções parciais difíceis de auditar e a indexação fica cara assim que você precisa filtrar ou ordenar pelo valor localizado.
O terceiro padrão, uma coluna por idioma (name_en, name_de, name_fr), é o que você deve evitar. Cada novo idioma vira uma migração de schema em cada tabela, e em um projeto Lovable isso significa pedir repetidamente ao agente para alterar tabelas de produção. Se quiser ver esse padrão detalhado, a extensão pública do Postgres pg_i18n implementa a abordagem de tabela de tradução como views com fallback embutido, e vale a pena ler antes de se comprometer com uma estrutura.
Qual padrão você deve escolher?
Tabela de tradução, a menos que o conteúdo seja pequeno e estático.
Escolha JSONB apenas quando você tiver poucos campos traduzíveis, não precisar filtrar ou ordenar por eles, e não planejar deixar ninguém além de você editá-los. Tudo o mais — qualquer coisa com uma interface de administração, qualquer coisa à qual você vá adicionar idiomas, qualquer coisa com mais de algumas centenas de linhas — pede a tabela.
Como consultar o idioma correto no momento da renderização?
Com um fallback explícito, dentro do banco de dados, para que o app nunca precise se preocupar com isso.
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;
$$;
Assim, o ponto de chamada carrega apenas um locale e nada mais:
const { data: products } = await supabase
.rpc('products_for_locale', { p_locale: locale });
Duas regras fazem isso resistir ao contato com dados reais. Primeiro, coalesce deve recorrer ao idioma de origem, nunca a uma string vazia: um catálogo parcialmente traduzido deve mostrar inglês, não cartões em branco. Segundo, o locale que você passa precisa ser o mesmo valor usado pelo segmento de rota e o mesmo valor pelo qual seus catálogos de UI são indexados. Uma única lista de locales, três consumidores: roteador, catálogos, banco de dados.
É nesse último ponto que a maioria dos projetos Lovable se perde. O roteador conhece de, os catálogos foram criados para de-DE, e o banco de dados tem linhas marcadas como german. Defina o conjunto exato de tags BCP 47 usadas nas rotas de locale e faça tudo o mais seguir esse padrão. Se você ainda não configurou essas rotas, os guias específicos por stack cobrem isso para as duas stacks geradas pelo Lovable: a build SPA com Vite e o padrão SSR com TanStack Start.
Por que não simplesmente deixar um widget em tempo de execução traduzir o texto do banco de dados?
Porque ele atua na página renderizada, não nos seus dados, e essa diferença aparece em três pontos.
Um widget de tradução em JavaScript lê o texto que acaba chegando ao DOM e o substitui depois. Isso realmente pega as linhas do banco de dados: é a única coisa que os widgets fazem e os catálogos não, e é por isso que eles parecem uma solução completa à primeira vista.
Os custos são estruturais. As linhas traduzidas existem apenas na sessão do navegador, então os mecanismos de busca que rastreiam a página veem o seu idioma de origem; a própria central de ajuda da Weglot afirma que sua integração em JavaScript não traz benefícios de SEO, porque essas traduções são renderizadas no lado do cliente. As correções ficam no painel do fornecedor em vez de no seu banco de dados, então o valor real de um campo fica dividido entre dois sistemas. E a troca acontece depois da renderização inicial, por isso apps traduzidos dessa forma mostram um flash em inglês a cada navegação.
Nada disso é motivo para descartar totalmente essa abordagem em uma ferramenta interna. Mas é motivo suficiente para não construir sobre ela um produto público, multilíngue e indexável.
Como as duas camadas se mantêm sincronizadas?
Compartilhando uma única lista de locales e um único gatilho.
A camada de código é automática. Adicione a skill lovable-i18n no seu workspace Lovable e peça ao Lovable para configurar o i18n, ou instale localmente com:
npx skills add globalize-now/globalize-skills
A partir daí, a extração roda e um pull request de tradução é aberto a cada push: sem passo em painel, sem revisão manual dos arquivos de locale. É o mesmo fluxo descrito em por que as traduções do Lovable quebram a cada push.
A camada de dados é sua, e ela precisa de um gatilho deliberado: sempre que você adicionar um locale, a mesma lista que alimenta suas rotas e catálogos deve alimentar um preenchimento retroativo na sua tabela de tradução. Em um projeto Lovable, a versão prática é uma função de seed que você pede ao agente para escrever uma vez, que lê a lista de locales e insere as linhas ausentes com os valores do idioma de origem. As linhas ausentes são então preenchidas, não inventadas, e coalesce cobre a lacuna enquanto isso.
A precificação também não complica nada aqui. O plano Starter custa €20 por mês por workspace e inclui €20 de crédito de tradução (cerca de 200.000 palavras) por mês, com o uso acima disso cobrado pela tarifa por caractere publicada. Não há cobrança por assento nem por idioma, então adicionar o quinto idioma custa apenas os caracteres que ele consome, e nada mais. O cadastro vem com €5 de crédito de tradução e sem necessidade de cartão. Mais detalhes sobre como isso se encaixa em uma stack construída por agentes nas páginas de vibe coders e desenvolvedores.
O que quebra quando um fornecedor de localização encerra as atividades?
Tudo o que estava armazenado nos servidores deles, e nada do que estava armazenado nos seus.
Isso não é hipotético neste momento. O Lovalingo, a camada de tradução em tempo de execução adotada por muitos criadores de apps Lovable, encerra as atividades em 31 de agosto de 2026 às 23h59 (horário de Berlim/CEST), conforme confirmado em sua própria página de encerramento de serviço, com novos projetos e assinaturas já interrompidos e exportações de preservação sendo preparadas para clientes ativos. A parte interessante é a própria orientação da empresa: ela diz aos clientes para implementar internacionalização própria do projeto em paralelo, e só remover a camada da Lovalingo depois que a substituta passar por testes de produção em ambiente real.
Esse é todo o argumento resumido em uma frase, vindo justamente do fornecedor cuja saída o comprova. Um catálogo versionado e uma tabela product_translations são coisas que você possui; elas não têm “status de serviço”. Uma camada de execução gerenciada por terceiros é uma dependência que renderiza o texto do seu produto, e dependências acabam.
Se você está migrando para fora de um widget agora, comece pela camada de dados. As strings de UI serão reextraídas do seu código em minutos. Já as traduções de banco de dados, se elas só existiram dentro do painel de um fornecedor, não voltam. Nossa comparação com o Lovalingo mostra o que verificar antes de remover qualquer coisa.
A camada de código é a parte que você não deveria gerenciar manualmente. O globalize.now extrai suas strings, gera os catálogos e abre um pull request de tradução a cada push, o que te deixa livre para desenhar a camada de dados uma única vez e esquecer o assunto.
O globalize.now transforma textos fixos do app em arquivos de locale prontos para tradução e os mantém atualizados a cada novo lançamento.
Experimente o globalize.now grátis