Skip to content

feat(i18n): traduzir ui/dialogs e ui/wizard (fase 3 de 3) - #463

Merged
MarkSant merged 21 commits into
mainfrom
claude/i18n-phase-3-handoff-40321d
Aug 15, 2026
Merged

feat(i18n): traduzir ui/dialogs e ui/wizard (fase 3 de 3)#463
MarkSant merged 21 commits into
mainfrom
claude/i18n-phase-3-handoff-40321d

Conversation

@MarkSant

Copy link
Copy Markdown
Owner

Fecha a migração de i18n. Com esta PR, i18n_scan.py src/zebtrack vai a
TOTAL: 0 e MIGRATED_PATHS passa a ser ("src/zebtrack",).

Cobre analysis/, core/, ui/gui.py, ui/ui_coordinator.py, ui/wizard/ e
ui/dialogs/ — 20 commits, ~1900 msgids. Fases 1 (#461) e 2 (#462) já no main.

A migração é um movimento, não um trabalho de tradução: o literal em
português vira o valor no _pairs/, então o diff do .po é o inverso exato do
diff do fonte.

Defeitos encontrados

Nenhum destes era o objetivo da tarefa; todos apareceram porque traduzir uma
string obriga a responder "quem mais depende deste texto?".

Código que ramificava por texto exibido ou por texto de exceção

  • detector_service._validate_range decidia entre "fora da faixa" e "não é
    um número" com if "deve estar entre" in str(e). Traduzir a mensagem teria
    reclassificado toda violação de faixa como erro de parsing, em silêncio. A
    checagem de faixa saiu do try, então o except só vê falha de conversão.
  • confirmation_step obtinha a identidade do template com
    .replace("Template carregado: ", "") sobre o texto renderizado; traduzir
    o prefixo o deixaria dentro do resumo. Nasceu
    format_template_banner_details().
  • model_selection_step anexava " ⭐ Recomendado" para exibir e retirava
    com _strip_annotation() / startswith. Traduzir só a exibição colaria o
    marcador no nome do peso e validate() recusaria um peso escolhido na própria
    lista. Passou a existir uma definição só, _recommended_suffix(), usada nos
    três pontos.
  • live_config_dialog exibia "Nenhuma câmera encontrada" e gravava a
    mesma string em camera_var, que apply() usa como chave em
    available_cameras. Virou _no_camera_label().

Texto de interface que o scanner nunca viu

O i18n_scan.py detecta português pelo acento. Isso deixou passar frases
inteiras dentro de pacotes que o ratchet já dava por prontos:

  • coordinators/ publicava "Aguardando sinal externo... (porta N)" como
    status de UI em dois arquivos, e o tooltip do gatilho externo retipava a mesma
    frase como prosa.
  • core/recording/live_session_manager empurrava quatro status
    ("Carregando detector...", "Iniciando em Ns...", "Iniciando captura...",
    "Gravando") para o mesmo rótulo onde já havia chamadas _() traduzidas.
    Na prática a linha de status trocava de idioma no meio da sessão.

Rótulos citados de memória

O guia pós-criação de projeto em project_workflow_service retipava seis
nomes de aba/botão como prosa e dois já estavam errados antes da migração
um deles nomeava um botão que não existe no código. Agora interpolam o msgid do
próprio widget.

Duplicação que a tradução transformaria em divergência

  • "Dia {n} - {grupo}" estava retipado em dezessete mensagens de
    block_detail_dialog; virou _block_label(). A chave persistida do mesmo
    bloco ("Dia_{n}_{grupo}") continua em português, de propósito.
  • custom_regex_dialog guardava cinco cópias de
    {"group": "Grupo", "day": "Dia", "subject": "Sujeito"}, que precisam
    concordar entre si na tela; viraram _field_labels().
  • Grafias duplicadas da mesma mensagem, unificadas num msgid só: duas redações
    da validação det/seg, pyserial não instalado/não disponível,
    Total de Vídeos/Total de vídeos, Porta Arduino:/Porta do Arduino:.
  • start_time_label renderizava "Início: Início: 12:34:56", repetindo no
    valor o rótulo da coluna vizinha. stop_time_label, ao lado, nunca fez isso.

_ usado como descarte

Sete ocorrências (name, _ = ..., rec, _ = ..., h, w, _ = frame.shape)
sombreavam o alias do gettext na função inteira — inclusive em chamadas
acima da atribuição. Todas foram pegas por ruff F823 ou por mypy;
nenhuma pela leitura do diff.

Tradução em tempo de import

Dicionários de rótulos em corpo de módulo/classe congelam o idioma no import:
_METHOD_LABELS, _METHOD_OPTIONS, _BADGE_*_TEXT e o MODE_DESCRIPTIONS
(typing.ClassVar). Todos viraram função.
tests/i18n/test_no_import_time_translation.py guarda isso.

Testes

  • Quatro asserções vácuas no wizard (or "Template" in ...,
    or "vazio" in ..., dois "indispon" not in ...) passariam mesmo se o aviso
    testado fosse exibido.
  • Quatro testes dirigiam widgets pelo rótulo em português
    (set("Detecção (det)")); com o rótulo traduzido paravam de resolver e o
    hint ficava vazio — o que fazia test_animal_method_hint_cleared_for_seg
    (que exige hint vazio) passar pelo motivo errado.
  • test_analysis_metadata_display não testava o código de produção: o
    state_synchronizer era um Mock() cujos side_effect reimplementavam os
    dois formatadores, copiando as f-strings que as asserções depois conferiam.
    Agora injeta um StateSynchronizer real; verificado por mutação (alterando o
    formatador de produção, os 4 testes falham — antes, passavam).
  • Uma colisão de msgid barrada em update_translations.py: --previous do
    Babel enrolava o msgid anterior no meio da string ao gerar o comentário #|,
    produzindo um .po que o polib recusa ler. --no-fuzzy-matching resolve.

O que NÃO foi traduzido, de propósito

Grupo_*/Dia_*/Sujeito_*, as chaves de session_duration_overrides, a aba
por_animal, as chaves do dict report e os cabeçalhos de coluna do .xlsx
são contratos de persistência. Traduzi-los não produz um app em inglês, produz
um app que não lê os projetos que ele mesmo gravou. Lista em
scripts/i18n_allowlist.txt.

⚠️ Ressalva ao critério de pronto

TOTAL: 0 prova apenas que não sobrou português ACENTUADO. O scanner — e
portanto o ratchet construído sobre ele — é cego a Salvar, Nenhum video,
Remover, dias, grupos. Foi exatamente assim que coordinators/ e
core/recording/ seguiram publicando texto em português depois de travados, e
foi só por leitura manual que apareceram.

Uma varredura dedicada a português sem acento continua pendente; não foi
feita nesta PR e não deve ser considerada coberta por ela.

Verificação

  • ruff check . limpo
  • mypy . limpo em 698 arquivos
  • pytest -q: 4197 passed
  • pytest -m gui -n0: 1079 passed
  • i18n_scan.py src/zebtrack --format=count: TOTAL: 0

🤖 Generated with Claude Code

MarkSant and others added 20 commits August 13, 2026 13:15
…, lote 1)

Primeiro lote da fase 3. `src/zebtrack/analysis` vai a zero literais e entra em
MIGRATED_PATHS -- `analysis/reporters/**` ja estava limpo desde a fase 1, entao
o pacote inteiro fecha de uma vez.

Dois defeitos achados de quebra, nenhum deles "so traduzir":

1. `data_transformer.rename_geotaxis_columns` gravava "Fundo"/"Meio"/"Superficie"
   como CABECALHO DE COLUNA do `_summary.xlsx`, no meio de um arquivo em que
   todo o resto (COLUMN_MAPPING, DISPLAY_COLUMN_MAPPING) ja e ingles. Eram as
   unicas colunas do sumario cujo nome dependia do idioma: a mesma analise saia
   com esquema diferente conforme a maquina, quebrando script de terceiro. Viram
   "Bottom"/"Middle"/"Surface" em ingles FIXO, sem `_()` -- rotulo de coluna de
   arquivo de dados nao pode variar com `ui.language`. Comentario no local
   registra o porque.

2. `visualization_generator` tinha uma unica legenda em portugues ("Area de
   processamento...") entre ~20 textos de grafico em ingles fixo (set_title,
   set_xlabel, ax.text). Mesmo tratamento: ingles fixo, sem `_()`.

O que e prosa de interface foi para o catalogo `zebtrack` com `_()`:
`roi.py` (12 avisos de degradacao do `seg_overlap` + o ValueError de
`bbox_intersects`) e `analysis_service.py` (aviso multi-animal, barra de status,
dialogos de erro/cancelamento/sucesso). Os avisos de `roi.py` sao conteudo de
relatorio: chegam ao leitor por degradation_warnings -> validation_warnings ->
apendice de validacao do `.docx`. Dominio `zebtrack`, nao `reporter`: so
`analysis/reporters/**` importa o `_` do reporter_context.

Cinco msgids que este lote precisava JA EXISTIAM com o portugues IDENTICO
("Starting processing for {count} videos...", "Analysis Error", "Cancelled",
"Success", "Ready.") -- reusados, omitidos de pr3-analysis.json. Escrever de
novo faria `update_translations.py` avisar "conflicting translations" e ignorar
o par em silencio.

Tres strings sem acento que o scanner NAO ve foram junto ("Cancelado",
"Sucesso", "Pronto." em analysis_service): confirmacao de que a contagem de 942
literais e piso, nao teto.

Asserts migrados no mesmo commit: test_seg_overlap_fallback (5),
test_roi_analyzer, test_visualization_generator, test_data_transformer_roi_metrics
e test_geotaxis_features (4 casos de nome de coluna).

Verde: ruff limpo, mypy limpo em 698 arquivos, 4209 fast (+1 = o novo caso
parametrizado do ratchet), 1079 gui.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e 3, lote 2)

Os dois arquivos soltos em `src/zebtrack/ui/` vao a zero e entram no ratchet.

O achado do lote: DEZ dos dezesseis msgids necessarios JA EXISTIAM com o
portugues identico. `gui.py` mantem copias proprias de dialogos e de defaults de
estado que os componentes migrados na fase 2 ja possuem -- "No Project",
"Open a project before adjusting the specific calibration.", "Failed to save
camera", "Could not save the camera as the project default:", "Form reloaded",
"Values restored to reflect the current settings." (dialog_manager),
"No analysis in progress.", "No task in progress.", "Analysis configuration:
{value}" (analysis_controls) e "Error" (pr1). Todos reusados; `pr3-ui-shell.json`
so carrega os 6 genuinamente novos. A duplicacao em si nao foi mexida -- e
refactor, nao traducao -- mas fica registrada aqui.

Outros pontos:

- `gui.py:805` ja estava em INGLES e sem `_()` ("This project is not configured
  for live experimental tracking."): aparecia em ingles para o usuario pt_BR sem
  ninguem notar, porque o scanner so procura acento. Agora traduzido de fato.
- `gui.py:865` passava `reason="Atualizacao manual"` ao debouncer do overview.
  `reason=` e tag de proveniencia, nunca renderizada -- virou "Manual refresh"
  em ingles FIXO, sem `_()`, como todo outro `reason=` da camada de UI
  ("zones_concluded", "Template Applied", "TabBuilder initialization").
- `ui_coordinator.py:873` montava "sessao(oes)" para fugir do plural. Virou dois
  msgids simples escolhidos por `session_count == 1`, o padrao do projeto
  (format_status_label): os arquivos de pares nao carregam formas plurais e
  ngettext nao e usado em lugar nenhum.
- Tres literais sem acento que o scanner nao ve entraram junto ("Ocorreu um erro
  desconhecido.", "Nenhuma tarefa em andamento.", "Nenhum Projeto"), achados por
  varredura lexica dos dois arquivos.

Verde: ruff limpo, mypy limpo em 698 arquivos, 4211 fast (+2 = os dois novos
casos parametrizados do ratchet), 1079 gui.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`core/recording` a zero e no ratchet: 53 literais em 5 arquivos, o fluxo ao vivo
inteiro (selecao de modo por hardware, abertura de camera, status de atraso da
analise, mensagens de conclusao).

DOIS `_` COMO DESCARTE sombreavam o gettext -- o gotcha nº 1 do guia, de novo:

  live_session_manager.py:459      success, _ = initialize_detector(...)
  frame_processing_pipeline.py:693 detections, _ = detector.detect(...)

Ambos ligam `_` como local, o que sombreia o alias do gettext na FUNCAO INTEIRA,
inclusive nas chamadas ACIMA do ponto de atribuicao. Pegos por ruff F823 e por
mypy (`Name "_" is used before definition`), NENHUM por leitura do diff.
Renomeados para `_detector` / `_annotated` com comentario no local.

Defeitos de texto corrigidos junto:

- `live_camera_mode.recommend_mode` montava a recomendacao com `reason +=` sobre
  meia oracao ("...mas N foram solicitados. " + "Recomendado: ..."). Um tradutor
  nao consegue reordenar meia frase. Virou DUAS oracoes completas, cada uma seu
  msgid.
- "aquario(s)" e "frame(s) descartado(s)" eram fuga do plural. Viraram pares
  singular/plural escolhidos por comparacao (`max_supported == 1`,
  `_dropped_frames_video == 1`), o padrao do projeto -- sem ngettext, que os
  arquivos de pares nao suportam.
- `get_mode_description` monta o dicionario DENTRO da funcao, entao pode levar
  `_()` direto; comentario registra por que nao pode subir para o corpo do
  modulo. O docstring dizia "Portuguese description" -- agora "Description in
  the active interface language".
- `LiveCameraModeRecommendation.__str__` NAO foi traduzido: nenhum call site o
  renderiza, e a primeira linha imprime `recommended_mode.value`, valor de enum
  persistido. Passou a ingles fixo, com o motivo no docstring.

`reason == "no_detections"` (live_analysis_post_processor) e sentinela comparada,
nao texto exibido -- intocada.

Asserts migrados: test_live_camera_phase5_hygiene ("verifique" -> "check the
disk"), test_live_camera_workflow_e2e ("HARDWARE INSUFICIENTE" ->
"INSUFFICIENT HARDWARE").

Verde: ruff limpo, mypy limpo em 698 arquivos, 4212 fast, 1079 gui.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`core/project` a zero e no ratchet: 72 literais acentuados em 7 arquivos, mais
uma duzia sem acento que o scanner nao ve.

ACHADO PRINCIPAL -- o guia pos-criacao de projeto apontava para botoes que nao
existem. `project_workflow_service._generate_post_creation_guide` instrui o
pesquisador citando nomes de aba e de botao ENTRE ASPAS, retipados a mao. As
fases 1 e 2 traduziram esses widgets; o guia nao acompanhou. Dois dos seis ja
estavam errados ANTES desta migracao:

  - "aba 'Relatorios'"  -> a aba se chama "Processamento e Relatorios"
  - "Clique em 'Adicionar e Processar Novos Videos'" -> esse rotulo nao existe
    em lugar nenhum do codigo; o botao e "Processar Videos Pendentes..."

Agora cada rotulo citado e interpolado a partir do msgid DO PROPRIO WIDGET
(`_("   - Open the '{tab}' tab").format(tab=_("Main Control"))`), entao o guia
nao pode mais divergir em silencio. Os seis msgids ja existiam das fases 1-2 e
foram reusados.

UM `_` COMO DESCARTE de novo, o terceiro da fase: `asset_manager.py:434`
(`_, entry = self._resolve_roi_template_entry(...)`). Desta vez o ruff NAO
pegou -- so o mypy, com `"int" not callable` / `"None" not callable`. Aproveitei
para eliminar os outros seis descartes latentes de `core/project`
(zone_manager, project_manager): hoje estao em funcoes sem `_()`, mas sao
armadilhas armadas para quem adicionar a primeira string traduzida.

Outros defeitos:

- `can_remove_asset` montava "Nao ha {label} registrada para este video." com o
  substantivo interpolado. O portugues concorda em genero com a palavra
  encaixada ("registrada"), coisa que nenhuma substituicao garante em traducao.
  Virou uma frase COMPLETA por asset.
- `project_lifecycle_manager` gravava `analysis_profiles[0].name = "Padrao"` no
  project.json, enquanto o factory canonico
  `AssetManager._default_analysis_profile()` ja gravava `"default"`. Dois nomes
  para o mesmo perfil embutido. Alinhado ao canonico e mantido em INGLES: e
  valor PERSISTIDO, e um nome que mudasse com `ui.language` daria projetos
  diferentes por maquina. `"active_weight": _("Default")` (project_workflow_
  service) e o oposto -- valor de EXIBICAO empurrado ao StateManager -- e leva
  `_()`.
- Guardas que nomeiam parametros de codigo (`num_aquariums deve ser <= 100`,
  `save_location invalido`, `Asset type '{asset}' desconhecido`) e as
  `Field(description=...)` do Pydantic em `schemas.py` (avaliadas no corpo da
  classe, onde `_()` e proibido) passaram a INGLES FIXO, sem `_()`: sao
  documentacao de esquema e guardas de desenvolvedor, nao copy de interface.

Asserts migrados em 7 arquivos de teste. Um foi DELIBERADAMENTE mantido em
portugues: `test_detection_enforcement.py:140` casa contra
`core/viewmodels/analysis_control_view_model.py`, que e do lote 3d. Esse arquivo
tem uma SEGUNDA redacao da mesma validacao det/seg -- quando 3d migra-lo, deve
reusar o msgid deste lote em vez de criar outro.

Verde: ruff limpo, mypy limpo em 698 arquivos, 4213 fast, 1079 gui.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…face

`_exempt_docstring` isentava apenas `body[0]` -- a docstring de modulo, classe
ou funcao. Uma docstring de ATRIBUTO (PEP 258: o paragrafo logo abaixo de um
membro de enum ou de um atributo de classe) e um `ast.Expr` de string como
qualquer outro, so que nao e o primeiro do corpo, entao passava direto e era
reportada como literal por traduzir.

Custava tres falsos positivos em `core/services/external_trigger_gate.py`, onde
cada membro do `ExternalTriggerDecision` documenta em portugues por que aquela
decisao existe -- prosa que o projeto decidiu MANTER (regra 10 do guia). Sem
esse conserto, fechar `core/services` exigiria ou traduzir documentacao ou
inflar o allowlist com texto que nao e contrato nenhum.

A regra nova e mais geral e mais barata de justificar: uma string que e um
EXPRESSION-STATEMENT e avaliada e descartada -- ela nao tem como chegar a um
widget. A unica razao para escrever uma e documentar.

Total do pacote cai de 786 para 783 literais sem tocar em uma linha de
`src/zebtrack`. Os 60 testes de `tests/i18n` seguem verdes, ou seja, a isencao
nao escondeu nenhuma string real nos pacotes ja migrados.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`core/services` a zero e no ratchet: 63 literais em 7 arquivos.

ACHADO PRINCIPAL -- ramificacao pelo TEXTO da excecao, o antipadrao que o guia
proibe, a um passo de ser detonado por esta propria migracao.
`detector_service._validate_range` fazia:

    try:
        val = float(value)
        if val < min_val or val > max_val:
            raise ValueError(f"{param} deve estar entre {min} e {max}, ...")
    except (TypeError, ValueError) as e:
        if isinstance(e, ValueError) and "deve estar entre" in str(e):
            raise
        raise ValueError(f"{param} deve ser um numero valido") from e

O `if` existia so para distinguir "fora da faixa" de "nao e numero", porque as
duas coisas eram levantadas de dentro do MESMO `try`. Traduzir a mensagem da
faixa faria o `in str(e)` falhar e o erro seria reetiquetado como "nao e um
numero" -- ramo ERRADO, sem excecao nenhuma para denunciar. A correcao nao e
traduzir com cuidado: e tirar o range check de dentro do `try`, o que elimina a
necessidade de distinguir. Comentario no local registra o porque.

Outros pontos:

- `arduino_ack_semantics.describe_inversion` encaixava "entrada"/"saida" DUAS
  vezes na mesma frase, com um "uma {edge_pt} deveria {expected}" que so
  funciona porque os dois substantivos sao femininos em portugues. Virou duas
  frases completas escolhidas pelo `edge` -- que e sentinela de codigo, nao
  texto exibido, entao a ramificacao continua legitima.
- `ArduinoBinding.describe` idem, em versao mais simples.
- Mais dois `_` como descarte em `weight_manager` (`_, current_default =` e um
  `for _, filename, *_` numa generator expression). O quarto e o quinto da fase.
- Os `metadata["description"]` gravados no `metadata.json` do modelo convertido
  passaram a INGLES FIXO, sem `_()`: sao valores escritos em disco, e uma
  descricao que mudasse com `ui.language` daria arquivos diferentes por maquina
  para o mesmo modelo. Mesma logica separa `details["last_conversion_error"]`
  (GRAVADO, ingles) do `OpenVINOExportError` (LEVANTADO ao operador, traduzido)
  -- a mesma frase, dois papeis distintos.
- As guardas de parametro do detector (`conf_threshold`, `track_buffer`) ficaram
  em ingles sem `_()`: nomeiam parametros de codigo e seus erros sobem pelo
  EventBus, que os engole antes de qualquer dialogo.

Asserts migrados em 8 arquivos. QUATRO deles so quebram sob `-m gui`
(test_calibration_step) -- a suite rapida passou verde com o bug presente, que e
exatamente por que o `-m gui -n0` e obrigatorio antes do push.

Verde: ruff limpo, mypy limpo em 698 arquivos, 4214 fast, 1079 gui.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`core/detection`, `core/video` e `core/viewmodels` a zero. Com isso o pacote
`core` inteiro esta migrado e as CINCO entradas que o ratchet acumulava
(app_runner.py, application_bootstrapper.py, project, recording, services)
colapsam em `src/zebtrack/core` -- qualquer arquivo novo sob `core/` passa a
nascer coberto.

DUAS REDACOES DA MESMA VALIDACAO viraram uma. `analysis_control_view_model`
tinha a sua propria versao da checagem det/seg que `project_workflow_service`
ja fazia, com texto diferente:

  pws:  "O modo de deteccao (det) para animais so e compativel com 1 animal
         por aquario. Configuracao atual: N animais... altere o metodo ...
         para 'seg' (segmentacao) nas configuracoes."
  acvm: "O modo de deteccao (det) suporta apenas 1 animal por aquario.
         Voce configurou N animais... use o modo de segmentacao (seg)."

Mesma regra, mesma correcao a sugerir, conselho redigido de dois jeitos
conforme o caminho que disparasse. Agora as duas compartilham UM msgid (o do
lote 3b) -- e o assert que eu tinha deixado propositalmente em portugues no
`test_detection_enforcement` pode finalmente casar com o ingles.

`_SLOT_LABELS` deixou de ser `ClassVar` e virou `_slot_labels()`. Era um dos
sitios de traducao em tempo de import listados no handoff: um dict de chamadas
a `_()` no corpo da classe e avaliado no import, ANTES de `i18n.install()`, e
congelaria os rotulos no idioma-fonte independentemente de `ui.language`.

`progress_notifier`: o plano era migrar o produtor e depois simplificar o
consumidor (`state_synchronizer`) para so `"step:"`. A apuracao mostrou que o
plano estava errado. NENHUM produtor poe prefixo no campo `step` -- os valores
vem do `live_camera_session_coordinator` ja traduzidos e nus, e o `"Etapa:"` do
`progress_notifier` vai para a MENSAGEM do SET_STATUS, outro campo. A tira-
prefixo e defensiva contra um produtor que nao existe, e o unico teste que a
cobre (`test_analysis_metadata_display`) REIMPLEMENTA a funcao em vez de
chama-la, entao passaria igual se o codigo real sumisse. Simplificar para so
`"step:"` seria pior ainda: em pt_BR nada tem prefixo nenhum. Deixei o
comportamento tolerante e corrigi o comentario, que afirmava o contrario.

`"Erro de Processamento"` (4 sitios) foi unificado com o `"Erro no
Processamento"` ja existente: um titulo de dialogo, um msgid. A variacao
de/no era ruido do fonte.

Guardas de `aquarium_detector` e `multi_aquarium_detector` ("Only 2 aquariums
are supported", "ByteTracker not initialised for aquarium N") ficaram em ingles
sem `_()`: nomeiam estado interno e contrato de API. O limite de 2 aquarios que
o OPERADOR ve continua vindo do wizard, esse sim traduzido.

Verde: ruff limpo, mypy limpo em 698 arquivos, 4210 fast, 1079 gui.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lote 4a)

Primeiro lote do `ui/wizard`. Tres arquivos a zero; os outros dez seguem.

`models.py` NAO foi traduzido: passou a INGLES FIXO, sem `_()`. E a camada de
esquema Pydantic, e a decisao segue a mesma de `core/project/schemas.py`:

  - `Field(description=...)` e avaliado no CORPO DA CLASSE, onde `_()` e
    proibido (congelaria no idioma-fonte no import);
  - o arquivo JA era misto -- os campos mais novos (`roi_inclusion_rule`,
    `behavioral_analysis`) ja tinham descricao em ingles. Traduzir metade e
    deixar metade era o estado anterior, nao um objetivo;
  - as mensagens dos validators nomeiam campos e so aparecem como
    `ValidationError` cru. O texto que o operador realmente le vem do
    `validate()` de cada step -- esse sim traduzido -- e da
    `wizard_service.validate_*`, migrada no lote 3c.

Cuidado deliberado com os valores de enum (`experimental`, `live`, `skip`,
`import_zones`, `partial`, `full`, `replace`, `merge`, `manual`): sao
persistidos no projeto e ficaram intocados. So rotulo exibido foi tocado.

`wizard_dialog.py`: os quatro botoes de navegacao e o titulo do messagebox de
validacao. `"Cancelar"` reusou o msgid `"Cancel"` da fase 1.

`experimental_design_step.py`: rotulos, os dois textos longos (tooltip de
duracao e caixa "Como isso sera usado?") e o resumo com contagem. Sete literais
acentuados mais oito sem acento que o scanner nao ve ("dias", "grupos",
"animais/grupo", "Animais por Grupo:", "Nomes dos Grupos", ...).

Verde: ruff limpo, mypy limpo em 698 arquivos, 4210 fast, 1079 gui.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
O guia descrevia o mecanismo (`_()`, dominios, `.format()`) mas nenhuma das
regras que a fase 3 descobriu na pratica. Elas existiam so em mensagens de
commit, o que nao ajuda quem abre o arquivo. Acrescentadas:

- **`_` e o alias do gettext, nunca descarte.** Ligar `_` em qualquer ponto
  sombreia o alias na FUNCAO INTEIRA, inclusive acima da atribuicao. Sete
  ocorrencias na migracao, todas pegas por ruff/mypy e nenhuma por leitura.
- **Nao ramifique por texto exibido nem por texto de excecao** -- agora com o
  caso real do `detector_service`, onde a distincao "fora da faixa" vs "nao e
  numero" saia de um `in str(e)`, e com o conserto certo (tirar o range check
  de dentro do `try` em vez de traduzir com cuidado).
- **Rotulo citado em texto de ajuda apodrece**: interpole o msgid do proprio
  widget. O guia pos-criacao tinha seis rotulos retipados, dois ja errados.
- **Reuso de msgid**: par conflitante e DESCARTADO com um aviso facil de perder;
  e reuso alto e sinal de duplicacao (10 de 16 msgids de `gui.py` ja existiam).
- **Regra geral do que nao traduzir**: gravado ou comparado => ingles fixo;
  renderizado => `_()`. Com as categorias novas (cabecalho de coluna de xlsx,
  metadata.json, `Field(description=...)` do Pydantic, tags de proveniencia) e
  o caso em que a MESMA frase tem os dois papeis.
- **Plural sem ngettext** e **um msgid por frase completa**, com o motivo da
  concordancia de genero que substituicao nenhuma garante.

A secao "The migration" deixou de descrever o plano e passou a descrever o
processo: o checklist por lote, o colapso das entradas do ratchet quando um
pacote fecha, e a ressalva de que a contagem do scanner e PISO -- ele so acha
acento, e cada lote rendeu de um terco a metade a mais de portugues sem acento.

Nenhuma mudanca de codigo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
104 literais acentuados saem de ui/wizard (665 -> 561). Quatro defeitos
apareceram no caminho, nenhum deles visivel no scan.

1. ConfirmationStep obtinha a identidade do template chamando
   .replace('Template carregado: ', '') sobre o texto JA RENDERIZADO do
   banner. Traduzir o prefixo faria o replace parar de casar em silencio e
   o resumo passaria a exibir "Template carregado: X" inteiro dentro de uma
   linha que ja diz o que e. templates.py ganhou
   format_template_banner_details(), que devolve so a identidade;
   format_template_banner() passa a ser o prefixo traduzido em volta dela.

2. coordinators/ ja esta em MIGRATED_PATHS e mesmo assim publicava
   "Aguardando sinal externo... (porta N)" como UI_SET_STATUS em dois
   arquivos. A frase nao tem acento, e o scanner so detecta acento -- logo
   o ratchet construido sobre ele nunca a viu. E a confirmacao pratica de
   que a contagem do scanner e piso, nao teto. O tooltip do gatilho externo
   retipava essa mesma frase como prosa; agora interpola o msgid, entao as
   duas nao podem mais divergir.

3. O `--previous` do Babel enrolava o msgid anterior no meio de uma string
   ao escrever o comentario `#|`, gerando um .po que o polib recusa
   ("unknown keyword Recommended:") e travando todo o pipeline. Corrigido
   com --no-fuzzy-matching: o i18n_pairs.py ja descartava explicitamente
   todo palpite fuzzy do Babel, entao a adivinhacao so produzia msgstr
   errado, flag fuzzy (que o CI reprova) e, aqui, corrupcao.

4. Quatro assercoes de teste eram vacuas e passariam com o codigo de
   producao quebrado: `or "Template" in banner_text`, `or "vazio" in ...`,
   e dois `"indispon" not in ...` que, uma vez traduzido o aviso, passariam
   ate se o aviso fosse exibido. Todas reancoradas no texto real.

Tambem unificados dois pares de grafias divergentes da mesma mensagem:
"pyserial nao instalado"/"nao disponivel" e "Total de Videos"/"Total de
videos" (este ultimo reusando o msgid ja existente em core/project).

Verificacao: ruff limpo, mypy limpo em 698 arquivos, 4210 fast, 1079 gui.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
69 literais acentuados saem de ui/wizard (561 -> 97 na pasta; wizard fica so
com os seis arquivos do lote 4d). Os dois arquivos tinham um dict de rotulos
em CORPO DE MODULO (_METHOD_LABELS, _METHOD_OPTIONS): traduzir no lugar
resolveria o catalogo no import e congelaria o idioma, entao viraram funcao.

Tres defeitos estruturais em model_selection_step, nenhum visivel no scan:

1. "  ⭐ Recomendado" era anexado ao peso recomendado PARA EXIBIR e retirado
   de volta por _strip_annotation() e pelo startswith de revalidacao. Traduzir
   so o lado da exibicao deixaria o marcador colado no nome do peso, e entao
   validate() recusaria um peso que o usuario acabou de escolher na propria
   lista -- com a mensagem "selecione um peso valido". Agora existe uma
   definicao unica, _recommended_suffix(), usada nos tres pontos.

2. _refresh_weight_dropdowns fazia `rec, _ = ...` duas linhas acima de onde o
   marcador precisa chamar _(). O gettext viraria o segundo elemento da tupla
   e seria chamado como funcao (TypeError), ou ficaria nao-atribuido quando
   nao ha perspectiva (UnboundLocalError). Mais tres `name, _ =` em
   _default_weight_for_method, renomeados pelo mesmo motivo.

3. O erro de faixa montava f"❌ {label.capitalize()} deve estar entre 0 e 1",
   capitalizando um substantivo traduzido. Virou uma frase pronta por campo,
   que e o que o guia pede e tambem resolve a concordancia.

Nos testes, quatro sitios DIRIGIAM o widget com o rotulo em portugues
(set("Detecção (det)")). Com o rotulo traduzido eles deixam de resolver,
_method_key_from_label devolve a string crua e o hint fica vazio -- o que
fazia test_animal_method_hint_cleared_for_seg, que exige justamente hint
vazio, passar pelo motivo errado. Passam a usar _method_display().

Verificacao: ruff limpo, mypy limpo em 698 arquivos, 4210 fast, 1079 gui.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Os seis arquivos restantes de ui/wizard: discovery_step, custom_regex_dialog,
file_selection_step, import_config_step, calibration_step e
design_editor_dialog. O pacote ui/wizard vai a ZERO literais acentuados
(561 -> 395 no pacote todo; o que sobra e ui/dialogs).

Tres achados estruturais:

1. custom_regex_dialog guardava CINCO copias do mesmo
   {"group": "Grupo", "day": "Dia", "subject": "Sujeito"}: o cabecalho da
   arvore, a linha de resultado por campo, o placeholder "aguardando", o erro
   de validacao e a pre-visualizacao. Cada copia precisaria do seu proprio
   _(), e elas TEM de concordar entre si na tela (o mesmo rotulo aparece nos
   cinco lugares). Colapsaram em _field_labels().

2. file_selection_step montava o resumo colando "N arquivo(s)" + " + " +
   " selecionado(s)". O participio final tem de concordar com um sujeito que
   pode ser masculino (arquivo), feminino (pasta) ou os dois ao mesmo tempo --
   e era por isso que carregava o "(s)". O portugues agora comeca por rotulo
   ("Seleção: 1 arquivo + 2 pastas"), entao nada precisa concordar, e as
   contagens usam os pares singular/plural que ja existiam.

3. import_config_step e confirmation_step descrevem as mesmas estrategias de
   ROI com portugues diferente ("Merge (manter ambos)" no radio,
   "Mesclar (manter ambos)" no resumo; "Full (do zero)" vs
   "Full (processar do zero)"). Mantidos msgids distintos em vez de colidir --
   colidir faria update_translations descartar um dos pares em silencio.

Nos testes, "1 arquivo(s)" virou "1 file": a assercao agora confere a forma
singular de verdade, que era justamente o que o "(s)" escondia.

Junto vao cinco anotacoes `str` -> `Path | str` em custom_regex_dialog e
file_selection_step. Sao violacoes PRE-EXISTENTES da regra do proprio repo: o
hook check-path-consistency so inspeciona arquivos tocados pelo commit, e este
lote e a primeira mudanca a alcancar esses arquivos. Os corpos sao orientados a
string (re.search, fatiamento, os.path.basename), entao cada um normaliza com
str(path) no topo -- a anotacao alarga, o comportamento nao muda.

Verificacao: ruff limpo, mypy limpo em 698 arquivos, 4210 fast, 1079 gui.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
color_selection_dialog, center_periphery_dialog, diagnostic_progress_dialog,
model_diagnostics_dialog, subject_selection_dialog, pending_videos_dialog e
preview_polygon_dialog. 395 -> 356 literais acentuados.

Dois defeitos estruturais:

1. Em color_selection_dialog o NOME da cor fazia quatro trabalhos ao mesmo
   tempo: rotulo do radio, VALOR do radio (via name.lower()), chave comparada
   no apply() e result["name"]. Traduzir o nome deixaria
   StringVar(value="verde") sem casar com nada -- nenhuma cor viria
   pre-selecionada, e apply() so acharia a cor depois de o usuario clicar.
   A tabela agora carrega uma chave estavel ao lado do rotulo traduzido.
   E seguro traduzir o rotulo porque o que se persiste e result["rgb"];
   result["name"] so e interpolado numa mensagem de status. Os seis nomes de
   cor reusam os msgids ja criados para ui/components/canvas/zone_editor.py,
   que mapeia exatamente as mesmas seis tuplas BGR.

2. preview_polygon_dialog tinha _BADGE_AUTO_TEXT/_BADGE_MANUAL_TEXT em corpo
   de MODULO: traduzir no lugar congelaria o cracha no idioma vigente no
   import. Viraram funcoes. As duas constantes _BADGE_*_BG continuam
   constantes -- cor nao e idioma.

subject_selection_dialog montava f"Cobaia {n}: {'Concluído' if ... }": os dois
particípios concordam com o substantivo, entao viraram uma frase pronta cada.

O TAG_STYLES (ClassVar) de pending_videos_dialog estava anotado no handoff
como sitio de traducao em tempo de import, mas contem so cores hexadecimais --
nao ha nada a traduzir ali.

Nos testes, os casos de cor fixavam justamente o acoplamento que este lote
desfaz: procuravam a cor pelo nome em portugues e dirigiam o widget com essa
mesma string como valor do radio. Passam a usar a chave estavel para busca e
selecao, e a conferir o rotulo em ingles -- deixam de depender do idioma.

Verificacao: ruff limpo, mypy limpo em 698 arquivos, 4210 fast, 1079 gui.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
O texto do commit do 5a diz "395 -> 356"; o numero correto e 375 (o lote
removeu 20 literais, nao 39). O codigo esta certo, so a contagem no texto
estava errada -- registrada aqui em vez de reescrever um commit ja empurrado.

Tambem quebra o restante do lote 5 em sub-lotes com as contagens atuais, e
carimba o unico sitio de traducao em tempo de import que resta em ui/dialogs:
MODE_DESCRIPTIONS, um typing.ClassVar em corpo de classe de
live_camera_mode_selection_dialog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… 5b)

Os dois maiores arquivos de ui/dialogs. 375 -> 237 literais acentuados.

Tres achados:

1. "Dia {n} - {grupo}" estava retipado dentro de DEZESSETE mensagens
   diferentes deste dialogo (titulo, progresso, avisos, cabecalho do .docx,
   confirmacao do lote...). Virou um unico _block_label(). E texto de
   EXIBICAO: a chave persistida do mesmo bloco e montada em separado como
   "Dia_{n}_{grupo}" em add_note e continua em portugues, porque e chave de
   dicionario dentro de project_data, nao copy.

2. Dois `_` usados como descarte dentro de _write_partial_report_word ficavam
   ABAIXO da chamada _() que monta o cabecalho do relatorio. O gettext virava
   variavel local nao atribuida naquele ponto; ruff F823 pegou. Renomeados.

3. Conflito de msgid detectado antes de gravar: "Detection Error" e
   "Detection failed" ja existiam em pr2-coordinators-tail.json com portugues
   um pouco diferente ("Erro na Detecção", "Detecção falhou"). Gravar um
   segundo par faria o update_translations.py descartar um dos dois em
   silencio -- os dois call sites reusam o msgid existente.

O trio "Sem Sessões"/"Sem Relatórios"/aviso de duracoes aparece identico em
generate_partial_report e mark_batch_complete (o proprio comentario do codigo
diz "mesmos textos"), entao os dois call sites continuam compartilhando um
msgid cada.

block_detail_dialog tambem escreve um .docx parcial. Esses cabecalhos ficam no
dominio zebtrack, e nao no reporter, porque o babel extrai por DIRETORIO
(--ignore-dirs reporters) e este arquivo nao esta sob analysis/reporters:
importar o _() do reporter aqui extrairia para zebtrack.pot mas procuraria no
catalogo reporter, ou seja, cairia sempre no ingles.

Os ValueError levantados em live_analysis_dialog.validate() foram embrulhados
porque sao interpolados direto numa messagebox, nao apenas registrados no log.

Verificacao: ruff limpo, mypy limpo em 698 arquivos, 4210 fast, 1079 gui.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
create_project_dialog, single_video_config_dialog e
project_video_import_dialog. 237 -> 132 literais acentuados.

Quatro msgids foram DESCARTADOS deste lote depois que a checagem de corpus
apontou colisao -- gravar um par conflitante faz o update_translations.py
derrubar um dos dois em silencio:

  'Number of Groups:', 'Group Names' e 'Validation' ja existiam identicos em
  pr3-wizard-shell.json;
  'Metadata' ja existia em pr2-components-tail.json como "Metadados". O
  cabecalho da arvore reusa o msgid existente e agora concorda com o resto
  do app, em vez de mostrar "Metadata" so nesta tela.

Os rotulos de coluna "Grupo"/"Dia"/"Sujeito" do project_video_import_dialog
reusam os msgids no singular criados no 4d (custom_regex_dialog), e o titulo
"Validação" -- que aparece em QUATORZE call sites -- ficou com um msgid so.

O resumo de selecao do create_project_dialog usava a mesma construcao
"N arquivo(s) + N pasta(s) selecionado(s)" do file_selection_step, entao
reusa os pares singular/plural e o involucro "Selected: {parts}" do 4d --
o participio final nao precisa mais concordar com nada.

"Grupo {n}:" no create_project_dialog e rotulo puro: group_name_vars nascem
vazias e o operador digita o nome real, entao nada persistido depende dele.

Verificacao: ruff limpo, mypy limpo em 698 arquivos, 4210 fast, 1079 gui.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
live_camera_mode_selection_dialog, aquarium_detection_progress_dialog,
calibration_dialog, start_recording_dialog, aquarium_assignment_dialog e
multi_aquarium_live_preview_window. 132 -> 53 literais acentuados.

MODE_DESCRIPTIONS era um typing.ClassVar em CORPO DE CLASSE -- o ultimo
sitio de traducao em tempo de import que restava em ui/dialogs. Um dict
literal em corpo de classe e avaliado no import, entao envolver aquelas
quatro descricoes em _() ali dentro congelaria o idioma no momento em que o
modulo fosse importado, antes de i18n.install() rodar. Virou o staticmethod
_mode_descriptions(). O nome curto do RECORD_ONLY reusa o msgid
"Recording Only" criado no 4b, onde o live_config_step cita exatamente esse
modo pelo nome no aviso de hardware insuficiente -- os dois nao podem mais
divergir.

start_recording_dialog duplica o seletor de camera inteiro do
block_detail_dialog, palavra por palavra. Todas essas strings reusam os
msgids do 5b; so tres entradas deste arquivo sao novas.

Os ValueError de aquarium_assignment_dialog._collect_configs foram
embrulhados porque o validate() os renderiza com
messagebox.showerror(..., str(e)) -- nao sao mensagens so de log.

Verificacao: ruff limpo, mypy limpo em 698 arquivos, 4210 fast, 1079 gui.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fecha ui/dialogs. O scanner sai de 53 literais acentuados em 8 arquivos para
zero; o total real migrado foi de 86 msgids novos, porque metade do portugues
desses arquivos nao tem acento e o scanner e cego a isso.

Defeitos encontrados:

1. core/recording/live_session_manager.py ja estava DENTRO do ratchet e mesmo
   assim empurrava quatro status em portugues para o rotulo do LivePreviewWindow
   -- "Carregando detector...", "Iniciando em Ns...", "Iniciando captura..." e
   "Gravando". Nenhuma tem acento, entao nem o scanner nem o ratchet construido
   sobre ele jamais as viram. Ficavam literalmente ao lado de chamadas _() ja
   traduzidas no MESMO rotulo, de forma que a linha de status trocava de idioma
   no meio da sessao.

2. LivePreviewWindow.start_timer escrevia "Inicio: HH:MM:SS" no rotulo de VALOR,
   que ja tem "Inicio:" como rotulo da coluna a esquerda -- a linha renderizava
   "Inicio:  Inicio: 12:34:56". stop_time_label, ao lado, nunca fez isso. O
   valor agora traz so o relogio, igual ao par dele.

3. ui/wizard chegou a zero no lote 4d mas nunca entrou em MIGRATED_PATHS:
   passou quatro lotes sem guarda. Entrou agora, junto com ui/dialogs.

4. "Nenhuma camera encontrada" em live_config_dialog era EXIBIDA e tambem
   gravada em camera_var, que apply() usa como chave de busca em
   available_cameras. Traduzir um sitio e nao o outro quebraria a busca em
   silencio. Virou _no_camera_label(), definicao unica para os tres sitios.
   "Camera {index}" (descricao padrao) estava duplicada em _detect_devices e
   _refresh_cameras; ambas passam a reusar o msgid criado no lote 4b.

5. "Porta Arduino:" era uma segunda grafia do "Porta do Arduino:" que
   live_config_step ja possuia -- mesmo campo, dois rotulos. Colapsou no msgid
   "Arduino Port:". Ja "Configuracao de Aquarios" NAO foi colapsado no
   "Aquarium Configuration" de aquarium_assignment_dialog: sao dialogos
   distintos, entao virou "Aquarium Setup".

6. A mensagem de erro do Arduino citava a opcao 'Usar Arduino' retipada como
   prosa; agora interpola o msgid do proprio checkbox.

7. save_roi_template_dialog usava `lambda *_:` num trace_add -- o padrao de `_`
   como descarte que ja quebrou o gettext tres vezes nesta migracao. Renomeado
   por precaucao, mesmo sem _() dentro do lambda.

8. camera_disconnect_recovery_dialog repetia a legenda da contagem regressiva
   em dois sitios com a mesma interpolacao; virou _countdown_text().

Em zone_reuse_dialog, "• Metodo: {auto-detectado|desenhado manualmente}" virou
uma frase completa por caso, em vez de um fragmento interpolado que o portugues
nao consegue concordar.

O hook Check Path | str acusou uma violacao PRE-EXISTENTE em
multi_aquarium_confirm_dialog.__init__ (video_path: str | None); a anotacao foi
alargada para Path | str | None e normalizada no topo da funcao.

Testes: seis asseracoes ancoradas no texto em portugues foram migradas (a suite
roda com ZEBTRACK_LANGUAGE=en). Duas delas -- "Tempo:" e "nenhuma porta serial"
-- so apareceram no `pytest -m gui -n0`, que nao roda na suite rapida.

Verificacao: ruff check limpo, mypy limpo em 698 arquivos, 4212 passed na suite
rapida, 1079 passed na de GUI, i18n_scan em TOTAL: 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MIGRATED_PATHS colapsa nas 17 entradas anteriores para ("src/zebtrack",), de
forma que qualquer arquivo novo do pacote ja nasce coberto. O comentario
"Migrado na PR1" saiu do topo do modulo.

No lugar dele entrou a ressalva que importa: verde nesse teste significa "sem
portugues ACENTUADO", nao "sem portugues". O i18n_scan detecta portugues pelo
acento, entao "Salvar", "Nenhum video", "Remover" e "dias" atravessam o scanner
e o ratchet sem serem vistos. Nao e teorico -- o lote 5e achou quatro strings
desse tipo dentro de core/recording/, pacote listado no ratchet desde o lote 3a,
e o lote 4b achou outras duas em coordinators/. A mesma ressalva foi registrada
no guia, no CHANGELOG e no ROLLING_TASK_LOG; a varredura dedicada a portugues
sem acento continua PENDENTE e nao foi feita aqui.

O CHANGELOG ganhou a secao das fases 2 e 3, que ate agora so tinha a fase 1.
Ela lista os defeitos que a traducao tornou visiveis em vez de so dizer o que
foi traduzido: rotulos citados de memoria em texto de ajuda (dois ja errados
ANTES da migracao, um deles nomeando um botao inexistente), ramificacao pelo
texto de uma excecao, o marcador "⭐ Recomendado" que era anexado para exibir e
retirado por _strip_annotation(), a identidade de template obtida por .replace()
sobre texto renderizado, cabecalhos de .xlsx em portugues no meio de arquivos
ingleses, e as grafias duplicadas da mesma mensagem que foram unificadas.

TASK-068 marcada como Completed, com 5b/5c/5d/5e preenchidos -- os tres
primeiros tinham ficado com o checkbox desmarcado embora os commits ja
tivessem entrado.

Verificacao: 44 passed em tests/i18n, i18n_scan em TOTAL: 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
As quatro asseracoes deste arquivo nao testavam o codigo de producao. O
`state_synchronizer` era um `Mock()` cujos `side_effect` REIMPLEMENTAVAM os dois
formatadores -- `_apply_analysis_metadata_strings` e
`update_analysis_task_status` -- copiando as mesmas f-strings que as asseracoes
depois conferiam. O teste comparava o stub com a expectativa escrita a partir do
proprio stub: passaria com os metodos de producao deletados, renomeados ou com
qualquer formato diferente.

Agora o fixture injeta um `StateSynchronizer(instance)` de verdade. O
`validation_manager` continua stub de proposito -- transformar metadados crus em
strings de exibicao e unidade dele, coberta em test_validation_manager.py -- mas
seus fallbacks passam a vir de `no_group_label()` / `no_day_label()` /
`not_reported_label()`, em vez de repetir "No Group"/"No Day"/"Not reported" a
mao, que era mais uma copia capaz de divergir em silencio.

Verificado por mutacao: trocando o texto dos dois formatadores em
state_synchronizer.py, os 4 testes FALHAM (antes desta mudanca, passavam).

Verificacao: ruff limpo, mypy limpo em 698 arquivos, 4197 passed na suite rapida
(4212 - 16 entradas de MIGRATED_PATHS + 1, apos o colapso do ratchet).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 15, 2026 01:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Finalizes the phase 3/3 i18n migration by moving remaining UI (wizard + dialogs) and core/analysis-facing strings to English msgids (with pt_BR in _pairs / catalog), plus tightening scanner/ratchet coverage and updating tests to assert the new msgids.

Changes:

  • Migrates ui/wizard/ and ui/dialogs/ UI copy to gettext msgids and refactors a few string-dependent flows to avoid translation breakage.
  • Propagates i18n updates across analysis/, core/, and runtime/coordinator status strings, aligning user-visible messaging.
  • Updates i18n tooling (i18n_scan.py, update_translations.py), ratchet scope, and tests to match the new i18n baseline.

Reviewed changes

Copilot reviewed 140 out of 143 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
CHANGELOG.md Documents the completed i18n migration and known scanner limitation (unaccented PT).
scripts/i18n_scan.py Exempts bare string expression-statements (attribute docstrings) from untranslated-literal scanning.
scripts/update_translations.py Adds --no-fuzzy-matching to avoid Babel fuzzy/previous-msgid parse hazards.
src/zebtrack/analysis/analysis_service.py Localizes analysis warnings/status strings and normalizes UI-facing messages.
src/zebtrack/analysis/data_transformer.py Fixes geotaxis column headers to stable English schema (not language-dependent).
src/zebtrack/analysis/roi.py Moves ROI analyzer errors/degradation warnings to msgids.
src/zebtrack/analysis/visualization_generator.py Replaces a remaining Portuguese plot label with English.
src/zebtrack/coordinators/live_camera_session_coordinator.py Localizes external-trigger wait status message.
src/zebtrack/coordinators/recording_session_coordinator.py Localizes external-trigger wait status message.
src/zebtrack/core/detection/aquarium_detector.py Standardizes internal contract error message to English.
src/zebtrack/core/detection/multi_aquarium_detector.py Standardizes runtime errors to English.
src/zebtrack/core/project/project_manager.py Localizes project-manager user-facing errors and avoids _ shadowing.
src/zebtrack/core/project/project_workflow_service.py Localizes validation and guide text; interpolates widget msgids instead of retyping labels.
src/zebtrack/core/project/roi_template_manager.py Localizes template validation/load errors; clarifies developer-facing guards.
src/zebtrack/core/project/schemas.py Converts schema descriptions/messages to English (avoids import-time _() in class metadata).
src/zebtrack/core/project/zone_manager.py Localizes user-facing ValueError and avoids _ shadowing.
src/zebtrack/core/recording/frame_processing_pipeline.py Localizes aquarium-detection status and avoids _ shadowing in tuple unpacking.
src/zebtrack/core/recording/live_analysis_post_processor.py Localizes completion dialogs and file lists.
src/zebtrack/core/recording/live_session_manager.py Localizes live-session status updates; avoids _ shadowing; adds plural-safe strings.
src/zebtrack/core/recording/recording_service.py Localizes recording start failure messages.
src/zebtrack/core/services/arduino_ack_semantics.py Localizes inversion descriptions without fragile string splicing.
src/zebtrack/core/services/arduino_bindings.py Localizes token-conflict description string.
src/zebtrack/core/services/detector_service.py Localizes several detector init errors and refactors range validation; updates track_buffer validation.
src/zebtrack/core/services/model_override_service.py Localizes warnings/status for applying global settings and project overrides.
src/zebtrack/core/services/model_service.py Localizes OpenVINO status text and model-not-found errors.
src/zebtrack/core/video/analysis_pipeline_runner.py Localizes processing error titles/messages.
src/zebtrack/core/video/processing_mode.py Localizes display names for processing modes.
src/zebtrack/core/video/progress_notifier.py Localizes progress/status messages.
src/zebtrack/core/video/tracking_session_runner.py Localizes “trajectory generated” status update.
src/zebtrack/core/video/video_context_factory.py Localizes processing error titles/messages and details.
src/zebtrack/core/viewmodels/analysis_control_view_model.py Localizes error/status strings and deduplicates det/seg validation msgid.
src/zebtrack/core/viewmodels/hardware_status_view_model.py Avoids import-time translation by generating slot-label dict at call time.
src/zebtrack/core/viewmodels/main_view_model_runtime.py Localizes critical camera-thread shutdown error payload.
src/zebtrack/locales/_pairs/pr3-analysis.json Adds Portuguese pairs for analysis-domain msgids added in this phase.
src/zebtrack/locales/_pairs/pr3-core-recording.json Adds Portuguese pairs for core/recording msgids added in this phase.
src/zebtrack/locales/_pairs/pr3-core-tail.json Adds Portuguese pairs for remaining core/viewmodel msgids.
src/zebtrack/locales/_pairs/pr3-dialogs-5a.json Adds Portuguese pairs for dialog msgids added in this phase.
src/zebtrack/locales/_pairs/pr3-ui-shell.json Adds Portuguese pairs for ui shell msgids added in this phase.
src/zebtrack/locales/_pairs/pr3-wizard-shell.json Adds Portuguese pairs for wizard shell msgids added in this phase.
src/zebtrack/ui/components/state_synchronizer.py Keeps step-prefix tolerance but avoids relying on displayed text for branching.
src/zebtrack/ui/dialogs/aquarium_assignment_dialog.py Localizes dialog title/frames and some warnings; adds validation errors.
src/zebtrack/ui/dialogs/aquarium_detection_progress_dialog.py Localizes detection progress dialog strings and thumbnail labels.
src/zebtrack/ui/dialogs/calibration_dialog.py Localizes calibration dialog titles/tabs and copy-to-project flows.
src/zebtrack/ui/dialogs/camera_disconnect_recovery_dialog.py Localizes disconnect copy; centralizes countdown text to avoid drift.
src/zebtrack/ui/dialogs/center_periphery_dialog.py Localizes method/value labels.
src/zebtrack/ui/dialogs/color_selection_dialog.py Separates stable color keys from translated labels; updates apply() result schema.
src/zebtrack/ui/dialogs/diagnostic_progress_dialog.py Localizes default title/status strings while avoiding import-time translation.
src/zebtrack/ui/dialogs/live_config_dialog.py Localizes live config dialog strings; centralizes the “no camera” placeholder to avoid lookup drift.
src/zebtrack/ui/dialogs/missing_metadata_dialog.py Localizes dialog copy and validation errors.
src/zebtrack/ui/dialogs/model_diagnostics_dialog.py Localizes dialog title.
src/zebtrack/ui/dialogs/multi_aquarium_confirm_dialog.py Localizes copy and normalizes video_path to Path for safer handling/logging.
src/zebtrack/ui/dialogs/multi_aquarium_live_preview_window.py Localizes window title and several captions; updates timer label behavior.
src/zebtrack/ui/dialogs/pending_videos_dialog.py Localizes dialog title/copy; adds singular/plural variant strings.
src/zebtrack/ui/dialogs/preview_polygon_dialog.py Avoids import-time translations by replacing badge constants with helper functions; localizes UI copy.
src/zebtrack/ui/dialogs/save_roi_template_dialog.py Localizes dialog copy; avoids _ shadowing in trace callback; localizes file dialog labels.
src/zebtrack/ui/dialogs/start_recording_dialog.py Localizes dialog titles/labels and camera chooser UI.
src/zebtrack/ui/dialogs/subject_selection_dialog.py Localizes per-subject completion labels without fragile string splicing.
src/zebtrack/ui/dialogs/zone_calibration_dialog.py Localizes zone calibration selection dialog copy/buttons.
src/zebtrack/ui/dialogs/zone_reuse_dialog.py Localizes zone-reuse confirmation copy and status details.
src/zebtrack/ui/gui.py Localizes fallback error strings, analysis-status defaults, and several dialog titles/messages.
src/zebtrack/ui/ui_coordinator.py Localizes runtime status updates; adds plural-safe batch report status message.
src/zebtrack/ui/wizard/experimental_design_step.py Migrates wizard step UI copy to gettext with formatted msgids.
src/zebtrack/ui/wizard/templates.py Introduces format_template_banner_details() to avoid parsing translated banner text.
src/zebtrack/ui/wizard/wizard_dialog.py Localizes navigation buttons and validation dialog title.
tests/analysis/test_data_transformer_roi_metrics.py Updates assertions for localized warning text.
tests/analysis/test_roi_analyzer.py Updates assertions for localized error message text.
tests/analysis/test_seg_overlap_fallback.py Updates assertions for localized degradation warnings.
tests/analysis/test_visualization_generator.py Updates assertions for updated plot label text.
tests/core/services/test_wizard_service_camera_friendly_names.py Updates camera description assertions to English msgids.
tests/core/test_analysis_control_view_model.py Updates assertions for localized titles/status messages.
tests/core/test_arduino_ack_semantics.py Updates inversion description assertions to new English wording.
tests/core/test_asset_manager_basic.py Updates assertions for localized error messages.
tests/core/test_detector_service.py Updates assertions for localized detector init error strings.
tests/core/test_live_camera_phase5_hygiene.py Updates assertions for localized disk-check hint text.
tests/core/test_processing_mode.py Updates assertions for ProcessingMode display names.
tests/core/test_project_workflow_service.py Updates assertions for localized validation and guide messages.
tests/core/test_roi_template_manager_validation.py Updates docstrings/regex matches for localized template errors.
tests/core/test_schemas.py Updates assertions for localized schema validation errors.
tests/core/test_weight_manager.py Updates assertion regex for localized “cannot delete last weight” error.
tests/core/test_wizard_service_multi_aquarium.py Updates assertions for localized multi-aquarium validation errors.
tests/i18n/test_no_untranslated_literals.py Expands ratchet scope to all of src/zebtrack.
tests/test_analysis_metadata_display.py Stops using a mock reimplementation; asserts real StateSynchronizer formatting.
tests/test_class_names_validation.py Updates assertion regex for localized model-not-found error.
tests/test_detection_enforcement.py Updates assertions for localized enforcement errors/titles.
tests/test_geotaxis_features.py Updates expected geotaxis zone display names to stable English.
tests/test_live_analysis_ui.py Updates assertions for localized live-analysis UI labels and fixes duplicated start-time caption expectation.
tests/test_live_camera_workflow_e2e.py Updates assertions for localized insufficient-hardware warning.
tests/test_project_manager.py Updates assertions for localized “reports” reason and “not found” project load error.
tests/ui/dialogs/test_block_detail_dialog.py Updates assertion for localized day label.
tests/ui/dialogs/test_calibration_dialog.py Updates assertions for localized titles/tab names.
tests/ui/dialogs/test_color_selection_dialog.py Updates tests for new stable color key + translated label result.
tests/ui/dialogs/test_dialogs_batch2.py Updates assertions for localized diagnostics progress dialog strings.
tests/ui/dialogs/test_live_camera_mode_selection_dialog.py Updates assertions for localized mode display names.
tests/ui/dialogs/test_live_config_dialog.py Updates assertions for localized live config validation errors.
tests/ui/dialogs/test_preview_polygon_dialog.py Updates tests to use badge helper functions rather than import-time constants.
tests/ui/dialogs/test_subject_selection_dialog.py Updates assertions for localized subject completion labels.
tests/ui/test_aquarium_dialogs.py Updates assertion for localized multi-aquarium confirm dialog title.
tests/ui/wizard/test_calibration_step.py Updates assertions for localized validation errors and template banner text.
tests/ui/wizard/test_live_config_step.py Updates assertions for localized “unavailable” warnings.
tests/ui/wizard/test_model_selection_step.py Stops driving widgets by Portuguese labels; uses production display helpers.
tests/ui/wizard/test_wizard_confirmation.py Updates assertions for localized summary/validation strings.
tests/ui/wizard/test_wizard_detection.py Updates assertions for localized status text.
tests/ui/wizard/test_wizard_file_selection.py Updates assertions for localized validation and summary strings.
tests/ui/wizard/test_wizard_import_config.py Updates assertions for localized “no video to configure” message.
tests/ui/wizard/test_wizard_integration.py Updates default project-name assertions to English msgids.
Suppressed comments (2)

src/zebtrack/ui/dialogs/multi_aquarium_live_preview_window.py:233

  • Several UI strings in the session footer remain hardcoded in Portuguese (e.g., "Fim:", "Frames gravados") and are not wrapped with _(), so they won't follow the selected UI language.
    src/zebtrack/ui/dialogs/aquarium_assignment_dialog.py:485
  • Labels for group/subject are still hardcoded in Portuguese ("Grupo:", "Sujeito:") and bypass gettext, so this dialog will not fully localize with the selected UI language.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 508 to +512
try:
val = float(value)
if val < min_val or val > max_val:
raise ValueError(
f"{param_name} deve estar entre {min_val} e {max_val}, recebido {val}"
)
except (TypeError, ValueError) as e:
if isinstance(e, ValueError) and "deve estar entre" in str(e):
raise
raise ValueError(f"{param_name} deve ser um número válido") from e
raise ValueError(f"{param_name} must be a valid number") from e
# The range check lives OUTSIDE the try on purpose. It used to sit
Comment on lines 530 to +535
if "track_buffer" in params_dict:
try:
if int(params_dict["track_buffer"]) < 1:
raise ValueError("track_buffer deve ser pelo menos 1")
raise ValueError("track_buffer must be at least 1")
except (TypeError, ValueError) as e:
raise ValueError("track_buffer deve ser um número inteiro") from e
raise ValueError("track_buffer must be an integer") from e
…ro de tipo

Apontado pelo revisor na PR #463. E o mesmo defeito que o lote 3c removeu de
_validate_range, deixado intacto no bloco vizinho, dez linhas abaixo:

    try:
        if int(params_dict["track_buffer"]) < 1:
            raise ValueError("track_buffer must be at least 1")
    except (TypeError, ValueError) as e:
        raise ValueError("track_buffer must be an integer") from e

O `raise` da faixa esta DENTRO do try, entao o proprio `except ValueError`
abaixo o captura e o re-levanta como erro de tipo. Passar `track_buffer=0` --
um inteiro -- era respondido com "track_buffer must be an integer". A conversao
agora fica sozinha no try e a checagem de faixa vem depois, igual ao
_validate_range; a mensagem passa a citar o valor recebido.

O que NAO foi mudado: o limite. O revisor sugeriu alinhar em `>= 0` com o
DetectorSetupCoordinator, mas ha TRES limites divergentes para o mesmo
parametro e o coordinator nao e a referencia canonica:

  settings.py            Field(300, ge=10, le=1000)   <- contrato de esquema
  DetectorSetupCoordinator  track_buffer < 0          <- aceita 0
  DetectorService           track_buffer < 1          <- rejeita 0

Escolher entre eles muda o que o app aceita e e decisao de dominio, nao de
mensagem de erro; fica registrado para ser resolvido em separado. Esta mudanca
preserva exatamente o conjunto de valores aceitos e corrige so a mensagem.

O outro comentario do revisor -- embrulhar estes ValueError em _() -- foi
verificado e NAO se aplica. As mensagens sao embrulhadas em
DetectorSetupCoordinatorError, que nao e capturado em NENHUM ponto de src/:
model_diagnostics_panel captura ValidationError do pydantic e
ui_state_coordinator.apply_detector_thresholds (que captura ValueError) nao e
chamado por ninguem. Elas nunca chegam a um showerror; terminam em log. Pela
regra do proprio guia, texto de excecao que so alcanca log fica em ingles puro.

Teste de regressao distingue os dois erros e foi verificado por mutacao:
restaurando a forma antiga, os dois casos de faixa falham.

Verificacao: ruff limpo, mypy limpo em 698 arquivos, 4202 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@MarkSant
MarkSant merged commit 262e3a4 into main Aug 15, 2026
6 checks passed
@MarkSant
MarkSant deleted the claude/i18n-phase-3-handoff-40321d branch August 15, 2026 04:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants