Ir para o conteúdo

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.

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.
  • I18nProvider recebe locale, fallbackLocale e messages; escreve <html lang> e persiste em localStorage["tempest-locale"] (desative com storageKey={null}).
  • useI18n() expõe t, plural, formatNumber, formatDate, setLocale, locale, availableLocales; useTranslate() é o atalho só-t.
  • t interpola {placeholder}, cai no fallback e por fim devolve a própria chave — nunca quebra.
  • plural usa os sufixos _one / _other; plurais complexos pedem i18next.
  • createI18n roda fora do React; withLocale clona 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
  • UtilsformatCurrency / formatDate para PT-BR direto