Internationalization (i18n) Cross-Surface Contract
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
- Implementar i18n em qualquer módulo / variante UI da Koder Stack
- Adicionar opção de troca de idioma em UI Koder
- Verificar se UI segue idioma do dispositivo
- Gerar testes de i18n via /k-test-gen-i18n
- Auditar cobertura de strings (hardcoded vs l10n) em qualquer módulo
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-idlogin/consent,koder-flowGitea 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].localesdo 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-US
→ en → 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 viaPlatform.localeName. v0.16.0+.engines/sdk/koder_web_kit(JS/Web) —KoderL10nJS module +<koder-language-picker>web component. Versão 0.3.0+.engines/sdk/go(Go services/CLIs) —koder.dev/sdk/go/l10npackage comL10n.T(ctx, key, args)que resolve viaAccept-Languageheader ou env vars per surface.engines/sdk/koder_chat,koder_player, etc. — usamkoder_kitoukoder_web_kitconforme 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 quandokoder.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-BRretorna 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:
- Adicionar
[i18n]block aokoder.toml. - Extrair strings via
/k-test-gen-i18n <módulo>(gera ARB/JSON stubs com en-US). - Adicionar
pt-BR(mínimo segundo locale). - Substituir literais por
KoderL10n.t('key'). - Adicionar
KoderLanguagePickerna Settings page. - Rodar T1-T6.
- 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
specs/koder-app/behaviors.kmdspecs/settings/patterns.kmdpolicies/language.kmdpolicies/reuse-first.kmd