Pular para o conteúdo
~/.primo-academy.sh
☰ Aulas · i18n / l10n: Intl APIs, ICU MessageFormat, RTL, translation workflow e pseudo-localization · 0/7
Recomendado: essencial

Translation workflow: i18next, Locize/Crowdin, glossarios, plural rules

5 min de leitura

fonte

Voce ja' sabe formatar (Intl), pluralizar (ICU), e fazer RTL funcionar. Agora vem o workflow real: como organizar arquivos de traducao por locale, integrar com i18next ou FormatJS, e gerenciar as traducoes com o time (developer + tradutor + PM). Esse no cobre o workflow completo - da estrutura de arquivos ao deploy.

Em producao, traducoes nao ficam hardcoded no codigo. Vaõ em arquivos JSON/YAML por locale, versionados em Git ou gerenciados em SaaS (Locize, Crowdin, Phrase). O i18next ou FormatJS carrega sob demanda e expõe funcoes t() que o codigo usa. O tradutor edita em uma UI web (Locize), o PM valida contexto, e o deploy pega tudo pronto.

Voce sai de "strings hardcoded" pra "workflow escalavel com 12 idiomas e 3 tradutores externos".

O essencial 🟢

Estrutura classica de arquivos.

src/
  i18n/
    locales/
      pt-BR/
        common.json
        cart.json
        checkout.json
      en-US/
        common.json
        cart.json
        checkout.json
      ja-JP/
        common.json
        cart.json
        checkout.json
    config.ts        # setup do i18next
    index.ts         # exporta t() e useTranslation()

common.json (chaves de UI compartilhadas).

// locales/pt-BR/common.json
{
  "app_name": "Minha App",
  "nav": {
    "home": "Início",
    "about": "Sobre",
    "contact": "Contato"
  },
  "buttons": {
    "save": "Salvar",
    "cancel": "Cancelar",
    "delete": "Excluir"
  }
}
// locales/en-US/common.json
{
  "app_name": "My App",
  "nav": {
    "home": "Home",
    "about": "About",
    "contact": "Contact"
  },
  "buttons": {
    "save": "Save",
    "cancel": "Cancel",
    "delete": "Delete"
  }
}

i18next setup basico.

pnpm add i18next react-i18next i18next-browser-languagedetector
// i18n/config.ts
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";

import ptBRCommon from "./locales/pt-BR/common.json";
import enUSCommon from "./locales/en-US/common.json";
import jaJPCommon from "./locales/ja-JP/common.json";

i18n
  .use(LanguageDetector)  // detecta do browser, localStorage, URL
  .use(initReactI18next)  // integra com React
  .init({
    resources: {
      "pt-BR": { common: ptBRCommon },
      "en-US": { common: enUSCommon },
      "ja-JP": { common: jaJPCommon },
    },
    fallbackLng: "en-US",     // locale padrao se user language nao tem
    defaultNS: "common",       // namespace padrao
    interpolation: {
      escapeValue: false,      // React ja escapa
    },
  });

export default i18n;

Usando no React.

import { useTranslation } from "react-i18next";

function BotaoSalvar() {
  const { t } = useTranslation();
  return <button>{t("buttons.save")}</button>;
}

// Com namespace especifico
function CartHeader() {
  const { t } = useTranslation("cart");
  return <h1>{t("title")}</h1>;
}

// Com interpolacao
function Welcome() {
  const { t } = useTranslation();
  return <p>{t("welcome", { name: "Ana" })}</p>;
}
// pt-BR: "Bem-vinda, Ana"
// en-US: "Welcome, Ana"

// Com plural (precisa i18next-icu)
function ItemCount({ count }: { count: number }) {
  const { t } = useTranslation();
  return <p>{t("cart.items", { count })}</p>;
}
// i18n/en-US: "cart_items": "You have {{count, plural, one {1 item} other {# items}}}"
// count=1: "You have 1 item"
// count=5: "You have 5 items"

Namespaces - separar concerns. Em apps grandes, 1 arquivo gigante vira pesadelo. Namespaces separam por feature:

locales/pt-BR/
  common.json     # "save", "cancel", "loading"
  auth.json       # "login", "logout", "register"
  cart.json       # "items", "total", "checkout"
  errors.json     # "network_error", "validation_failed"

Lazy load de namespaces (carrega so quando user entra na rota):

i18n.init({
  ns: ["common"],  // so carrega common no boot
});

// Lazy load
await i18n.loadNamespaces(["cart"]);
// agora t("cart.items") funciona

Detecção de locale - ordem de prioridade.

  1. URL (ex: meusite.com/pt-BR/...)
  2. Cookie/localStorage (escolha previa do user)
  3. Accept-Language header (browser)
  4. fallbackLng (default do app)
// i18next-browser-languagedetector config
i18n.use(LanguageDetector).init({
  detection: {
    order: ["path", "localStorage", "navigator"],
    lookupFromPathIndex: 0,    // /pt-BR/products
    caches: ["localStorage"],
  },
  fallbackLng: "en-US",
});

Translation management SaaS - Locize, Crowdin, Phrase. Quando o time tem traducao externa (3rd party, freelancers, agencia), gerenciar JSONs em Git vira bagunça (PRs, conflitos, falta de contexto). SaaS resolve:

  • Locize (locize.com) - mais usado em i18next ecosystem, plano free generoso.
  • Crowdin (crowdin.com) - plataforma completa, integra com GitHub.
  • Phrase (phrase.com) - enterprise, integra com Figma.
  • Lokalise (lokalise.com) - similar ao Locize, UI moderna.
  • Weblate (weblate.org) - open source, self-hosted.

Locize em i18next.

pnpm add i18next-locize-backend locize
import i18n from "i18next";
import LocizeBackend from "i18next-locize-backend";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";

i18n
  .use(LocizeBackend)
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    backend: {
      projectId: "your-locize-project-id",
      apiKey: "your-api-key",  // opcional, so pra write
    },
    fallbackLng: "en-US",
  });
// i18next busca traducoes de https://api.locize.app/...
// Traducoes editadas em Locize UI

Vantagens do Locize:

  • Tradutor edita em UI web, sem Git.
  • Contexto (screenshot, comentario) anexado a cada string.
  • Plural rules por locale.
  • Glossario (termos consistentes).
  • Auto-translate (Google/DeepL como rascunho).
  • Versionamento + rollback.
  • CDN pra deliver (rapido).

Plural rules no Locize. Locize tem suporte nativo a ICU MessageFormat - tradutor escreve plural/select direto na UI:

// En-US
cart_items: "You have {{count, plural, one {1 item} other {# items}}}"

// Pl-PL
cart_items: "Masz {{count, plural, one {1 przedmiot} few {# przedmioty} many {# przedmiotów} other {# przedmiotu}}}"

Glossarios - terminologia consistente. Em apps com 100+ strings, garantir que "save" sempre traduza pra "Salvar" (e nao "Gravar" ou "Salvar Dados") exige glossario:

EN: "Save" -> PT-BR: "Salvar"
EN: "Submit" -> PT-BR: "Enviar"
EN: "Cancel" -> PT-BR: "Cancelar"

Locize e Crowdin tem glossario built-in (com cores, contexto, exemplos). Tradutor recebe alerta se usar termo nao aprovado.

CI/CD - check de traducao faltando. Antes de fazer deploy, verifique que todas as chaves existem em todos os locales:

// scripts/check-translations.ts
import ptBRCommon from "../locales/pt-BR/common.json";
import enUSCommon from "../locales/en-US/common.json";

function flattenKeys(obj: any, prefix = ""): string[] {
  return Object.entries(obj).flatMap(([k, v]) => {
    const key = prefix ? `${prefix}.${k}` : k;
    if (typeof v === "object" && v !== null) {
      return flattenKeys(v, key);
    }
    return [key];
  });
}

const ptKeys = flattenKeys(ptBRCommon);
const enKeys = flattenKeys(enUSCommon);

const missing = ptKeys.filter((k) => !enKeys.includes(k));
if (missing.length > 0) {
  console.error("Missing translations in en-US:", missing);
  process.exit(1);
}

Adiciona no CI (GitHub Actions).

name: Check translations
on: [pull_request]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      - run: pnpm install
      - run: pnpm tsx scripts/check-translations.ts

Build-time vs runtime translation.

  • Build-time: t() retorna string direto, bundle maior, mas zero latencia (Vite plugin, webpack loader).
  • Runtime: traduzcoes carregadas dinamicamente, bundle menor, latencia inicial (default).
// Build-time (vite-plugin-i18next)
import { useTranslation } from "react-i18next";
// strings ja' vem no bundle, sem fetch

// Runtime (default)
import { useTranslation } from "react-i18next";
// carrega de /locales/*.json no boot

Escolha: app pequeno/medio = build-time. App grande/enterprise = runtime (CDN, cache, versionamento).

Aprofundamento 🟡

FormatJS (react-intl) vs i18next. Comparacao real:

Aspectoi18nextFormatJS / react-intl
Bundle~12KB~30KB
ICU nativovia pluginsim
Sintaxe{{var}}{var}
Lingua principalJS-firstReact-first
Translation mgmtLocize, CrowdinLocize, Crowdin
Type safetyvia typescript pluginsim (types nativos)
PluralICU (com plugin)ICU (nativo)
Adocao~10M downloads/semana~2M downloads/semana

i18next domina o ecossistema JS. FormatJS brilha em React enterprise com type safety forte.

LinguiJS - performance primeiro. Lingui compila ICU em estatico (bundle minimo, sem parser runtime). Em 2026, melhor escolha pra apps com performance critica.

i18n no SSR (Next.js, Remix, Astro). Server-side rendering i18n tem 2 opcoes:

  1. Subpath routing (/pt-BR/about): cada locale e' uma rota, SEO melhor, cache melhor.
  2. Subdomain (pt-br.site.com/about): mesmo conteudo, locale diferente.
// next.config.js (Next.js 13+ App Router)
const i18n = {
  defaultLocale: "pt-BR",
  locales: ["pt-BR", "en-US", "ja-JP"],
};

// app/[locale]/layout.tsx
export default function Layout({ children, params: { locale } }) {
  return (
    <html lang={locale} dir={isRtl(locale) ? "rtl" : "ltr"}>
      <body>{children}</body>
    </html>
  );
}

Versionamento de traducoes. Quando voce atualiza o app, algumas strings mudam. Traducoes velhas ficam stale. Estrategia:

  • Major version (1.x → 2.x): reseta traducoes (forca retraducao).
  • Minor version (1.0 → 1.1): mantem traducoes existentes, marca strings novas como "missing".
  • Locize tem "versions" - da' pra ter 1.0 e 1.1 simultaneas e migrar gradual.

Translation memory. SaaS (Locize, Crowdin) tem TM (Translation Memory): se voce traduziu "Save" → "Salvar" antes, a proxima vez que aparecer "Save", sugere "Salvar" automaticamente. Acelera traducao 3-5x.

MT (Machine Translation) - rascunho inicial. Crowdin/Locize integra com Google Translate / DeepL / Claude API pra gerar rascunho de traducao. Tradutor revisa (mais rapido que traduzir do zero).

i18n em React Native / mobile. Mesmo conceito, mas bundle size importa. Use Lingui (compila ICU estatico) ou i18next + namespaces lazy. Em Expo, use expo-localization pra detectar locale do device.

i18next-browser-languagedetector - opcoes de deteccao.

import LanguageDetector from "i18next-browser-languagedetector";

i18n.use(LanguageDetector).init({
  detection: {
    order: [
      "querystring",    // ?lng=pt-BR
      "cookie",
      "localStorage",
      "sessionStorage",
      "navigator",      // navigator.language
      "htmlTag",        // <html lang="...">
    ],
    lookupQuerystring: "lng",
    lookupCookie: "i18next",
    lookupLocalStorage: "i18nextLng",
    caches: ["localStorage", "cookie"],
  },
});

Intl.Locale - normalizacao. Use pra normalizar locale string antes de comparar:

const userLocale = "pt_br";   // user digitou
const normalized = new Intl.Locale(userLocale).baseName;
// "pt-BR" (canonical)

Pra quem quer ir mais alem 🔴

ICU MessageFormat em Locize/Crowdin. SaaS moderno suporta ICU nativo. Tradutor ve preview de plural/select renderizado, nao texto cru. Recomendado pra plurals.

Translation memory export/import. Locize e Crowdin suportam TMX (Translation Memory eXchange) - exporta/importa memoria de traducao entre plataformas. Use pra migrar de Crowdin pra Locize (ou vice-versa) sem perder trabalho.

A11y + i18n. Screen readers leem conteudo em idioma detectado. Sem <html lang="...">, VoiceOver/NVDA pronuncia em ingles (sotaque errado). Sempre defina lang no <html> ou no elemento especifico.

A/B test de traducoes. Em Locize, da' pra fazer A/B de variacoes ("Salvar" vs "Gravar") e medir CTR no botao. Nao e' ciencia exata, mas em apps de e-commerce pode mudar conversion.

i18n audit - encontre strings hardcoded. Ferramenta i18n-unused encontra chaves definidas mas nao usadas no codigo. Inverso: eslint-plugin-i18next acha strings hardcoded.

# Install
pnpm add -D @intlify/eslint-plugin-i18next
// .eslintrc
{
  "plugins": ["@intlify/i18next"],
  "rules": {
    "i18next/no-literal-string": ["error", { "mode": "jsx-text-only" }]
  }
}

Leitura recomendada:

Dica: o erro mais comum em translation workflow e' gerenciar JSONs em Git com 5+ tradutores. Conflitos, PRs infinitas, falta de contexto, sem glossario. Use SaaS (Locize, Crowdin) desde o dia 1 se tiver mais que 2 idiomas e mais que 1 tradutor externo. Em apps pessoais/pequenos, Git + script de check + glossario no README funciona. Escolha baseado em tamanho do time de traducao, nao tamanho do app.

No proximo no, vamos pseudo-localization: tecnica de testar i18n sem traducoes reais - transforme "Hello" em "[Ĥéļļö世界]" e veja todos os bugs de i18n aparecerem: strings concatenadas, hardcoded, truncamento, layout quebra, RTL esquecido. Roda em CI, pega bugs antes do tradutor humano.

// Quiz

Por que usar um SaaS de translation management (Locize, Crowdin) em vez de gerenciar arquivos JSON direto em Git?

Escolha uma alternativa

// recursos

// avaliação da trilha

—
ainda sem avaliações