Internacionalização no react e prisma: como validar chaves no build com typescript#
O padrão de diretórios /src/locales/en.json que aparece frequentemente em repositórios open source não é coincidência: trata-se da arquitetura essencial para internacionalização. Plugar um ecossistema completo de i18n em uma aplicação moderna com Bun, React, TypeScript e Prisma exige separação clara em três dimensões:
- Interface Estática: Rótulos fixos de botões, formulários e mensagens de validação.
- Tipagem em Tempo de Compilação: Validação de chaves de tradução pelo TypeScript no build, evitando erros silenciosos de runtime.
- Conteúdo Dinâmico no Banco de Dados: Modelagem normalizada no Prisma para entidades multilíngues (como posts, produtos e categorias).
Abaixo, apresento o passo a passo completo da implementação de cada camada.
1. Instalação de dependências com Bun#
bun add i18next react-i18next i18next-browser-languagedetector
i18next: Motor principal de internacionalização, interpolação e gerenciamento de recursos.react-i18next: Bindings oficiais do React, disponibilizando o hookuseTranslatione o provider de contexto.i18next-browser-languagedetector: Detecção automática do idioma do usuário vialocalStorage,navigator.languageou cookies.
2. Estrutura de dicionários e arquivos JSON#
Organize os arquivos por idioma dentro de src/locales/:
src/
└── locales/
├── en/
│ └── translation.json
└── pt-BR/
└── translation.json
Arquivo mestre de referência (src/locales/en/translation.json):#
{
"welcome": "Welcome to my App",
"description": "Built with Bun and React",
"auth": {
"login": "Log in",
"logout": "Log out",
"email_placeholder": "Enter your email"
},
"dashboard": {
"title": "Dashboard",
"stats": {
"total_users": "Total Users"
}
}
}
Arquivo localizado (src/locales/pt-BR/translation.json):#
{
"welcome": "Bem-vindo ao meu App",
"description": "Construído com Bun e React",
"auth": {
"login": "Entrar",
"logout": "Sair",
"email_placeholder": "Digite seu e-mail"
},
"dashboard": {
"title": "Painel",
"stats": {
"total_users": "Total de Usuários"
}
}
}
3. Inicialização e configuração do i18n (src/i18n.ts)#
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import enTranslations from './locales/en/translation.json';
import ptBrTranslations from './locales/pt-BR/translation.json';
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
en: { translation: enTranslations },
'pt-BR': { translation: ptBrTranslations }
},
fallbackLng: 'en',
interpolation: {
escapeValue: false // O React já trata escape de strings contra XSS
},
detection: {
order: ['localStorage', 'navigator', 'htmlTag'],
caches: ['localStorage']
}
});
export default i18n;
Importe o arquivo no ponto de entrada (src/main.tsx):
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './i18n';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
4. Tipagem estrita com typescript declaration merging#
Crie o arquivo src/@types/i18next.d.ts para que o editor ofereça autocompletar e validação estática de chaves:
import 'i18next';
import en from '../locales/en/translation.json';
declare module 'i18next' {
interface CustomTypeOptions {
defaultNS: 'translation';
resources: {
translation: typeof en;
};
}
}
Qualquer chave digitada incorretamente (como t('auth.logon')) gerará um erro em tempo de compilação.
5. Utilização nos componentes react#
import { useTranslation } from 'react-i18next';
export function Header() {
const { t, i18n } = useTranslation();
return (
<header>
<h1>{t('welcome')}</h1>
<p>{t('description')}</p>
<button onClick={() => i18n.changeLanguage(i18n.language === 'en' ? 'pt-BR' : 'en')}>
{i18n.language === 'en' ? 'Mudar para Português' : 'Switch to English'}
</button>
</header>
);
}
6. Modelagem de conteúdo dinâmico no prisma#
Para entidades que possuem textos traduzíveis no banco de dados (ex: títulos e artigos):
// prisma/schema.prisma
model Post {
id Int @id @default(autoincrement())
slug String @unique
createdAt DateTime @default(now())
translations PostTranslation[]
}
model PostTranslation {
id Int @id @default(autoincrement())
postId Int
lang String // ex: "pt-BR", "en"
title String
content String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
@@unique([postId, lang])
}
Consultando com fallback:#
export async function getPost(slug: string, lang: string) {
const post = await prisma.post.findUnique({
where: { slug },
include: {
translations: {
where: { lang: { in: [lang, 'en'] } }
}
}
});
const translation = post?.translations.find(t => t.lang === lang)
|| post?.translations.find(t => t.lang === 'en');
return { ...post, translation };
}
Boas práticas para arquiteturas multilíngues#
- Defina um arquivo mestre: Mantenha sempre o
en/translation.jsoncomo fonte de verdade para a tipagem do TypeScript. - Separe conteúdo estático de dinâmico: Use arquivos JSON para textos de layout e tabelas relacionais do Prisma para dados de banco de dados.
- Habilite persistência no localStorage: Configurar o detector para salvar no
localStorageevita que a página recarregue no idioma padrão a cada navegação.
Este artigo foi útil?
Deixe uma reação rápida para apoiar o conteúdo:
Este post está licenciado sob CC BY-NC.



Comentários
Participe da discussão abaixo.
0 comentários