Internacionalização (i18n)
- Introdução
- Arquitetura
- Hooks e Funções
- Padrões de Tradução
- Namespaces
- Integração com Zod
- Pluralização e Interpolação
- Melhores Práticas de Interpolação
- Formatação HTML em Traduções
- Caching com localStorage
- Seletor de Idioma
- CLI e Extração
- Validação i18n (CI/CD)
- Testes
- Anti-patterns
- Tipagem TypeScript (Intellisense)
Introdução
Seção intitulada “Introdução”| Biblioteca | Versão | Propósito |
|---|---|---|
i18next | 25.x | Core de internacionalização |
react-i18next | 16.x | Bindings React |
i18next-browser-languagedetector | 8.x | Detecção automática |
i18next-chained-backend | 5.x | Múltiplos backends |
i18next-localstorage-backend | 4.x | Cache em localStorage |
i18next-resources-to-backend | 1.x | Converter recursos inline |
i18next-cli | 1.x | Extração de strings (CLI) |
Idiomas Suportados
Seção intitulada “Idiomas Suportados”| Código | Nome | Status |
|---|---|---|
pt-BR | Português (Brasil) | Fonte |
en | English | Tradução |
Arquitetura
Seção intitulada “Arquitetura”┌─────────────────────────────────────────────────────────────┐│ @gaio/i18n ││ (Package dedicado para internacionalização) │└─────────────────────────┬───────────────────────────────────┘ │ ┌───────────────┼───────────────┐ ▼ ▼ ▼┌─────────────────┐ ┌───────────┐ ┌───────────────┐│ I18nProvider │ │ useT │ │ zodMessages ││ (Wrapper App) │ │ (Hook) │ │ (Validação) │└─────────────────┘ └───────────┘ └───────────────┘ │ ▼┌─────────────────────────────────────────────────────────────┐│ locales/ ││ pt-BR/common.json │ en/common.json │ ... │└─────────────────────────────────────────────────────────────┘Imports Padrão
Seção intitulada “Imports Padrão”O package é dividido em dois módulos:
// ============================================// @gaio/i18n/core - SEM dependência de React// Use em: utils, services, schemas Zod, constantes// ============================================import { i18n, zodMessages, SUPPORTED_LOCALES, LOCALE_LABELS, clearI18nCache, getCacheVersion,} from "@gaio/i18n/core";
// ============================================// @gaio/i18n/react - COM dependência de React// Use em: componentes, hooks, providers// ============================================import { I18nProvider, useT, useLocale, changeLocale } from "@gaio/i18n/react";
// Componente Trans (re-export do react-i18next)import { Trans } from "@gaio/i18n/react";Quando usar cada um:
| Módulo | Uso | Exemplos |
|---|---|---|
@gaio/i18n/core | Arquivos sem React | Schemas Zod, utils, services, constantes |
@gaio/i18n/react | Componentes React | Hooks, Provider, componentes UI |
Arquitetura
Seção intitulada “Arquitetura”Estrutura do Package
Seção intitulada “Estrutura do Package”packages/i18n/├── package.json├── tsconfig.json├── i18next.config.ts└── src/ ├── core/ # @gaio/i18n/core (sem React) │ ├── index.ts # Exports: i18n, zodMessages, constantes, cache utils │ ├── constants.ts # SUPPORTED_LOCALES, NAMESPACES │ ├── config.ts # Setup i18next com ChainedBackend │ └── validation.ts # zodMessages │ ├── react/ # @gaio/i18n/react (com React) │ ├── index.ts # Exports: Provider, hooks, Trans │ ├── provider.tsx # I18nProvider │ └── hooks.ts # useT, useLocale, changeLocale │ ├── types/ # Tipagem TypeScript │ └── resources.d.ts # Declaração para intellisense │ └── locales/ # Traduções ├── pt-BR/ │ ├── index.ts │ ├── common.json │ ├── validation.json │ ├── errors.json │ ├── auth.json │ ├── inbox.json │ ├── workflows.json │ └── settings.json └── en/ └── ... (mesmos arquivos)Exports do Package
Seção intitulada “Exports do Package”{ "exports": { "./core": "./src/core/index.ts", "./react": "./src/react/index.ts" }}Integração com Console
Seção intitulada “Integração com Console”O I18nProvider deve ser o provider mais externo na aplicação:
import { I18nProvider } from "@gaio/i18n/react";
export function AppProviders({ children }: { children: ReactNode }) { return ( <I18nProvider> <TrackingProvider> <QueryClientProvider client={queryClient}> {children} </QueryClientProvider> </TrackingProvider> </I18nProvider> );}Hooks e Funções
Seção intitulada “Hooks e Funções”Hook principal para acessar traduções. Wrapper do useTranslation do react-i18next.
import { useT } from '@gaio/i18n/react';
function MyComponent() { const { t } = useT();
return <Button>{t('common:actions.save')}</Button>;}Com Namespace Específico
Seção intitulada “Com Namespace Específico”// Namespace únicoconst { t } = useT("validation");t("required"); // validation:required
// Múltiplos namespacesconst { t } = useT(["common", "validation"]);t("common:actions.save");t("validation:required");Retorno do useT
Seção intitulada “Retorno do useT”| Propriedade | Tipo | Descrição |
|---|---|---|
t | (key, options?) => string | Função de tradução |
i18n | i18n | Instância i18next |
ready | boolean | Se traduções estão prontas |
useLocale
Seção intitulada “useLocale”Hook para obter o idioma atual.
Localização: @gaio/i18n/react
import { useLocale } from '@gaio/i18n/react';
function LanguageIndicator() { const locale = useLocale(); // 'pt-BR' | 'en-US'
return <span>Idioma: {locale}</span>;}changeLocale
Seção intitulada “changeLocale”Função para trocar o idioma programaticamente.
Localização: @gaio/i18n/react
import { changeLocale } from "@gaio/i18n/react";import type { SupportedLocale } from "@gaio/i18n/core";
function handleLanguageChange(locale: SupportedLocale) { changeLocale(locale); // Atualiza i18n + cookie}Padrões de Tradução
Seção intitulada “Padrões de Tradução”Convenção de Keys
Seção intitulada “Convenção de Keys”IMPORTANTE: Todas as keys devem estar em inglês usando snake_case:
// ✅ CORRETO - keys em inglês, descritivast("validation:required");t("validation:invalid_email");t("common:actions.save");t("common:messages.success.saved");
// ❌ ERRADO - keys em portuguêst("validation:campo_obrigatorio");t("validation:email_invalido");t("common:acoes.salvar");Texto Simples
Seção intitulada “Texto Simples”import { useT } from "@gaio/i18n/react";
function SaveButton() { const { t } = useT();
return <Button>{t("common:actions.save")}</Button>;}{ "actions": { "save": "Salvar", "cancel": "Cancelar", "confirm": "Confirmar" }}Com Interpolação
Seção intitulada “Com Interpolação”const { t } = useT();
// Simplest("welcome", { name: "João" }); // "Olá, João!"
// Múltiplas variáveist("summary", { count: 5, total: 100 }); // "5 de 100 itens"{ "welcome": "Olá, {{name}}!", "summary": "{{count}} de {{total}} itens"}JSX Complexo (Trans)
Seção intitulada “JSX Complexo (Trans)”Use o componente Trans quando precisar de JSX dentro da tradução:
import { Trans } from "@gaio/i18n/react";
<Trans i18nKey="terms"> Ao continuar, você aceita os <Link to="/terms">Termos de Uso</Link></Trans>;{ "terms": "Ao continuar, você aceita os <1>Termos de Uso</1>"}Toast Notifications
Seção intitulada “Toast Notifications”import { useT } from "@gaio/i18n/react";import { toast } from "sonner";
function MyComponent() { const { t } = useT();
const handleSave = async () => { try { await saveData(); toast.success(t("common:messages.success.saved")); } catch { toast.error(t("common:messages.error.save")); } };}Fora de Componentes React
Seção intitulada “Fora de Componentes React”Para usar traduções fora de componentes (utils, services), importe a instância i18n do módulo core:
import { i18n } from "@gaio/i18n/core";
export function formatError(code: string): string { return i18n.t(`errors:${code}`);}Namespaces
Seção intitulada “Namespaces”Os namespaces organizam traduções por domínio/feature:
| Namespace | Conteúdo | Uso |
|---|---|---|
common | Botões, labels, status | UI compartilhada |
validation | Mensagens de validação | Schemas Zod |
errors | Erros HTTP/API | Error handlers |
auth | Login, registro | screens/auth/ |
inbox | Chat, mensagens | screens/dashboard/inbox/ |
workflows | Editor de automação | screens/dashboard/workflows/ |
settings | Configurações | screens/dashboard/settings/ |
Vocabulário Padrão
Seção intitulada “Vocabulário Padrão”O namespace common contém elementos reutilizáveis em toda a aplicação. Sempre use essas keys ao invés de criar duplicatas.
Ações (Botões)
Seção intitulada “Ações (Botões)”{ "actions": { "save": "Salvar", "cancel": "Cancelar", "confirm": "Confirmar", "create": "Criar", "edit": "Editar", "delete": "Excluir", "remove": "Remover", "add": "Adicionar", "send": "Enviar", "back": "Voltar", "next": "Próximo", "previous": "Anterior", "close": "Fechar", "open": "Abrir", "search": "Buscar", "filter": "Filtrar", "clear": "Limpar", "copy": "Copiar", "paste": "Colar", "duplicate": "Duplicar", "refresh": "Atualizar", "load_more": "Carregar mais", "view_more": "Ver mais", "view_less": "Ver menos", "expand": "Expandir", "collapse": "Recolher", "export": "Exportar", "import": "Importar", "download": "Baixar", "upload": "Enviar arquivo" }}Uso:
t("common:actions.save"); // "Salvar"t("common:actions.cancel"); // "Cancelar"{ "status": { "active": "Ativo", "inactive": "Inativo", "pending": "Pendente", "approved": "Aprovado", "rejected": "Rejeitado", "completed": "Concluído", "in_progress": "Em andamento", "paused": "Pausado", "canceled": "Cancelado", "archived": "Arquivado", "draft": "Rascunho", "published": "Publicado", "scheduled": "Agendado", "expired": "Expirado", "error": "Erro", "success": "Sucesso", "loading": "Carregando...", "processing": "Processando..." }}Uso:
t("common:status.active"); // "Ativo"t("common:status.pending"); // "Pendente"Labels de Campos
Seção intitulada “Labels de Campos”{ "fields": { "name": "Nome", "email": "E-mail", "phone": "Telefone", "password": "Senha", "confirm_password": "Confirmar senha", "description": "Descrição", "title": "Título", "date": "Data", "time": "Hora", "start_date": "Data de início", "end_date": "Data de término", "address": "Endereço", "city": "Cidade", "state": "Estado", "country": "País", "zip_code": "CEP", "cpf": "CPF", "cnpj": "CNPJ", "notes": "Observação", "attachment": "Anexo", "file": "Arquivo", "image": "Imagem", "link": "Link", "url": "URL", "quantity": "Quantidade", "value": "Valor", "price": "Preço", "total": "Total" }}Uso:
t("common:fields.name"); // "Nome"t("common:fields.email"); // "E-mail"Placeholders
Seção intitulada “Placeholders”{ "placeholders": { "search": "Buscar...", "select": "Selecione...", "type": "Digite...", "email": "exemplo@email.com", "phone": "(00) 00000-0000", "date": "DD/MM/AAAA" }}Uso:
<Input placeholder={t("common:placeholders.search")} />Mensagens Genéricas
Seção intitulada “Mensagens Genéricas”{ "messages": { "success": { "saved": "Salvo com sucesso", "created": "Criado com sucesso", "updated": "Atualizado com sucesso", "deleted": "Excluído com sucesso", "sent": "Enviado com sucesso", "copied": "Copiado para a área de transferência" }, "error": { "generic": "Ocorreu um erro. Tente novamente.", "connection": "Erro de conexão. Verifique sua internet.", "load": "Erro ao carregar dados", "save": "Erro ao salvar", "delete": "Erro ao excluir", "permission": "Você não tem permissão para esta ação" }, "confirmation": { "delete": "Tem certeza que deseja excluir?", "cancel": "Tem certeza que deseja cancelar?", "exit": "Tem certeza que deseja sair?", "discard": "Descartar alterações não salvas?" }, "empty": { "list": "Nenhum item encontrado", "search": "Nenhum resultado para sua busca", "data": "Sem dados para exibir" } }}Uso:
toast.success(t("common:messages.success.saved"));toast.error(t("common:messages.error.generic"));Tempo e Datas
Seção intitulada “Tempo e Datas”{ "time": { "today": "Hoje", "yesterday": "Ontem", "tomorrow": "Amanhã", "now": "Agora", "just_now": "Há pouco", "seconds": "{{count}} segundo", "seconds_plural": "{{count}} segundos", "minutes": "{{count}} minuto", "minutes_plural": "{{count}} minutos", "hours": "{{count}} hora", "hours_plural": "{{count}} horas", "days": "{{count}} dia", "days_plural": "{{count}} dias", "weeks": "{{count}} semana", "weeks_plural": "{{count}} semanas", "months": "{{count}} mês", "months_plural": "{{count}} meses" }}Uso:
t("common:time.days", { count: 5 }); // "5 dias"t("common:time.today"); // "Hoje"Paginação e Listas
Seção intitulada “Paginação e Listas”{ "pagination": { "page": "Página {{current}} de {{total}}", "items": "{{count}} item", "items_plural": "{{count}} itens", "showing": "Mostrando {{from}} a {{to}} de {{total}}", "per_page": "Por página", "first": "Primeira", "last": "Última" }}Regra de Ouro
Seção intitulada “Regra de Ouro”// ❌ ERRADO - Criar key específica para algo comumt("workflows:save");t("inbox:cancel_button");t("settings:delete_account");
// ✅ CORRETO - Usar vocabulary padrãot("common:actions.save");t("common:actions.cancel");t("common:actions.delete");
// ✅ CORRETO - Key específica apenas para contexto únicot("settings:account.delete_permanently"); // Ação específica com consequência diferenteCriar Novo Namespace
Seção intitulada “Criar Novo Namespace”Guia passo a passo para adicionar um novo escopo de tradução.
Criar Arquivos JSON
Seção intitulada “Criar Arquivos JSON”# Estrutura de arquivospackages/i18n/src/locales/├── pt-BR/│ └── campaigns.json # NOVO└── en/ └── campaigns.json # NOVO{ "title": "Campanhas", "create": "Criar campanha", "edit": "Editar campanha", "delete": "Excluir campanha", "status": { "active": "Ativa", "paused": "Pausada", "finished": "Finalizada" }, "messages": { "created_success": "Campanha criada com sucesso", "updated_success": "Campanha atualizada com sucesso", "deleted_success": "Campanha excluída com sucesso", "error_create": "Erro ao criar campanha" }}{ "title": "Campaigns", "create": "Create campaign", "edit": "Edit campaign", "delete": "Delete campaign", "status": { "active": "Active", "paused": "Paused", "finished": "Finished" }, "messages": { "created_success": "Campaign created successfully", "updated_success": "Campaign updated successfully", "deleted_success": "Campaign deleted successfully", "error_create": "Error creating campaign" }}Registrar no Index de Locales
Seção intitulada “Registrar no Index de Locales”import auth from "./auth.json";import campaigns from "./campaigns.json";import common from "./common.json";import errors from "./errors.json";import inbox from "./inbox.json";import settings from "./settings.json";import validation from "./validation.json";import workflows from "./workflows.json";
export default { auth, campaigns, common, errors, inbox, settings, validation, workflows,};Adicionar nas Constantes
Seção intitulada “Adicionar nas Constantes”export const NAMESPACES = [ "auth", "campaigns", "common", "errors", "inbox", "settings", "validation", "workflows",] as const;Atualizar Tipagem TypeScript
Seção intitulada “Atualizar Tipagem TypeScript”import "i18next";
import auth from "../locales/pt-BR/auth.json";import campaigns from "../locales/pt-BR/campaigns.json";import common from "../locales/pt-BR/common.json";import errors from "../locales/pt-BR/errors.json";import inbox from "../locales/pt-BR/inbox.json";import settings from "../locales/pt-BR/settings.json";import validation from "../locales/pt-BR/validation.json";import workflows from "../locales/pt-BR/workflows.json";
declare module "i18next" { interface CustomTypeOptions { defaultNS: "common"; resources: { auth: typeof auth; campaigns: typeof campaigns; common: typeof common; errors: typeof errors; inbox: typeof inbox; settings: typeof settings; validation: typeof validation; workflows: typeof workflows; }; }}Usar o Novo Namespace
Seção intitulada “Usar o Novo Namespace”import { useT } from "@gaio/i18n/react";
function CampaignsPage() { const { t } = useT("campaigns");
return ( <div> <h1>{t("title")}</h1> <Button>{t("create")}</Button> </div> );}// Acesso a keys aninhadast("campaigns:status.active"); // "Ativa"t("campaigns:messages.created_success"); // "Campanha criada com sucesso"Checklist Novo Namespace
Seção intitulada “Checklist Novo Namespace”- Criar
pt-BR/{namespace}.json - Criar
en/{namespace}.json - Adicionar import em
locales/pt-BR/index.ts - Adicionar import em
locales/en/index.ts - Adicionar em
NAMESPACESemconstants.ts - Adicionar em
resources.d.tspara intellisense - Testar com
useT('namespace')no componente
Integração com Zod
Seção intitulada “Integração com Zod”O zodMessages fornece mensagens de validação traduzidas para schemas Zod.
Mensagens Disponíveis
Seção intitulada “Mensagens Disponíveis”| Função | Exemplo de Uso | Key |
|---|---|---|
required() | .min(1, zodMessages.required()) | validation:required |
minLength(n) | .min(3, zodMessages.minLength(3)) | validation:min_length |
maxLength(n) | .max(100, zodMessages.maxLength(100)) | validation:max_length |
invalidEmail() | .email(zodMessages.invalidEmail()) | validation:invalid_email |
invalidUrl() | .url(zodMessages.invalidUrl()) | validation:invalid_url |
invalidPhone() | Custom validator | validation:invalid_phone |
passwordMismatch() | Refine de confirmação | validation:password_mismatch |
passwordMinLength() | .min(8, ...) | validation:password_min_length |
passwordUppercase() | Regex validator | validation:password_uppercase |
passwordLowercase() | Regex validator | validation:password_lowercase |
passwordNumber() | Regex validator | validation:password_number |
passwordSpecial() | Regex validator | validation:password_special |
Exemplo de Schema
Seção intitulada “Exemplo de Schema”import { z } from "zod";import { zodMessages } from "@gaio/i18n/core";
export const RegisterSchema = z .object({ name: z .string() .min(1, zodMessages.required()) .max(100, zodMessages.maxLength(100)),
email: z .string() .min(1, zodMessages.required()) .email(zodMessages.invalidEmail()),
password: z .string() .min(8, zodMessages.passwordMinLength()) .regex(/[A-Z]/, zodMessages.passwordUppercase()) .regex(/[a-z]/, zodMessages.passwordLowercase()) .regex(/[0-9]/, zodMessages.passwordNumber()),
confirmPassword: z.string(), }) .refine((data) => data.password === data.confirmPassword, { message: zodMessages.passwordMismatch(), path: ["confirmPassword"], });Mensagens Customizadas
Seção intitulada “Mensagens Customizadas”Para mensagens específicas de um form, use i18n.t() diretamente:
import { i18n, zodMessages } from "@gaio/i18n/core";
const WorkflowSchema = z.object({ name: z .string() .min(1, zodMessages.required()) .max(50, () => i18n.t("workflows:name_max_50")),});Pluralização e Interpolação
Seção intitulada “Pluralização e Interpolação”Pluralização
Seção intitulada “Pluralização”i18next usa sufixos para pluralização:
{ "message": "{{count}} mensagem", "message_plural": "{{count}} mensagens", "message_zero": "Nenhuma mensagem"}t("message", { count: 0 }); // "Nenhuma mensagem"t("message", { count: 1 }); // "1 mensagem"t("message", { count: 5 }); // "5 mensagens"Regras de Sufixos
Seção intitulada “Regras de Sufixos”| Sufixo | Quando usado |
|---|---|
| (nenhum) | count === 1 |
_plural | count !== 1 |
_zero | count === 0 (opcional) |
Interpolação com Formatação
Seção intitulada “Interpolação com Formatação”{ "price": "R$ {{value, number}}", "date": "{{date, datetime}}"}t("price", { value: 1234.56 }); // "R$ 1.234,56" (formatado pelo locale)Context (Gênero)
Seção intitulada “Context (Gênero)”{ "user_updated": "Usuário atualizado", "user_updated_male": "Usuário atualizado", "user_updated_female": "Usuária atualizada"}t("user_updated", { context: "female" }); // "Usuária atualizada"Melhores Práticas de Interpolação
Seção intitulada “Melhores Práticas de Interpolação”Quando Usar Interpolação
Seção intitulada “Quando Usar Interpolação”Use interpolação apenas para valores dinâmicos que só podem ser conhecidos em runtime:
// ✅ CORRETO - valor dinâmico (nome do usuário vem do banco)t("welcome", { name: user.name });
// ✅ CORRETO - valores numéricos dinâmicost("items_count", { count: items.length });
// ✅ CORRETO - timestamps e datast("last_updated", { date: formatDate(updatedAt) });Quando NÃO Usar Interpolação
Seção intitulada “Quando NÃO Usar Interpolação”Evite interpolação para valores conhecidos no momento da tradução. Use chaves separadas:
// ❌ ERRADO - concatenação de stringst("error_prefix") + errorType + t("error_suffix");
// ❌ ERRADO - interpolação para valores conhecidost("file_type_error", { type: "PDF" });
// ✅ CORRETO - chaves separadas e autocontidast("errors.invalid_pdf_file");t("errors.invalid_image_file");t("errors.invalid_document_file");Tabela de Decisão
Seção intitulada “Tabela de Decisão”| Cenário | Abordagem | Exemplo |
|---|---|---|
| Nome de usuário | Interpolação | Olá, {{name}} |
| Contagens | Interpolação + Pluralização | {{count}} mensagem / {{count}} mensagens |
| Tipos de arquivo conhecidos | Chaves separadas | invalid_pdf_file, invalid_image_file |
| Status conhecidos | Chaves separadas | status.active, status.inactive |
| Mensagens de erro específicas | Chaves separadas | errors.network_error, errors.auth_error |
| Métodos de pagamento | Chaves separadas | payment.credit_card, payment.paypal |
Formatação HTML em Traduções
Seção intitulada “Formatação HTML em Traduções”Quando traduções precisam conter formatação (negrito, itálico, links), use o componente Trans do react-i18next em vez de dangerouslySetInnerHTML.
defaultTransComponents
Seção intitulada “defaultTransComponents”Use o helper defaultTransComponents para elementos HTML padrão:
import { defaultTransComponents } from "@gaio/i18n/react";
// Elementos suportados:// <b> e <strong> → <strong className="font-semibold" />// <i> e <em> → <em className="italic" />// <br> → <br />// <highlight> → <span className="font-medium text-primary" />Exemplo Completo
Seção intitulada “Exemplo Completo”import { Trans, defaultTransComponents } from "@gaio/i18n/react";
export const InviteDialog = () => { return ( <p className="text-muted-foreground text-xs"> <Trans components={defaultTransComponents} i18nKey="settings:collaboration_invite.invites_count" values={{ current: fields.length, max: 20 }} /> </p> );};Quando Usar Trans vs t()
Seção intitulada “Quando Usar Trans vs t()”| Cenário | Use | Exemplo |
|---|---|---|
| Texto simples sem formatação | t() | t('common:labels.save') |
| Texto com interpolação simples | t() | t('messages.hello', { name }) |
| Texto com negrito, itálico, etc. | Trans | <Trans i18nKey="key" components={defaultTransComponents} /> |
| Texto com links | Trans + createLinkComponent | Ver exemplo acima |
Seletor de Idioma
Seção intitulada “Seletor de Idioma”Componente de Seleção
Seção intitulada “Componente de Seleção”import { useT, useLocale, changeLocale } from "@gaio/i18n/react";import { LOCALE_LABELS, SUPPORTED_LOCALES } from "@gaio/i18n/core";
export function LanguageSelector() { const { t } = useT(); const currentLocale = useLocale();
return ( <Field> <Label>{t("settings:language")}</Label> <Select value={currentLocale} onValueChange={changeLocale}> {SUPPORTED_LOCALES.map((code) => ( <SelectItem key={code} value={code}> {LOCALE_LABELS[code]} </SelectItem> ))} </Select> </Field> );}Persistência
Seção intitulada “Persistência”A preferência de idioma é salva automaticamente em um cookie ga:i18n (duração: 1 ano).
Ordem de detecção:
cookie(preferência salva)navigator.language(idioma do navegador)pt-BR(fallback)
CLI e Extração
Seção intitulada “CLI e Extração”Comandos
Seção intitulada “Comandos”# Extrair strings do código para os JSONsnpm run i18n:extract -w @gaio/i18n
# Verificar se há strings não extraídas (CI)npm run i18n:check -w @gaio/i18nWorkflow de Tradução
Seção intitulada “Workflow de Tradução”- Desenvolver feature com strings usando
t('key') - Rodar
npm run i18n:extract -w @gaio/i18n - Preencher traduções em
pt-BR/*.json
Validação i18n (CI/CD)
Seção intitulada “Validação i18n (CI/CD)”Comandos da CLI Gaio
Seção intitulada “Comandos da CLI Gaio”gaio i18n validate # Validação completa (usado no CI)gaio i18n validate --fix # Validação com auto-fix de chaves órfãsgaio i18n extract # Extrair chaves de tradução do códigogaio i18n status # Ver status das traduções (completude)gaio i18n lint # Detectar hardcoded stringsO que é Validado
Seção intitulada “O que é Validado”- Chaves Órfãs - Chaves que existem nos JSONs mas não são usadas no código
- Hardcoded Strings - Strings visíveis ao usuário que não usam
t() - Status das Traduções - Chaves faltantes entre idiomas
Workflow de Desenvolvimento
Seção intitulada “Workflow de Desenvolvimento”gaio i18n extract- Extrair novas chaves- Preencher traduções em ambos idiomas
gaio i18n validate- Validar localmente- Corrigir problemas com
--fixse necessário
Test Provider
Seção intitulada “Test Provider”import { I18nProvider } from "@gaio/i18n/react";
export function TestI18nWrapper({ children }: { children: ReactNode }) { return <I18nProvider>{children}</I18nProvider>;}
render( <TestI18nWrapper> <MyComponent /> </TestI18nWrapper>,);Mock de Traduções
Seção intitulada “Mock de Traduções”vi.mock("@gaio/i18n/react", () => ({ useT: () => ({ t: (key: string) => key, }),}));Anti-patterns
Seção intitulada “Anti-patterns”// ❌ ERRADO - Strings hardcoded<Button>Salvar</Button>;
// ✅ CORRETOconst { t } = useT();<Button>{t("common:actions.save")}</Button>;// ❌ ERRADO - Concatenação de stringst("you_have") + count + t("messages");
// ✅ CORRETO - Interpolaçãot("you_have_messages", { count });// ❌ ERRADO - Keys em portuguêst("validation:campo_obrigatorio");
// ✅ CORRETOt("validation:required");// ❌ ERRADO - useT em loops{ items.map((item) => { const { t } = useT(); // Hook em cada iteração! return <span>{t("item")}</span>; });}
// ✅ CORRETOconst { t } = useT();{ items.map((item) => <span key={item.id}>{t("item")}</span>);}TypeScript
Seção intitulada “TypeScript”Para ter intellisense das variáveis de interpolação em cada tradução, configure os tipos:
import "i18next";import common from "../locales/pt-BR/common.json";// ... outros imports
declare module "i18next" { interface CustomTypeOptions { defaultNS: "common"; resources: { common: typeof common; // ... outros namespaces }; }}Benefícios
Seção intitulada “Benefícios”// ✅ Autocomplete de keyst("welcome"); // Sugere todas as keys disponíveis
// ✅ Erro se key não existet("key_inexistente"); // TypeScript error!
// ✅ Autocomplete de variáveis de interpolaçãot("welcome", { name: "João" }); // TypeScript sabe que 'name' é obrigatórioReferências
Seção intitulada “Referências”- i18next: https://www.i18next.com
- react-i18next: https://react.i18next.com
- TypeScript setup: https://www.i18next.com/overview/typescript