Pular para o conteúdo

Internacionalização (i18n)


BibliotecaVersãoPropósito
i18next25.xCore de internacionalização
react-i18next16.xBindings React
i18next-browser-languagedetector8.xDetecção automática
i18next-chained-backend5.xMúltiplos backends
i18next-localstorage-backend4.xCache em localStorage
i18next-resources-to-backend1.xConverter recursos inline
i18next-cli1.xExtração de strings (CLI)
CódigoNomeStatus
pt-BRPortuguês (Brasil)Fonte
enEnglishTradução
┌─────────────────────────────────────────────────────────────┐
│ @gaio/i18n │
│ (Package dedicado para internacionalização) │
└─────────────────────────┬───────────────────────────────────┘
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌───────────┐ ┌───────────────┐
│ I18nProvider │ │ useT │ │ zodMessages │
│ (Wrapper App) │ │ (Hook) │ │ (Validação) │
└─────────────────┘ └───────────┘ └───────────────┘
┌─────────────────────────────────────────────────────────────┐
│ locales/ │
│ pt-BR/common.json │ en/common.json │ ... │
└─────────────────────────────────────────────────────────────┘

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óduloUsoExemplos
@gaio/i18n/coreArquivos sem ReactSchemas Zod, utils, services, constantes
@gaio/i18n/reactComponentes ReactHooks, Provider, componentes UI

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)
package.json
{
"exports": {
"./core": "./src/core/index.ts",
"./react": "./src/react/index.ts"
}
}

O I18nProvider deve ser o provider mais externo na aplicação:

packages/console/src/providers.tsx
import { I18nProvider } from "@gaio/i18n/react";
export function AppProviders({ children }: { children: ReactNode }) {
return (
<I18nProvider>
<TrackingProvider>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</TrackingProvider>
</I18nProvider>
);
}

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>;
}
// Namespace único
const { t } = useT("validation");
t("required"); // validation:required
// Múltiplos namespaces
const { t } = useT(["common", "validation"]);
t("common:actions.save");
t("validation:required");
PropriedadeTipoDescrição
t(key, options?) => stringFunção de tradução
i18ni18nInstância i18next
readybooleanSe traduções estão prontas

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>;
}

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
}

IMPORTANTE: Todas as keys devem estar em inglês usando snake_case:

// ✅ CORRETO - keys em inglês, descritivas
t("validation:required");
t("validation:invalid_email");
t("common:actions.save");
t("common:messages.success.saved");
// ❌ ERRADO - keys em português
t("validation:campo_obrigatorio");
t("validation:email_invalido");
t("common:acoes.salvar");
import { useT } from "@gaio/i18n/react";
function SaveButton() {
const { t } = useT();
return <Button>{t("common:actions.save")}</Button>;
}
locales/pt-BR/common.json
{
"actions": {
"save": "Salvar",
"cancel": "Cancelar",
"confirm": "Confirmar"
}
}
const { t } = useT();
// Simples
t("welcome", { name: "João" }); // "Olá, João!"
// Múltiplas variáveis
t("summary", { count: 5, total: 100 }); // "5 de 100 itens"
{
"welcome": "Olá, {{name}}!",
"summary": "{{count}} de {{total}} itens"
}

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>"
}
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"));
}
};
}

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}`);
}

Os namespaces organizam traduções por domínio/feature:

NamespaceConteúdoUso
commonBotões, labels, statusUI compartilhada
validationMensagens de validaçãoSchemas Zod
errorsErros HTTP/APIError handlers
authLogin, registroscreens/auth/
inboxChat, mensagensscreens/dashboard/inbox/
workflowsEditor de automaçãoscreens/dashboard/workflows/
settingsConfiguraçõesscreens/dashboard/settings/

O namespace common contém elementos reutilizáveis em toda a aplicação. Sempre use essas keys ao invés de criar duplicatas.

common.json
{
"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"
common.json
{
"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"
common.json
{
"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"
common.json
{
"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")} />
common.json
{
"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"));
common.json
{
"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"
common.json
{
"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"
}
}
// ❌ ERRADO - Criar key específica para algo comum
t("workflows:save");
t("inbox:cancel_button");
t("settings:delete_account");
// ✅ CORRETO - Usar vocabulary padrão
t("common:actions.save");
t("common:actions.cancel");
t("common:actions.delete");
// ✅ CORRETO - Key específica apenas para contexto único
t("settings:account.delete_permanently"); // Ação específica com consequência diferente

Guia passo a passo para adicionar um novo escopo de tradução.

Terminal window
# Estrutura de arquivos
packages/i18n/src/locales/
├── pt-BR/
└── campaigns.json # NOVO
└── en/
└── campaigns.json # NOVO
packages/i18n/src/locales/pt-BR/campaigns.json
{
"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"
}
}
packages/i18n/src/locales/en/campaigns.json
{
"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"
}
}
packages/i18n/src/locales/pt-BR/index.ts
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,
};
packages/i18n/src/core/constants.ts
export const NAMESPACES = [
"auth",
"campaigns",
"common",
"errors",
"inbox",
"settings",
"validation",
"workflows",
] as const;
packages/i18n/src/types/resources.d.ts
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;
};
}
}
packages/console/src/screens/dashboard/campaigns/index.tsx
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 aninhadas
t("campaigns:status.active"); // "Ativa"
t("campaigns:messages.created_success"); // "Campanha criada com sucesso"
  • 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 NAMESPACES em constants.ts
  • Adicionar em resources.d.ts para intellisense
  • Testar com useT('namespace') no componente

O zodMessages fornece mensagens de validação traduzidas para schemas Zod.

FunçãoExemplo de UsoKey
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 validatorvalidation:invalid_phone
passwordMismatch()Refine de confirmaçãovalidation:password_mismatch
passwordMinLength().min(8, ...)validation:password_min_length
passwordUppercase()Regex validatorvalidation:password_uppercase
passwordLowercase()Regex validatorvalidation:password_lowercase
passwordNumber()Regex validatorvalidation:password_number
passwordSpecial()Regex validatorvalidation:password_special
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"],
});

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")),
});

i18next usa sufixos para pluralização:

locales/pt-BR/common.json
{
"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"
SufixoQuando usado
(nenhum)count === 1
_pluralcount !== 1
_zerocount === 0 (opcional)
{
"price": "R$ {{value, number}}",
"date": "{{date, datetime}}"
}
t("price", { value: 1234.56 }); // "R$ 1.234,56" (formatado pelo locale)
{
"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"

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âmicos
t("items_count", { count: items.length });
// ✅ CORRETO - timestamps e datas
t("last_updated", { date: formatDate(updatedAt) });

Evite interpolação para valores conhecidos no momento da tradução. Use chaves separadas:

// ❌ ERRADO - concatenação de strings
t("error_prefix") + errorType + t("error_suffix");
// ❌ ERRADO - interpolação para valores conhecidos
t("file_type_error", { type: "PDF" });
// ✅ CORRETO - chaves separadas e autocontidas
t("errors.invalid_pdf_file");
t("errors.invalid_image_file");
t("errors.invalid_document_file");
CenárioAbordagemExemplo
Nome de usuárioInterpolaçãoOlá, {{name}}
ContagensInterpolação + Pluralização{{count}} mensagem / {{count}} mensagens
Tipos de arquivo conhecidosChaves separadasinvalid_pdf_file, invalid_image_file
Status conhecidosChaves separadasstatus.active, status.inactive
Mensagens de erro específicasChaves separadaserrors.network_error, errors.auth_error
Métodos de pagamentoChaves separadaspayment.credit_card, payment.paypal

Quando traduções precisam conter formatação (negrito, itálico, links), use o componente Trans do react-i18next em vez de dangerouslySetInnerHTML.

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" />
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>
);
};
CenárioUseExemplo
Texto simples sem formataçãot()t('common:labels.save')
Texto com interpolação simplest()t('messages.hello', { name })
Texto com negrito, itálico, etc.Trans<Trans i18nKey="key" components={defaultTransComponents} />
Texto com linksTrans + createLinkComponentVer exemplo acima

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>
);
}

A preferência de idioma é salva automaticamente em um cookie ga:i18n (duração: 1 ano).

Ordem de detecção:

  1. cookie (preferência salva)
  2. navigator.language (idioma do navegador)
  3. pt-BR (fallback)

Terminal window
# Extrair strings do código para os JSONs
npm run i18n:extract -w @gaio/i18n
# Verificar se há strings não extraídas (CI)
npm run i18n:check -w @gaio/i18n
  1. Desenvolver feature com strings usando t('key')
  2. Rodar npm run i18n:extract -w @gaio/i18n
  3. Preencher traduções em pt-BR/*.json

Terminal window
gaio i18n validate # Validação completa (usado no CI)
gaio i18n validate --fix # Validação com auto-fix de chaves órfãs
gaio i18n extract # Extrair chaves de tradução do código
gaio i18n status # Ver status das traduções (completude)
gaio i18n lint # Detectar hardcoded strings
  1. Chaves Órfãs - Chaves que existem nos JSONs mas não são usadas no código
  2. Hardcoded Strings - Strings visíveis ao usuário que não usam t()
  3. Status das Traduções - Chaves faltantes entre idiomas
  1. gaio i18n extract - Extrair novas chaves
  2. Preencher traduções em ambos idiomas
  3. gaio i18n validate - Validar localmente
  4. Corrigir problemas com --fix se necessário

import { I18nProvider } from "@gaio/i18n/react";
export function TestI18nWrapper({ children }: { children: ReactNode }) {
return <I18nProvider>{children}</I18nProvider>;
}
render(
<TestI18nWrapper>
<MyComponent />
</TestI18nWrapper>,
);
vi.mock("@gaio/i18n/react", () => ({
useT: () => ({
t: (key: string) => key,
}),
}));

// ❌ ERRADO - Strings hardcoded
<Button>Salvar</Button>;
// ✅ CORRETO
const { t } = useT();
<Button>{t("common:actions.save")}</Button>;
// ❌ ERRADO - Concatenação de strings
t("you_have") + count + t("messages");
// ✅ CORRETO - Interpolação
t("you_have_messages", { count });
// ❌ ERRADO - Keys em português
t("validation:campo_obrigatorio");
// ✅ CORRETO
t("validation:required");
// ❌ ERRADO - useT em loops
{
items.map((item) => {
const { t } = useT(); // Hook em cada iteração!
return <span>{t("item")}</span>;
});
}
// ✅ CORRETO
const { t } = useT();
{
items.map((item) => <span key={item.id}>{t("item")}</span>);
}

Para ter intellisense das variáveis de interpolação em cada tradução, configure os tipos:

packages/i18n/src/types/resources.d.ts
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
};
}
}
// ✅ Autocomplete de keys
t("welcome"); // Sugere todas as keys disponíveis
// ✅ Erro se key não existe
t("key_inexistente"); // TypeScript error!
// ✅ Autocomplete de variáveis de interpolação
t("welcome", { name: "João" }); // TypeScript sabe que 'name' é obrigatório