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
| Variavel | Contexto | Descricao |
|---|---|---|
BRAND_LANGUAGE | Build-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:
| Codigo | Idioma |
|---|---|
pt-br | Portugues (Brasil) |
pt | Portugues (Portugal) |
en | Ingles |
en-ng | Ingles (Nigeria) |
es | Espanhol |
es-cl | Espanhol (Chile) |
fi-fi | Finlandes |
:::warning es-mx nao existe
Nao ha locale es-mx no SDK. Uma brand mexicana usa es.
:::
Namespaces
| Namespace | Descricao | Exemplos de chaves |
|---|---|---|
common | UI generica (defaultNS) | button.save, status.active, error.network |
auth | Autenticacao | login.email, register.password, forgot_password.title |
consent | Banner/preferencias de cookies | — |
countries | Paises/nacionalidades | nationalities.BRA, countryNames.MEX |
payments | Pagamentos | deposit.amount, withdraw.confirm, pix.qr_code |
casino | Cassino | casino.popular, search.no_results, detail.rtp |
user | Conta | account.name, wallet.balance, protection.self_exclusion |
validation | Validacoes | steps.email.title, blocker.message |
sports | Esportes | title, live, my_bets |
gamification | VIP | missions.daily, tournaments.prize_pool, store.redeem |
layout | App shell | header.deposit, sidebar.casino, footer.copyright |
profiles | Perfis 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:
- Locales do core (
@cactus-agents/i18n/locales/<lang>/) - Base agnostic —
app/locales/overrides/<namespace>.json - Base language-specific —
app/locales/overrides/<lang>/<namespace>.json - Brand agnostic —
overrides/<brand>/app/locales/overrides/<namespace>.json - 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
- Adicionar a chave em
packages/i18n/locales/<lang>/<namespace>.json— em todos os 7 locales - Publicar versao nova do
@cactus-agents/i18n - Bumpar a dependencia no base
Namespace novo
- Criar
packages/i18n/locales/<lang>/<namespace>.jsonnos 7 locales - Adicionar o nome em
TRANSLATION_NAMESPACES(packages/i18n/src/types.ts) — mantendo a lista alinhada com os arquivos reais - Importar no
app/context/i18n.tsxdo template e adicionar abaseResources - Usar via
useTranslation("<namespace>")