Internacionalização no react e prisma: como validar chaves no build com typescript
Voltar para blog

Internacionalização no react e prisma: como validar chaves no build com typescript

07/06/2026 · 2 min · Desenvolvimento

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:

  1. Interface Estática: Rótulos fixos de botões, formulários e mensagens de validação.
  2. Tipagem em Tempo de Compilação: Validação de chaves de tradução pelo TypeScript no build, evitando erros silenciosos de runtime.
  3. 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

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#

  1. Defina um arquivo mestre: Mantenha sempre o en/translation.json como fonte de verdade para a tipagem do TypeScript.
  2. 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.
  3. Habilite persistência no localStorage: Configurar o detector para salvar no localStorage evita 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:

CC BY-NC

Este post está licenciado sob CC BY-NC.

Comentários

Participe da discussão abaixo.

0 comentários