i18n
i18n minimalista (≈ 1.5KB gzip) embutido no SDK. Cobre o caso comum — 2 ou 3 idiomas, interpolação e plural simples — sem o peso de uma biblioteca completa.
Por que um i18n próprio?
A maioria dos apps Tempest precisa apenas de PT-BR + EN com algumas dezenas de strings. Puxar i18next (e seus plugins de backend, detector e formatador) para isso adiciona kilobytes e configuração que ninguém vai manter. O SDK entrega o suficiente para o caso simples e barato — e sai do caminho quando você precisa de mais.
O catálogo
Tudo começa por um catálogo: um mapa { [locale]: { [key]: "texto" } }. As chaves são suas — use nomes planos, com namespace ("auth.login") ou o que combinar com o seu pipeline de strings. O SDK não impõe schema.
// src/i18n.ts
import type { Catalog } from "tempest-react-sdk";
export const messages: Catalog = {
"pt-BR": {
greet: "Olá, {name}",
"nav.home": "Início",
alos_one: "{count} Alô",
alos_other: "{count} Alôs",
},
en: {
greet: "Hi, {name}",
"nav.home": "Home",
alos_one: "{count} Alo",
alos_other: "{count} Alos",
},
};
Montando o provider
Envolva a árvore com I18nProvider, informando o locale inicial, um fallbackLocale opcional e o catálogo. Aqui vai um app completo e executável:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { I18nProvider } from "tempest-react-sdk";
import "tempest-react-sdk/styles.css";
import { messages } from "./i18n";
import { App } from "./App";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<I18nProvider locale="pt-BR" fallbackLocale="en" messages={messages}>
<App />
</I18nProvider>
</StrictMode>,
);
Grátis: <html lang> e persistência
O I18nProvider escreve <html lang="pt-BR"> automaticamente (bom para SEO e leitores de tela) e persiste a escolha do usuário em localStorage["tempest-locale"]. Para desativar a persistência, passe storageKey={null}.
Traduzindo na UI
useI18n() devolve os helpers de tradução mais setLocale, locale e availableLocales. Um componente completo:
import { useI18n } from "tempest-react-sdk";
export function Header() {
const { t, plural, formatNumber, locale, availableLocales, setLocale } = useI18n();
return (
<header>
<p>{t("greet", { name: "Mau" })}</p> {/* "Olá, Mau" */}
<p>{plural("alos", 3)}</p> {/* "3 Alôs" */}
<p>{formatNumber(1234.5, { style: "currency", currency: "BRL" })}</p> {/* "R$ 1.234,50" */}
<select value={locale} onChange={(event) => setLocale(event.target.value)}>
{availableLocales.map((code) => (
<option key={code} value={code}>
{code}
</option>
))}
</select>
</header>
);
}
Quando só precisa de t, use o atalho useTranslate() — evita o destructuring:
import { useTranslate } from "tempest-react-sdk";
function NavHome() {
const t = useTranslate();
return <a href="/">{t("nav.home")}</a>;
}
useI18n exige o provider
Chamar useI18n() (ou useTranslate()) fora de um <I18nProvider> lança useI18n must be used inside an <I18nProvider>. Mantenha o provider acima de qualquer componente que traduza.
useOptionalI18n() para código que precisa funcionar sem catálogo
Devolve null fora do provider em vez de lançar. É o que uma peça reutilizável usa quando quer ser traduzida onde existe catálogo e continuar funcionando onde não existe — i18n é opt-in neste SDK, e exigir o provider transformaria "você não configurou tradução" em crash. O useDescribeApiError do módulo http é exatamente esse caso.
const i18n = useOptionalI18n();
const titulo = i18n?.t("dashboard.title") ?? "Painel";
Interpolação
Placeholders no formato {name} são substituídos pelos valores passados em params. Uma chave ausente cai para o fallbackLocale; se também faltar lá, o helper devolve a própria chave (nunca quebra a tela). Um placeholder sem valor permanece literal — {name} — facilitando achar a string esquecida.
t("greet", { name: "Ana" }); // "Olá, Ana"
t("greet"); // "Olá, {name}" (sem params → placeholder literal)
t("inexistente"); // "inexistente" (sem fallback → a própria chave)
Texto default por chave
Quando a chave não existe em nenhum dos locales, t devolve a própria chave — o que é ótimo para achar string esquecida em desenvolvimento e péssimo para o usuário, que lê cart.empty na tela. O terceiro argumento resolve:
t("cart.empty", undefined, { default: "Seu carrinho está vazio" });
// catálogo definiu → a tradução
// catálogo não definiu → "Seu carrinho está vazio"
O default é interpolado como qualquer outra mensagem, então carrega placeholders:
t("cart.count", { n: 3 }, { default: "{n} itens" }); // "3 itens"
plural aceita o mesmo, depois das duas buscas com sufixo:
plural("boxes", 2, undefined, { default: "{count} caixas" }); // "2 caixas"
Não detecte o miss comparando com a chave
A tentação é const v = t(k); const texto = v === k ? meuDefault : v;. Isso está
errado para um catálogo que mapeia legitimamente a chave para ela mesma —
{ "cart.empty": "cart.empty" }, o que catálogo gerado por máquina ou catálogo
placeholder faz — e nesse caso o seu default vence uma tradução que existia.
Só a camada de i18n sabe se houve miss, porque só ela viu o catálogo. Passe
default e deixe ela responder. Este é o caminho que as strings internas do
próprio SDK usam (tempest.error.offline, em useDescribeApiError).
Plurais
plural(key, count, params?) escolhe entre ${key}_one (quando count === 1) e ${key}_other (qualquer outro valor), com {count} disponível na interpolação:
plural("alos", 1); // "1 Alô" → usa alos_one
plural("alos", 5); // "5 Alôs" → usa alos_other
Plurais ricos? Vá de i18next
Esse esquema _one / _other cobre PT-BR e EN. Idiomas com mais categorias plurais (russo, polonês, árabe) precisam de Intl.PluralRules — não vale a pena reimplementar aqui. Para esses casos, troque por i18next diretamente. O SDK assume o caso simples deliberadamente.
Sem React (imperativo)
createI18n é a base; o provider só adiciona o estado React por cima. Use-o em utilitários, testes ou loaders fora da árvore de componentes:
import { createI18n } from "tempest-react-sdk";
import { messages } from "./i18n";
const i18n = createI18n({ locale: "pt-BR", fallbackLocale: "en", messages });
i18n.t("greet", { name: "Mau" }); // "Olá, Mau"
i18n.formatDate(new Date(), { dateStyle: "long" });
const en = i18n.withLocale("en"); // novo I18n, mesmo catálogo
en.t("greet", { name: "Mau" }); // "Hi, Mau"
Carregamento dinâmico
Carregue um JSON por locale e passe para o provider. Para code-split por rota, troque messages via state — chaves não encontradas caem no fallback enquanto o resto carrega:
const ptBR = await fetch("/i18n/pt-BR.json").then((response) => response.json());
Recap
- O catálogo é um mapa
{ locale: { key: "texto" } }— chaves livres, sem schema. I18nProviderrecebelocale,fallbackLocaleemessages; escreve<html lang>e persiste emlocalStorage["tempest-locale"](desative comstorageKey={null}).useI18n()expõet,plural,formatNumber,formatDate,setLocale,locale,availableLocales;useTranslate()é o atalho só-t.tinterpola{placeholder}, cai no fallback e por fim devolve a própria chave — nunca quebra.pluralusa os sufixos_one/_other; plurais complexos pedemi18next.createI18nroda fora do React;withLocaleclona o objeto em outro idioma.
Veja também
- Tema — combinar com
<html lang>é grátis - App Providers — montar i18n junto com Query, Theme e ErrorBoundary
- Utils —
formatCurrency/formatDatepara PT-BR direto