Internationalization (i18n) Cross-Surface Contract

i18n specs/i18n/contract.kmd

Contrato cross-surface de internacionalização da Koder Stack: idioma padrão = locale do dispositivo, seletor de idioma em local canônico por surface (Flutter app, Web/landing, CLI, TUI, TV, Server-rendered HTML), persistência, fallback chain en-US, key-naming, validação por testes TDD. Estende `specs/koder-app/behaviors.kmd §9` e `policies/language.kmd` adicionando posicionamento de UI e cobertura cross-surface.

When this pattern applies

All triggers

Specification body

Spec — Internationalization (i18n) Cross-Surface Contract

Applicability

Toda variante de UI de qualquer módulo da Koder Stack — independente de surface (Flutter mobile/desktop, Web/landing, CLI, TUI, TV TizenOS/WebOS) e de domínio (products/, services/, engines/, infra/, meta/sites/). Inclui:

  • Apps Flutter (mobile, desktop)
  • Apps Web (Flutter Web, templ + HTMX, Next.js, React)
  • Landing pages (<slug>.koder.dev)
  • CLIs (Go cobra, Node CLI, Koda CLI)
  • TUIs (Bubble Tea)
  • Smart TV apps (TizenOS/WebOS, JS/React)
  • Páginas server-rendered (koder-id login/consent, koder-flow Gitea fork, etc.)

Não se aplica a:

  • Documentação técnica em meta/docs/ (sempre en-US, fonte única).
  • Código-fonte, identificadores, paths (sempre en-US — ver policies/language.kmd §File and Directory Naming).

Required Behavior

R1 — Default = device locale

Toda UI Koder DEVE detectar o locale do dispositivo / browser / ambiente do usuário no primeiro launch e exibir todo o texto user- facing nesse idioma se houver tradução; senão fallback en-US (ver R5). Nunca mostrar dialog "escolha seu idioma" no onboarding (per behaviors.kmd §9.4).

Detecção canônica por surface:

Surface Fonte do locale
Flutter (qualquer) Platform.localeName (dart:io) ou WidgetsBinding.instance.platformDispatcher.locale
Web (browser) navigator.language (primeiro elemento de navigator.languages)
Web (server-rendered) Accept-Language header (parseado, primeiro com q-weight maior)
CLI (Linux/macOS) $LC_ALL$LC_MESSAGES$LANG (primeiro não-vazio, antes de .)
CLI (Windows) GetUserDefaultLocaleName() ou $Env:UICulture (PowerShell)
TUI (Bubble Tea) mesmo que CLI
TV (TizenOS/WebOS) navigator.language (mesma API que Web)

R2 — Persistência da escolha do usuário

Se o usuário trocar idioma no Settings/picker, a escolha DEVE persistir e sobrepor o device locale em launches subsequentes. Fallback do device locale só vale enquanto a chave persistida for null.

Storage canônica por surface:

Surface Storage Chave
Flutter SharedPreferences (via KoderL10n.persist()) koder_locale
Web (browser) localStorage koder.locale
Web (server) Set-Cookie: koder_locale=<bcp47>; Path=/; Max-Age=31536000 cookie koder_locale
CLI/TUI ~/.config/koder/<slug>/locale (single-line BCP-47) filesystem
TV localStorage (TizenOS/WebOS expõem) koder.locale

R3 — Seletor de idioma (posicionamento canônico)

Toda UI Koder com mais de um locale registrado DEVE expor um seletor de idioma. Posição canônica por surface — opcional declarar custom em koder.toml [i18n].picker_position se houver justificativa:

Surface Posição canônica
Flutter (mobile/desktop/TV) Settings page → grupo "General" → tile KoderLanguagePicker. Acesso secundário: ícone globo (Icons.language) na app bar quando koder.toml [i18n].quick_picker = true (default false)
Web (app) mesma regra do Flutter — Settings page
Landing page Footer (canto inferior-direito), label do idioma atual + chevron, dropdown com lista
Server-rendered HTML Footer (mesmo do landing) OU dropdown discreto no header se for página de auth/consent (caso koder-id)
CLI Flag global --locale <bcp47> + subcomando <binary> config set locale <bcp47>
TUI Atalho Ctrl+Alt+L abre modal de troca; também via Settings se a TUI tiver

Regras adicionais:

  • Lista de opções DEVE conter os idiomas do koder.toml [i18n].locales do módulo, mais "System default" que volta ao locale do dispositivo e remove a chave persistida.
  • Cada opção exibe o nome do idioma na própria língua (autoglottonym): "English (US)" / "Português (Brasil)" / "Español" / "日本語" / "中文 (简体)".
  • Mudança DEVE ser instantânea — sem reload manual exigido.

R4 — Cobertura: zero strings hardcoded

Toda string user-facing DEVE estar em uma tabela de tradução. Strings inline em código de UI são proibidas exceto:

  • Identificadores técnicos não-traduzíveis (URLs canônicas, slugs, brand names, IDs de erro <APP>-<CAT>-<CODE>)
  • Strings de teste / debug (gated por kDebugMode)
  • Strings em meta/docs/ (que são em en-US por contrato)

Validação: ver §Tests.

R5 — Fallback chain

Resolução de uma chave: <full locale><language only>en-USen → fallback literal passada na chamada → key string. Implementado por KoderL10n.t() no koder_kit (Flutter) e KoderL10n.t() no koder_web_kit (Web).

R6 — Key naming convention

  • <feature>.<screen>.<element>.<state?> em snake_case (ex: auth.signin.button_label, auth.signin.error_invalid_credentials)
  • Pluralização ICU MessageFormat:
    "tabs.count": "{count, plural, =0 {No tabs} one {1 tab} other {# tabs}}"
    
  • Genderização e formatação de número/data via ICU também.
  • Keys em ARB (Flutter) ou JSON (Web/CLI/TUI/TV) sem prefixo de surface (a mesma key vale cross-surface se a string aparece em mais de um lugar).

R7 — Locales suportados (baseline)

Todo módulo Koder DEVE shippar baseline en-US. pt-BR é o segundo locale obrigatório se o módulo tem usuários no Brasil (todos os produtos atuais até 2026-05). Locales adicionais (es, ja, zh-CN, fr, de) são opcionais e shipam por demanda explícita.

Declarado em <module>/koder.toml:

[i18n]
locales = ["en-US", "pt-BR"]
strings_dir = "lib/l10n"           # default per surface (ver tabela)
picker_position = "settings"       # default; opcional override
quick_picker = false               # default; true habilita ícone globo na app bar

R8 — String files (per surface)

Surface Formato Path canônico
Flutter ARB (*.arb) lib/l10n/app_<locale>.arb (intl convention)
Web (Vue/React/JS) JSON src/locales/<locale>.json
Web (templ/HTMX/Go) Go map literal internal/l10n/<locale>.go (string consts)
CLI Go (cobra) Go map literal cmd/<binary>/l10n/<locale>.go
TUI Go (Bubble Tea) Go map literal internal/tui/l10n/<locale>.go
TV (JS/React) JSON src/locales/<locale>.json
Server HTML (templ) Go map literal internal/web/l10n/<locale>.go

SDK / Library Implementation

A implementação canônica do contrato vive nos SDKs:

  • engines/sdk/koder_kit (Flutter) — KoderL10n, KoderLanguagePicker, KoderApp(locale: null) autodetect via Platform.localeName. v0.16.0+.
  • engines/sdk/koder_web_kit (JS/Web) — KoderL10n JS module + <koder-language-picker> web component. Versão 0.3.0+.
  • engines/sdk/go (Go services/CLIs) — koder.dev/sdk/go/l10n package com L10n.T(ctx, key, args) que resolve via Accept-Language header ou env vars per surface.
  • engines/sdk/koder_chat, koder_player, etc. — usam koder_kit ou koder_web_kit conforme runtime.

CLIs/TUIs Koda (futuras) devem usar std.koder.l10n (não shipado).

Tests (TDD validation)

Cada módulo com UI Koder DEVE ter testes de i18n cobrindo:

  • T1 — Hardcoded-string analyzer: scan estático do código de UI buscando string literals fora de chamadas a KoderL10n.t() / equivalente da surface. Falha = strings hardcoded existem.
  • T2 — Locale resolution: instancia a UI em locales registrados
    • um locale não-suportado, verifica que cada chave renderiza no idioma esperado e que o não-suportado faz fallback em-US.
  • T3 — Switcher visibility: confirma que o KoderLanguagePicker (ou equivalente) aparece na posição canônica per R3 quando koder.toml [i18n].locales.length >= 2.
  • T4 — Persistence: troca o idioma via picker, restart simulado, confirma idioma preservado; remove a chave persistida, confirma device locale volta a valer.
  • T5 — No locale picker on onboarding (R1 + behaviors.kmd §9.4): cold start não exibe dialog de seleção.
  • T6 — Server Accept-Language honoring (apenas para surfaces server-rendered): GET com Accept-Language: pt-BR retorna HTML com strings pt-BR; sem header retorna en-US.

Templates de cada caso vivem em specs/i18n/test-template.kmd. Geração via /k-test-gen-i18n <módulo>.

Forbidden Patterns

Não mostrar dialog de seleção de idioma no onboarding.

Não hardcodar strings user-facing em widget/template code.

Não exigir reload manual após troca de idioma.

Não dispersar arquivos de tradução fora dos paths canônicos (R8).

Não usar key names com prefixo de surface (ex: flutter.auth.button_x) — usa o mesmo key cross-surface.

Não depender de comments / inline tags pra detectar idioma do usuário — sempre via API canônica per R1.

Migration

Módulos existentes que ainda têm strings hardcoded:

  1. Adicionar [i18n] block ao koder.toml.
  2. Extrair strings via /k-test-gen-i18n <módulo> (gera ARB/JSON stubs com en-US).
  3. Adicionar pt-BR (mínimo segundo locale).
  4. Substituir literais por KoderL10n.t('key').
  5. Adicionar KoderLanguagePicker na Settings page.
  6. Rodar T1-T6.
  7. Commit + abrir backlog ticket "i18n compliance" com numeração.

Seguir policies/reuse-first.kmd — sempre passar pelo SDK em vez de reimplementar resolução por módulo.

References