Pular para o conteúdo principal

Internacionalizacao (i18n)

O template usa @cactus-agents/i18n com selecao de idioma em build-time. Apenas um idioma entra no bundle do cliente — zero overhead de idiomas nao utilizados.

Como funciona

Build-time
|
v
Vite alias ~i18n → @cactus-agents/i18n/locales/{BRAND_LANGUAGE}
|
v
import common from "~i18n/common.json" (resolve para pt-br/common.json)
|
v
initI18n({ language: __BRAND_LANGUAGE__, resources, overrides })
|
v
i18nInstance (singleton exportado) + <TranslationProvider>
|
v
useTranslation("namespace") em qualquer componente

Configuracao

app/context/i18n.tsx

O arquivo importa os 12 namespaces estaticamente e inicializa o i18next:

import { deepMerge, initI18n } from "@cactus-agents/i18n";
import auth from "~i18n/auth.json";
import casino from "~i18n/casino.json";
import common from "~i18n/common.json";
import consent from "~i18n/consent.json";
import countries from "~i18n/countries.json";
import gamification from "~i18n/gamification.json";
import layout from "~i18n/layout.json";
import payments from "~i18n/payments.json";
import profiles from "~i18n/profiles.json";
import sports from "~i18n/sports.json";
import user from "~i18n/user.json";
import validation from "~i18n/validation.json";

const baseResources = {
common,
auth,
consent,
countries,
payments,
casino,
user,
validation,
sports,
gamification,
layout,
profiles,
};

export const i18nInstance = initI18n({
language: __BRAND_LANGUAGE__,
resources: baseResources,
overrides: loadOverrides(),
});

i18nInstance e exportado de proposito: codigo server-side (loaders, funcoes meta) resolve strings traduzidas sem passar por contexto React. E um singleton de modulo inicializado com __BRAND_LANGUAGE__ (substituido pelo Vite em build), entao so um idioma fica ativo por build — mesma restricao do uso no React.

Variaveis de ambiente

VariavelContextoDescricao
BRAND_LANGUAGEBuild-time (Vite define)Idioma do build; o Vite injeta como __BRAND_LANGUAGE__

Idiomas suportados

@cactus-agents/i18n publica 7 diretorios de locale (packages/i18n/locales/), cada um com os 12 namespaces — 84 arquivos JSON no total:

CodigoIdioma
pt-brPortugues (Brasil)
ptPortugues (Portugal)
enIngles
en-ngIngles (Nigeria)
esEspanhol
es-clEspanhol (Chile)
fi-fiFinlandes

:::warning es-mx nao existe Nao ha locale es-mx no SDK. Uma brand mexicana usa es. :::

Namespaces

NamespaceDescricaoExemplos de chaves
commonUI generica (defaultNS)button.save, status.active, error.network
authAutenticacaologin.email, register.password, forgot_password.title
consentBanner/preferencias de cookies
countriesPaises/nacionalidadesnationalities.BRA, countryNames.MEX
paymentsPagamentosdeposit.amount, withdraw.confirm, pix.qr_code
casinoCassinocasino.popular, search.no_results, detail.rtp
userContaaccount.name, wallet.balance, protection.self_exclusion
validationValidacoessteps.email.title, blocker.message
sportsEsportestitle, live, my_bets
gamificationVIPmissions.daily, tournaments.prize_pool, store.redeem
layoutApp shellheader.deposit, sidebar.casino, footer.copyright
profilesPerfis oficiais

:::warning O namespace de cassino chama casino Foi renomeado de games para casino. O arquivo em cada locale e casino.json; games.json nao existe. Uso: useTranslation("casino").

Atencao: a constante TRANSLATION_NAMESPACES exportada pelo SDK ainda lista "games" em vez de "casino" — ela e puramente informativa (initI18n aceita Record<string, …> e nao valida contra ela), mas nao a use como inventario confiavel. A fonte de verdade e o conteudo de packages/i18n/locales/<lang>/. :::

Uso em componentes

import { useTranslation } from "react-i18next";

function DepositButton() {
const { t } = useTranslation("payments");
return <button>{t("deposit.submit")}</button>;
}

// Multiplos namespaces
function Header() {
const { t } = useTranslation(["layout", "common"]);
return <span>{t("layout:header.deposit")}</span>;
}

// Interpolacao
function Greeting({ name }: { name: string }) {
const { t } = useTranslation("common");
return <span>{t("greeting", { name })}</span>;
}

Overrides — dois niveis

Overrides sobrescrevem chaves especificas sem duplicar o JSON inteiro. Existem duas camadas, e a distincao importa: a primeira vale pro base todo, a segunda so pra uma brand.

Nivel 1 — base-wide (app/locales/overrides/**)

Globbado por loadOverrides() em app/context/i18n.tsx:

import.meta.glob(["../locales/overrides/*.json", "../locales/overrides/*/*.json"], {
eager: true,
import: "default",
});

Dois formatos aceitos:

app/locales/overrides/
├── layout.json # language-agnostic (vale pra qualquer BRAND_LANGUAGE)
└── <lang>/
└── layout.json # language-specific (so quando __BRAND_LANGUAGE__ === <lang>)

O language-specific deep-merge por cima do agnostic. Hoje o base usa somente o formato por idioma — ha um diretorio por locale suportado (en, en-ng, es, es-cl, fi-fi, pt, pt-br), cada um com um subconjunto de namespaces (casino, common, gamification, layout, user). Consulte app/locales/overrides/ pra lista atual.

Nivel 2 — por brand (app/locales/brand-overrides.ts)

O base envia um mapa vazio:

// app/locales/brand-overrides.ts
export const brandOverrideModules: Record<string, Record<string, unknown>> = {};

O brandOverridesPlugin redireciona o import ~/locales/brand-overrides para overrides/<brand>/app/locales/brand-overrides.ts, cujo proprio import.meta.glob resolve relativo a si mesmo e por isso enxerga apenas os JSONs daquela brand:

// overrides/<brand>/app/locales/brand-overrides.ts
export const brandOverrideModules = import.meta.glob<Record<string, unknown>>(
["./overrides/*.json", "./overrides/*/*.json"],
{ eager: true, import: "default" },
);

:::info Por que precisa desse arquivo intermediario Globbar o diretorio da brand direto do base e impossivel: import.meta.glob nao le caminho fora de app/, e o plugin so reescreve imports ~/ (nao padroes de glob relativos). O modulo file-replaced e a unica saida. :::

O layout dos arquivos e o mesmo do nivel 1: overrides/<namespace>.json (agnostic) e overrides/<lang>/<namespace>.json (language-specific).

Ordem de precedencia

Do mais fraco pro mais forte:

  1. Locales do core (@cactus-agents/i18n/locales/<lang>/)
  2. Base agnostic — app/locales/overrides/<namespace>.json
  3. Base language-specific — app/locales/overrides/<lang>/<namespace>.json
  4. Brand agnostic — overrides/<brand>/app/locales/overrides/<namespace>.json
  5. Brand language-specific — overrides/<brand>/app/locales/overrides/<lang>/<namespace>.json

O merge e recursivo (deepMerge do SDK), entao apenas as chaves declaradas sao substituidas.

Exemplo — override de common.json

{
"button": {
"save": "Gravar"
}
}

Apenas button.save e sobrescrito; todas as outras chaves de common mantem o valor do SDK.

Adicionando traducoes

:::warning Traducao nova vai no core Nao crie traducoes no base (app/locales/overrides/) a menos que seja explicitamente uma customizacao de wording. Chave nova vai em @cactus-agents/i18n. :::

Chave nova num namespace existente

  1. Adicionar a chave em packages/i18n/locales/<lang>/<namespace>.jsonem todos os 7 locales
  2. Publicar versao nova do @cactus-agents/i18n
  3. Bumpar a dependencia no base

Namespace novo

  1. Criar packages/i18n/locales/<lang>/<namespace>.json nos 7 locales
  2. Adicionar o nome em TRANSLATION_NAMESPACES (packages/i18n/src/types.ts) — mantendo a lista alinhada com os arquivos reais
  3. Importar no app/context/i18n.tsx do template e adicionar a baseResources
  4. Usar via useTranslation("<namespace>")