ADR 0177 — O idioma das respostas mora em três lugares — conta, sessão e projeto —, em lista aberta de BCP-47 e nunca no users.locale
Status
Accepted. 2026-09-28 (AT-162, épico EP-028). Decisões do mantenedor de
2026-09-28, escritas nas notas da AT-168 e da AT-169: preferência por CONTA com
override por SESSÃO; lista ABERTA de idiomas (qualquer BCP-47); conta nova em
automatico; o detectado só vale CONFIRMADO; artefatos compartilhados e turnos
sem autor no idioma do PROJETO.
Referencia, sem editar, o ADR 0121 (tabela nova entra no arquivo do agregado dela) e o ADR 0006 (o prompt de sistema é do AGENTE, não do autor do turno — é por isso que o idioma não entra nele, e sim numa orientação por turno, que é da AT-164).
Context
O idioma da resposta de um agente, até aqui, era decidido só pelo modelo, a
partir de um prompt de sistema escrito em pt-BR e das mensagens. O relato que
abriu o épico foi uma conversa em português respondida em espanhol, com
DeepSeek via OpenRouter. users.locale (RN-432) existe, mas é o idioma da
INTERFACE: fechado a pt-BR/en porque cada valor precisa de arquivo de
tradução, e o engine não o lê em ponto nenhum.
As decisões do mantenedor pedem quatro fontes que não existiam e uma que existia, com uma precedência entre elas, e três escopos diferentes:
- a escolha explícita é da PESSOA e vale em todo projeto e sessão;
- a pessoa pode fixar OUTRO idioma numa sessão, só para ela — dois usuários na mesma sessão não se tocam;
- o detectado pelas mensagens é da PESSOA, global, e só conta depois de ela confirmar;
- o que é de todos (artefato compartilhado, turno sem autor) segue o PROJETO.
Decision
- Nenhum enum. Os três lugares guardam
textcom um código BCP-47 CANÔNICO, validado na api pornormalizarIdiomaBcp47(apps/api/src/domain/iam/idioma-de-resposta.ts): forma pelaIntl.getCanonicalLocales, subtag de idioma conhecida pelo ICU do processo (Intl.DisplayNamescomfallback: 'none'), nuncaundnem uso privado, no máximo 35 caracteres. Abrir o idioma da RESPOSTA não depende de arquivo de recurso nenhum; um enum obrigaria migration a cada idioma, que é o custo que a decisão "lista aberta" recusou. - Conta: duas colunas em
users, elocaleintocado.users.response_language—NULLé o AUTOMÁTICO, e é assim que toda conta nasce (sem valor-sentinela dentro de uma coluna de idiomas). E o parusers.detected_language+users.detected_language_confirmed_at, com um CHECK que exige os dois juntos: o detectado só existe CONFIRMADO. Quem grava o par é a AT-163 (a detecção e a pergunta de confirmação); a coluna nasce aqui porque é um degrau da precedência. - Sessão: tabela
session_language_overrides, no agregado de sessões, com chave{session_id, user_id}. Configuração e não evento: fixar de novo é upsert, soltar éDELETE, e a sessão apagada leva as linhas. A chave é o PAR porque o override é da pessoa; não há leitura "da sessão inteira". - Projeto: coluna
projects.language(implementada na AT-243, com o default e o papel mínimo decididos lá, na RN-619). É o idioma do que não tem um autor humano. - A precedência de quem tem autor (RN-618), resolvida por PESSOA e nunca
por sessão: override da sessão > escolha da conta > detectado confirmado >
users.locale. Comousers.localeéNOT NULL, a cadeia sempre termina num idioma. O "pedido pontual" dentro de uma mensagem não é degrau: quem o atende é o modelo, pela redação da orientação. - Um ponto de resolução.
ResolverIdiomaDaRespostaUseCasedevolve o vencedor, a ORIGEM e a cadeia inteira; é o que a tela mostra (a Conta, a barra da sessão) e o que a AT-164 chama para mandar o idioma ao engine.
Consequences
users.localecontinua fechado e continua sendo só da interface: nem a escolha das respostas nem a detecção escrevem nele. A página de Conta mostra os dois lado a lado, e a rotaPATCH /users/me/preferencesaceita os dois campos como OPCIONAIS e independentes.- A lista de idiomas aceitos é a do ICU do Node que roda a api. Um código ISO 639-3 raro que o ICU não nomeia é recusado, declarado — nenhum idioma de resposta plausível está nesse caso.
- Ainda não há consumidor no engine: nenhum turno muda de comportamento com este ADR. O transporte (a orientação efêmera por turno) é a AT-164; a detecção e a confirmação, a AT-163. Até lá, escolher um idioma na Conta é gravado e mostrado, e ainda não chega ao modelo — a tela não afirma o contrário.
- O override por sessão é permitido em sessão encerrada: ele não dispara turno nem escreve no event log.