Respostas de erro multilíngues (pt-BR + en + es) na API pública
TLDR: Centralizar toda mensagem user-facing da API em arquivos de locale, adicionar as traduções de inglês e espanhol que faltavam, e resolver o idioma da resposta a partir do header
Accept-Languagepara a API pública responder no idioma do chamador.
Contexto
As mensagens de erro user-facing eram inconsistentes:
- A configuração de i18n já existia (
config/application.rb):default_locale = :"pt-BR",available_locales = ["pt-BR", :en],fallbacks = true. ApplicationController#set_localesó liaparams[:locale]— ignorava o headerAccept-Language, então o chamador não conseguia receber inglês de fato.- As mensagens viviam em três padrões concorrentes:
- via
I18n.t— sóAuth::Login(v2); - inglês hardcoded — API v1 (
memberships,apolo_membership,user_profiles),admin_greenfield,current_user_apolo; - português hardcoded — services e
webhook_email_controller.
- via
- Não existia tradução em inglês:
en.ymltinha apenashello. As chaveserrors.messages.*existiam só empt-BR.yml, então?locale=encaía silenciosamente em pt-BR. O fluxo bilíngue não funcionava. - Havia um typo em mensagem hardcoded:
"Token de autentição inválido"(webhook_email_controller.rb:38).
Objetivos
- Resolver o idioma da resposta a partir do header
Accept-Language, mantendoparams[:locale]como override explícito e pt-BR como fallback. - Mover toda mensagem de erro/sucesso user-facing da API pública (v1 + v2) para arquivos de locale.
- Fornecer tradução completa em
pt-BR,eneespara todas as chaves migradas, incluindo mensagens de validação/atributo do ActiveRecord expostas ao usuário. - Preservar os shapes de resposta existentes (
{ error: <string> },{ message: <string> },{ errors: [<string>] }) para não quebrar o frontend — só a origem do texto e o locale mudam. - Substituir strings hardcoded por chamadas inline de
I18n.t(...), sem helper/concern — os shapes divergem entre endpoints, então um helper compartilhado não agrega.
Fora de escopo
- Mensagens dos services do Admin/ActiveAdmin (
admin_*_service.rb) — UI interna de admin. - Mudar o shape do JSON de erro ou introduzir códigos de erro — evitado deliberadamente para manter o contrato do frontend estável.
Decisões de design
- Resolução de locale:
around_action :switch_localeemApplicationControllerenvolvendo o request emI18n.with_locale, para o locale thread-local nunca vazar entre requests. Precedência:params[:locale]→ melhor match doAccept-Language→I18n.default_locale. Só valores emI18n.available_localessão aceitos. O parsing deAccept-Languagerespeita os pesosqe é feito à mão emApplicationController, sem gem nova. - Namespace de chaves: aninhar sob
api.errors.<domínio>.<chave>(ex.:api.errors.common.not_found,api.errors.membership.not_found,api.errors.apolo.missing_token,api.errors.auth.invalid_credentials). As chaves de auth emerrors.messages.*foram movidas para esse namespace. - Shape da resposta: inalterado. Cada controller mantém o shape atual e só troca o literal por
I18n.t("api.errors.<chave>").
Mudanças
Config / infra
app/controllers/application_controller.rb—before_action :set_localeviraaround_action :switch_locale; resolução de locale a partir deAccept-Language.config/application.rb—esadicionado aI18n.available_locales.
Controllers — trocar strings hardcoded por chaves de i18n
application_controller.rb—"Not permitted".admin_greenfield_controller.rb—"Not logged","Not authorized".concerns/current_user_apolo.rb—"user not authorized or access expired".api/v1/user_profiles_controller.rb—"Not found".api/v1/memberships_controller.rb—"Not found".api/v1/apolo_membership_controller.rb—"Membership does not exist","Not found","Missing APOLO Access Token","Invalid APOLO Access Token".api/v1/me_controller.rb—"Perfil atualizado com sucesso","Bad credentials".api/v1/webhook_email_controller.rb—"Campos mínimos não enviados: ...","Token de autentição inválido"(typo corrigido),"Usuário não encontrado","Já existe um usuário...".api/v1/webhook_controller.rb—"Campos mínimos enviados: ...","No membership found for ...","Token de autentição inválido"(typo corrigido).app/models/auth/login.rb— repontar chaves deerrors.messages.*paraapi.errors.auth.*.app/models/onboarding/steps/process.rb—"Onboarding not found for user"e"Step '...' not found in onboarding"paraapi.errors.onboarding.*.
Arquivos de locale
config/locales/pt-BR.yml— namespaceapi.errors.*com todas as mensagens migradas.config/locales/en.yml— tradução completa deapi.*. As mensagens de validação do ActiveRecord (blank,taken, …) expostas porme_controller/onboardingsaem em inglês pelos defaults nativos do Rails e nomes de atributo são humanizados automaticamente, então não é preciso espelharactiverecord.config/locales/es.yml(novo) — tradução completa deapi.*. Validações do ActiveRecord/Devise vêm dos gemsrails-i18n/devise-i18n, já presentes.
Como verificar
- Teste de request em um endpoint de erro v1 e um v2, afirmando: request default → mensagem pt-BR;
Accept-Language: en→ mensagem em inglês;?locale=ensobrepõe o header; locale desconhecido/não suportado → cai em pt-BR. - Manual:
curl -H "Accept-Language: en" .../api/v1/memberships/0devolve a mensagem “not found” em inglês; sem o header devolve em pt-BR. grepconfirma que não sobrou string user-facing hardcoded nos arquivos de API pública tocados.- Suíte completa:
make test.
Documentação
- Respostas de erro multilíngues da API pública — resolução de locale, convenção de chaves
api.*e a exigência de tradução nos três idiomas.