diff --git a/CHANGELOG.md b/CHANGELOG.md index d5244cbf..94a0be59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,61 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Migração de idioma concluída (i18n — fases 2/3 e 3/3) + +- **A interface inteira passou para inglês como língua-fonte.** As fases 2 e 3 + cobriram `coordinators/`, `ui/components/`, `io/`, `plugins/`, `utils/`, + `ui/builders/`, `analysis/`, `core/`, `ui/gui.py`, `ui/ui_coordinator.py`, + `ui/wizard/` e `ui/dialogs/` — cerca de 1900 msgids no total. `i18n_scan.py` + está em zero e o ratchet + (`tests/i18n/test_no_untranslated_literals.py`) passou a cobrir `src/zebtrack` + inteiro, arquivos futuros incluídos. +- **A migração revelou defeitos que a tradução só tornou visíveis.** Os de maior + impacto: + - **Rótulos citados de memória em textos de ajuda.** O guia pós-criação de + projeto 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. + - **Ramificação pelo texto de uma exceção** em + `detector_service._validate_range`: traduzir a mensagem teria reclassificado + toda violação de faixa como "não é um número". A checagem de faixa saiu do + `try`. + - **Marcador "⭐ Recomendado" anexado para exibir e retirado por + `_strip_annotation()`** no wizard: traduzir só a exibição colaria o marcador + no nome do peso e `validate()` recusaria um peso escolhido na própria lista. + - **Identidade de template obtida por `.replace('Template carregado: ', '')` + sobre o texto renderizado** — traduzir o prefixo o deixaria dentro do resumo. + - **Status de UI em `coordinators/` e `core/recording/`** que o scanner nunca + viu por não ter acento: "Aguardando sinal externo... (porta N)" e quatro + strings do painel ao vivo, essas últimas ao lado de chamadas `_()` já + traduzidas no mesmo rótulo — a linha de status trocava de idioma no meio da + sessão. + - **Cabeçalhos de coluna do `.xlsx` e uma legenda de figura em português** no + meio de arquivos 100% ingleses. Cabeçalhos são esquema, não texto: agora são + inglês fixo, sem `_()`, para que o mesmo dado não gere planilhas de esquemas + diferentes conforme a máquina. + - **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:`. +- **Asserções de teste vácuas encontradas de passagem.** Quatro no wizard + (`or "Template" in ...`, dois `"indispon" not in ...`) passariam mesmo se o + aviso testado fosse exibido; e quatro testes dirigiam widgets pelo rótulo em + português, o que fazia `test_animal_method_hint_cleared_for_seg` passar pelo + motivo errado. Todas reancoradas. +- **Contratos de persistência não foram traduzidos**, e isso é deliberado: + `Grupo_*`/`Dia_*`/`Sujeito_*`, as chaves de `session_duration_overrides`, a aba + `por_animal` e as chaves do dict `report`. Traduzi-los não produziria um app em + inglês, e sim um app incapaz de ler os projetos que ele mesmo gravou. A lista + vive em `scripts/i18n_allowlist.txt`. + +#### Limitação conhecida + +- **O scanner só enxerga português ACENTUADO.** `Salvar`, `Nenhum video`, + `Remover`, `dias` e afins passam por ele e pelo ratchet sem serem vistos — + foi exatamente assim que os status de `coordinators/` e `core/recording/` + sobreviveram dentro de pacotes já travados. Uma varredura dedicada a português + sem acento continua pendente. + ### Interface em inglês por padrão, português selecionável (i18n — fase 1/3) - **O idioma agora é uma escolha do pesquisador, não do sistema operacional.** Na diff --git a/docs/guides/developer/i18n.md b/docs/guides/developer/i18n.md index e881bede..79a72503 100644 --- a/docs/guides/developer/i18n.md +++ b/docs/guides/developer/i18n.md @@ -38,6 +38,33 @@ _("Removed {count} videos").format(count=n) # correct _(f"Removed {n} videos") # never matches; do not do this ``` +One msgid per **complete sentence**. A sentence assembled from fragments cannot +be reordered by a translator, and a fragment with a noun spliced into it cannot +even be made to agree: Portuguese inflects for the spliced word (`Não há {X} +registrad**a**`, `uma {entrada/saída} deveria...`), which no substitution can +guarantee. Write one full sentence per case instead: + +```python +# Wrong: the adjective has to agree with whatever `label` holds. +_("There is no {label} recorded for this video.").format(label=label) + +# Right: one msgid per case. +{"arena": _("There is no arena recorded for this video."), + "rois": _("There are no ROIs recorded for this video.")}[asset] +``` + +## Plurals + +There is no `ngettext` in this project and the `_pairs` files carry no plural +forms. Use two plain msgids selected by a comparison: + +```python +_("Maximum supported: 1 aquarium.") if n == 1 else _("Maximum supported: {count} aquariums.").format(count=n) +``` + +A parenthetical like `aquário(s)` or `frame(s) descartado(s)` is a dodge around +the plural, not a style — replace it with the pair. + ## The two domains | Domain | Covers | Reached through | @@ -116,6 +143,35 @@ yesterday. They are listed in `scripts/i18n_allowlist.txt`: Where such a token is *displayed*, translate at the display boundary — render `_("Unassigned")` for a stored `Sem_Grupo` — and never change what is stored. +The allowlist covers the tokens that already exist. The general rule behind it: + +> **Written to disk or compared in code ⇒ plain English, no `_()`. +> Rendered to a person ⇒ `_()`.** + +The same sentence can have both roles and then needs both treatments. In +`core/services/weight_manager.py`, `details["last_conversion_error"]` is stored +in the weights config (English) while the `OpenVINOExportError` raised beside it +reaches the operator (translated). + +Categories that are *stored*, and therefore stay English even though they read +like copy: + +- **Spreadsheet column headers.** The geotaxis columns in + `analysis/data_transformer.py` used to emit `Fundo`/`Meio`/`Superfície` into + `_summary.xlsx` while every other header in the file was English. A header that + changed with `ui.language` would give the same analysis a different schema on + each machine. +- **`metadata.json` of a converted model** (`weight_manager`), and the built-in + analysis-profile name in `project.json` — which had drifted into two spellings, + `"default"` from the canonical factory and `"Padrão"` from a fallback. +- **`Field(description=...)` on Pydantic models**, plus their validator messages. + A description is evaluated in the class body, where `_()` is forbidden anyway + (see below); treat the whole schema layer as English. `ui/wizard/models.py` and + `core/project/schemas.py` follow this. +- **Provenance tags** never rendered — `reason="Manual refresh"`, + `reason="zones_concluded"` — and `__str__` implementations used for logs, + especially when they print a persisted `enum.value`. + ## Exception messages Exception text that only reaches logs and tracebacks stays in plain English and @@ -127,6 +183,80 @@ Never branch on the words inside an exception message. `except SomeError:` survives translation; `if "caminho não definido" in str(e)` does not, and it fails by silently taking the wrong branch. +Usually the branch exists because two different failures are raised from the +same `try`. Restructure so they cannot be confused, rather than matching prose. +`core/services/detector_service.py` had: + +```python +try: + val = float(value) + if val < min_val or val > max_val: + raise ValueError(f"{param} deve estar entre {min_val} e {max_val}") +except (TypeError, ValueError) as e: + if isinstance(e, ValueError) and "deve estar entre" in str(e): + raise # range error: re-raise as-is + raise ValueError(f"{param} deve ser um número válido") from e +``` + +Translating that message would have relabelled every range violation as "is not +a number". The fix was to move the range check **out** of the `try`, so the +`except` only ever sees a parse failure and no distinction is needed. + +## Never branch on displayed text either + +The same rule applies to labels. Anchor on the widget, the frame, or a real +constant — never on what the widget says. This has already broken in production: +`navigate_to_processing_reports_tab` looked its tab up by label, phase 1 +translated that label, and the tab simply stopped opening, with a `log.warning` +as the only symptom. + +Two related traps: + +- **A string that is both displayed and compared** must come from one function, + so the two sides can never drift (see `weight_inherit_label()` in + `ui/components/project_model_configuration_panel.py`). +- **Help text that quotes a button or tab name.** Retyping the label is how it + rots: the post-creation guide in `core/project/project_workflow_service.py` + quoted six of them, and two were already wrong before the migration — one named + a button that exists nowhere in the codebase. Interpolate the widget's own + msgid instead: + + ```python + _(" - Open the '{tab}' tab").format(tab=_("Main Control")) + ``` + +## `_` is the gettext alias, never a discard + +Binding `_` anywhere in a function shadows the alias for that **whole function**, +including calls that appear *above* the assignment: + +```python +success, _ = self.detector_service.initialize_detector(...) # breaks _() above +success, _detector = self.detector_service.initialize_detector(...) # correct +``` + +`for _ in range(3)`, `h, w, _ = frame.shape`, `for _, name, *_ in items` and +`lambda *_:` all do it. Seven of these turned up during the migration; every one +was caught by `ruff` (F823/F402) or `mypy` (`Name "_" is used before definition`, +`"int" not callable`) and none by reading the diff. Run both, not just `pytest`. + +## Reusing an existing msgid + +Before adding a pair, check whether the msgid already exists in +`src/zebtrack/locales/_pairs/*.json`: + +- **Same Portuguese** ⇒ reuse it and omit it from your batch file. +- **Different Portuguese** ⇒ pick a different, natural English msgid. + +Writing a conflicting pair makes `update_translations.py` print `conflicting +translations across batches` and **drop yours** — easy to miss in a long run. +Reuse is also a design signal: when `ui/gui.py` was migrated, 10 of its 16 +msgids already existed, which is how it became clear the file kept its own copies +of dialogs `dialog_manager` already owned. Prefer one msgid over two spellings of +the same message — `analysis_control_view_model.py` and +`project_workflow_service.py` had two different wordings of one validation, so +the advice the operator read depended on which path tripped it. + ## Adding a language 1. Add it to `SUPPORTED_LANGUAGES` in `src/zebtrack/i18n.py` and to the @@ -139,8 +269,10 @@ fails by silently taking the wrong branch. ## The migration -The UI is being converted from hardcoded Portuguese in stages. Two tools support -it: +The UI was converted from hardcoded Portuguese in stages; that work is done and +`i18n_scan.py` reports zero. What follows is kept because the same tooling and +the same traps apply to any future package, and because the ratchet's guarantee +is narrower than it looks. Two tools support it: - `scripts/i18n_scan.py` lists Portuguese literals that still need extracting (`--format=count` for a per-file tally). @@ -150,6 +282,34 @@ it: migration is a move, not a translation job — and a reviewer can check that the `.po` diff is the exact inverse of the source diff. -`tests/i18n/test_no_untranslated_literals.py` holds the list of packages already -migrated and fails if one of them gains a new Portuguese literal. Extend that -list in the same PR that migrates a package. +`tests/i18n/test_no_untranslated_literals.py` fails if a new Portuguese literal +appears. Its list is now just `("src/zebtrack",)`, so files added from here on +are covered the moment they exist. + +**The scanner's count is a floor, not a ceiling — and so is the ratchet's.** +Both detect Portuguese by its accented characters, so unaccented Portuguese — +`Salvar projeto`, `Nenhum video`, `Remover`, `dias`, `grupos` — is invisible to +them. In practice each batch turned up between a third and half again as many +literals as the scan reported. Worse, being listed in `MIGRATED_PATHS` proves +nothing about unaccented text: `coordinators/` was published as "done" while +still showing `Aguardando sinal externo... (porta N)`, and `core/recording/` +pushed four unaccented status strings into the live preview label for four +batches after being locked. Read the whole file, and grep for `text=`, `label=`, +`title=`, `show_*`, `set_status` and `add_command` to catch the rest. + +Per batch: + +1. Read the files; scan, then read for unaccented Portuguese too. +2. Decide stored-vs-rendered for each literal (see the rule above). +3. Check `_pairs/` for msgids you can reuse; write the rest to + `_pairs/.json` with the original Portuguese as the value. +4. `python scripts/update_translations.py --domain zebtrack` until it reports + `All catalogues are complete`. +5. Migrate the matching test assertions in the same commit, and add the path to + `MIGRATED_PATHS`. +6. `ruff check .`, `mypy .`, `pytest -q`, **and `pytest -m gui -n0`** — the GUI + marker does not run in the fast suite, and one batch had four failures that + only that run caught. + +Portuguese comments and docstrings in migrated files are deliberately left as +they are, so the `.po` diff stays the exact inverse of the source diff. diff --git a/docs/tasks/active/ROLLING_TASK_LOG.md b/docs/tasks/active/ROLLING_TASK_LOG.md index 9970832e..6b6ec37c 100644 --- a/docs/tasks/active/ROLLING_TASK_LOG.md +++ b/docs/tasks/active/ROLLING_TASK_LOG.md @@ -6,6 +6,173 @@ This document tracks all major agent interventions, technical debt resolutions, ## Active Tasks +### [2026-08-13] Internacionalização fase 3 de 3 — ui/dialogs, ui/wizard, core, analysis + +__ID:__ TASK-068 +__Agent:__ Claude Code (Opus 5) +__Status:__ Completed ✅ +__Branch:__ claude/i18n-phase-3-handoff-40321d +__Description:__ +Fase final da migração para inglês como idioma-fonte. Fases 1 (PR #461) e 2 +(PR #462) já mescladas; `coordinators/**`, `ui/components/**`, `io/`, `plugins/`, +`utils/` e `ui/builders/` estão a zero literais e travados pelo ratchet em +`tests/i18n/test_no_untranslated_literals.py`. + +Restam 942 literais acentuados em 74 arquivos (`ui/dialogs` 395, `ui/wizard` 300, +`core` 216, `analysis` 19, `ui/gui.py` + `ui/ui_coordinator.py` 12). O volume real +é maior: o scanner só detecta acento, e o português sem acento somou de um terço a +metade a mais por arquivo nas fases anteriores. + +__Critério de pronto:__ `i18n_scan.py src/zebtrack` em `TOTAL: 0`, +`MIGRATED_PATHS == ("src/zebtrack",)` e remoção do comentário "Migrado na PR1" +no topo do ratchet; depois, atualizar a seção "A migração" em +`docs/guides/developer/i18n.md` e o CHANGELOG. + +### Subtasks (TASK-068) + +- [x] Lote 1: `analysis/**` a zero e no ratchet. Domínio `zebtrack` nos quatro + arquivos: só `analysis/reporters/**` usa o domínio `reporter`, e o que + restava fora dele é prosa de UI/apêndice de validação. Dois defeitos + achados de quebra — cabeçalho de coluna do `.xlsx` e legenda de figura em + português no meio de arquivos 100% ingleses. +- [x] Lote 2: `ui/gui.py` + `ui/ui_coordinator.py`, ambos no ratchet. 10 dos 16 + msgids já existiam com o português IDÊNTICO — `gui.py` duplicava diálogos e + defaults de estado que `dialog_manager`/`analysis_controls` já possuem. +- [x] Lote 3a: `core/recording/**` a zero e no ratchet. Dois `_` como descarte + sombreavam o gettext (`live_session_manager`, `frame_processing_pipeline`) + — pegos por ruff F823 + mypy, nenhum por leitura. +- [x] Lote 3b: `core/project/**` a zero e no ratchet. O guia pós-criação citava + SEIS rótulos de aba/botão retipados à mão, dois deles já errados; agora + interpolam o msgid do próprio widget. Mais um `_` descarte quebrando o + gettext (asset_manager), pego só pelo mypy. +- [x] Lote 3c: `core/services/**` a zero e no ratchet. Removida uma ramificação + pelo TEXTO da exceção em `detector_service._validate_range` — traduzir a + mensagem teria quebrado o `if` em silêncio, no sentido pior. +- [x] Lote 3d: `core/video/**`, `core/detection/**`, `core/viewmodels/**`. + __`core/` inteiro fechado__ — `MIGRATED_PATHS` colapsou cinco entradas em + `src/zebtrack/core`. A segunda redação da validação det/seg em + `analysis_control_view_model` passou a reusar o msgid do 3b. + O consumidor do `progress_notifier` NÃO foi simplificado: apurou-se que + nenhum produtor emite o prefixo no campo `step` — a tira-prefixo é + defensiva contra nada. Só o comentário mentiroso foi corrigido. +- [ ] Lote 4: `ui/wizard/**` (13 arquivos) — valores de enum em models.py/enums.py + são persistidos, traduzir só os rótulos exibidos. + - [x] 4a: `models.py` (inglês fixo, camada de esquema), `wizard_dialog.py`, + `experimental_design_step.py`. + - [x] 4b: `confirmation_step.py` (55) + `live_config_step.py` (49), mais dois + arquivos que eles arrastaram junto. Quatro defeitos: + (1) o resumo obtinha a identidade do template com + `.replace('Template carregado: ', '')` sobre o texto RENDERIZADO — + traduzir o prefixo deixaria o prefixo dentro do resumo, em silêncio; + nasceu `format_template_banner_details()` em `templates.py`; + (2) `coordinators/**` já está no ratchet e mesmo assim publicava + "Aguardando sinal externo... (porta N)" como status de UI em DOIS + arquivos — a frase não tem acento, então o scanner (e o ratchet + construído sobre ele) nunca a viu. O tooltip do gatilho externo + retipava essa mesma frase como prosa; agora interpola o msgid; + (3) `--previous` do Babel enrolava o msgid anterior no meio da string + ao gerar o comentário `#|`, produzindo um `.po` que o polib se recusa + a ler. `--no-fuzzy-matching` resolve e alinha com o `i18n_pairs.py`, + que já jogava fora todo palpite fuzzy do Babel; + (4) quatro asserções de teste eram vácuas — `or "Template" in ...`, + `or "vazio" in ...` e dois `"indispon" not in ...` que passariam + mesmo se o aviso fosse exibido. Todas ancoradas no texto real agora. + Unificados ainda `pyserial não instalado`/`não disponível` (duas + grafias da mesma falha) e `Total de Vídeos`/`Total de vídeos`. + - [x] 4c: `model_selection_step.py` (41) + `detection_step.py` (28). Os dois + arquivos tinham um dict de rótulos em CORPO DE MÓDULO (`_METHOD_LABELS`, + `_METHOD_OPTIONS`) — traduzir no lugar congelaria o idioma no import, + então viraram função. Três defeitos estruturais em model_selection: + (1) `" ⭐ Recomendado"` era ANEXADO para exibir e RETIRADO de volta por + `_strip_annotation()` e pelo `startswith`; traduzir só a exibição + colaria o marcador no nome do peso e o `validate()` recusaria um peso + escolhido na própria lista. Agora há uma definição só, + `_recommended_suffix()`, usada nos três pontos; + (2) `_refresh_weight_dropdowns` fazia `rec, _ = ...` DUAS linhas acima de + onde o marcador precisa de `_()` — o gettext viraria o segundo item da + tupla e seria chamado como função (mais três `name, _ =` em + `_default_weight_for_method`); + (3) o erro de faixa montava `f"❌ {label.capitalize()} deve estar..."`, + capitalizando texto traduzido; virou uma frase pronta por campo. + Nos testes, quatro sítios DIRIGIAM o widget com o rótulo em português + (`set("Detecção (det)")`); com o rótulo traduzido eles param 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. Agora usam `_method_display()`. + - [x] 4d: `discovery_step` (20), `custom_regex_dialog` (19), + `file_selection_step` (19), `import_config_step` (16), + `calibration_step` (14), `design_editor_dialog` (9). + __`ui/wizard` inteiro fechado__ — o pacote foi a zero. + `custom_regex_dialog` guardava CINCO cópias do mesmo + `{"group": "Grupo", "day": "Dia", "subject": "Sujeito"}` (cabeçalho, + linha de resultado, placeholder "aguardando", erro de validação e + pré-visualização); cada uma precisaria do seu `_()` e elas têm de + concordar entre si na tela — viraram `_field_labels()`. + `file_selection_step` montava o resumo colando `"N arquivo(s)"` + + `" selecionado(s)"`, onde o particípio teria de concordar com um + sujeito que pode ser masculino, feminino ou os dois ao mesmo tempo; o + português agora começa por rótulo (`"Seleção: ..."`) e nada precisa + concordar. Os rótulos de estratégia de ROI do `import_config_step` + têm português diferente do resumo do `confirmation_step`, então + ganharam msgids próprios em vez de colidir. +- [x] Lote 5: `ui/dialogs/**` (26 arquivos). + - [x] 5a: os sete menores — `color_selection_dialog`, `center_periphery_dialog`, + `diagnostic_progress_dialog`, `model_diagnostics_dialog`, + `subject_selection_dialog`, `pending_videos_dialog`, + `preview_polygon_dialog`. 395 → 375 (o texto do commit diz "395 -> 356"; + o número certo é 375). Dois defeitos: o nome da cor em + `color_selection_dialog` era rótulo, VALOR do radio, chave do `apply()` e + `result["name"]` ao mesmo tempo — traduzir deixaria + `StringVar(value="verde")` sem casar com nada; e os `_BADGE_*_TEXT` de + `preview_polygon_dialog` eram constantes de módulo. O `TAG_STYLES` de + `pending_videos_dialog`, apontado no handoff como sítio de import, + contém só cores — não há o que traduzir. + - [x] 5b: `block_detail_dialog` (79) + `live_analysis_dialog` (59). + 375 → 237. `"Dia {n} - {grupo}"` estava retipado dentro de DEZESSETE + mensagens deste diálogo; virou `_block_label()`. A chave PERSISTIDA do + mesmo bloco (`"Dia_{n}_{grupo}"`, em `add_note`) continua em português, + de propósito. Mais dois `_` descarte abaixo de um `_()` (ruff F823) e + uma colisão de msgid barrada antes de gravar (`Detection Error` / + `Detection failed` já existiam em `pr2-coordinators-tail.json`). + - [x] 5c: `single_video_config_dialog` (40), `project_video_import_dialog` (38), + `create_project_dialog` (27). 237 → 132. __Quatro msgids descartados__ + depois que a checagem de corpus apontou colisão: `Number of Groups:`, + `Group Names`, `Validation` e `Metadata` (este último já existia como + "Metadados"; o cabeçalho da árvore passou a concordar com o resto do + app em vez de exibir "Metadata" só nesta tela). + - [x] 5d: `live_camera_mode_selection_dialog` (17), `aquarium_detection_progress_dialog` + (15), `calibration_dialog` (13), `start_recording_dialog` (13), + `aquarium_assignment_dialog` (11), `multi_aquarium_live_preview_window` + (10). `MODE_DESCRIPTIONS` era `typing.ClassVar` em corpo de classe — o + último sítio de tradução em tempo de import de `ui/dialogs` — e virou + `_mode_descriptions()`. `start_recording_dialog` duplica o seletor de + câmera inteiro de `block_detail_dialog`, então quase tudo reusou 5b. + - [x] 5e: `camera_disconnect_recovery_dialog` (9), `live_config_dialog` (9), + `save_roi_template_dialog` (7), `live_preview_window` (6), + `multi_aquarium_confirm_dialog` (6), `zone_calibration_dialog` (6), + `zone_reuse_dialog` (6), `missing_metadata_dialog` (4). + 53 acentuados → 0, mas __86 msgids novos__: metade do português desses + arquivos não tem acento. Achados: (1) `core/recording/live_session_manager`, + __já dentro do ratchet__, empurrava quatro status em português sem + acento para o mesmo rótulo onde já havia `_()` traduzido — a linha + trocava de idioma no meio da sessão; (2) `start_timer` renderizava + "Início: Início: 12:34:56", porque repetia no valor o rótulo da coluna + vizinha (`stop_time_label`, ao lado, nunca fez isso); (3) __`ui/wizard` + chegou a zero no 4d e nunca entrou em `MIGRATED_PATHS`__ — passou quatro + lotes sem guarda; (4) `"Nenhuma câmera encontrada"` era exibida E + gravada em `camera_var`, que `apply()` usa como chave de busca — virou + `_no_camera_label()`; (5) `"Porta Arduino:"` era segunda grafia do + `"Porta do Arduino:"` do wizard, colapsada num msgid só. +- [x] Fechamento: `MIGRATED_PATHS == ("src/zebtrack",)`, guia e CHANGELOG + atualizados. + +__Ressalva ao critério de pronto:__ `TOTAL: 0` prova apenas que não sobrou +português __acentuado__. O `i18n_scan.py` — 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. Uma varredura dedicada a português sem +acento continua __pendente__ e não foi feita nesta tarefa. + ### [2026-06-09] Sexteto de bugs em projetos live (zonas, lote, contadores, OpenVINO, settings globais) __ID:__ TASK-067 diff --git a/scripts/i18n_scan.py b/scripts/i18n_scan.py index bd72b550..495d473c 100644 --- a/scripts/i18n_scan.py +++ b/scripts/i18n_scan.py @@ -112,6 +112,20 @@ def _exempt_docstring(self, node: ast.Module | ast.ClassDef | ast.FunctionDef) - if isinstance(first.value.value, str): self._exempt.add(id(first.value)) + def visit_Expr(self, node: ast.Expr) -> None: + """Exempt every bare string statement, not just the leading docstring. + + A string that is an expression-statement is evaluated and thrown away — + it can never reach a widget. The only reason to write one is + documentation: PEP 258 attribute docstrings (the paragraph under an enum + member or a class attribute) are exactly this shape, and the project + deliberately keeps its Portuguese prose. Exempting only ``body[0]`` + reported those as untranslated interface strings. + """ + if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str): + self._exempt.add(id(node.value)) + self.generic_visit(node) + # -- visitors ------------------------------------------------------------- def visit_Module(self, node: ast.Module) -> None: self._exempt_docstring(node) diff --git a/scripts/update_translations.py b/scripts/update_translations.py index 80f195a1..ef76e841 100644 --- a/scripts/update_translations.py +++ b/scripts/update_translations.py @@ -88,7 +88,15 @@ def update_or_init(domain: str, pot_path: Path) -> None: if po_path.exists(): subcommand = "update" - extra = ["--previous"] + # --no-fuzzy-matching: without it Babel seeds each new msgid's msgstr + # from whatever existing entry looks similar, marks it fuzzy, and + # records the old text in a "#|" previous-msgid comment. i18n_pairs.py + # throws those guesses away regardless (it overwrites fuzzy msgstr from + # the _pairs files), so the only thing they produce is risk: when the + # guessed msgid spans several lines Babel wraps that "#|" comment in + # the middle of a quoted string, and polib then refuses to parse the + # catalogue at all. + extra = ["--previous", "--no-fuzzy-matching"] else: subcommand = "init" extra = [] diff --git a/src/zebtrack/analysis/analysis_service.py b/src/zebtrack/analysis/analysis_service.py index 8e123690..88dd9619 100644 --- a/src/zebtrack/analysis/analysis_service.py +++ b/src/zebtrack/analysis/analysis_service.py @@ -24,6 +24,7 @@ from zebtrack.analysis.roi import ROI, ROIAnalyzer from zebtrack.analysis.trajectory_validator import TrajectoryQualityValidator from zebtrack.core.services.roi_rule_resolver import RoiRuleConfig, resolve_roi_rule +from zebtrack.i18n import _ from zebtrack.ui import payloads as payloads from zebtrack.ui.event_bus_v2 import Event, UIEvents @@ -238,14 +239,15 @@ def run_full_analysis( unique_tracks = int(validation_stats.get("unique_tracks", 1) or 1) if unique_tracks > 1: validation_warnings.append( - f"Trajetória com {unique_tracks} animais (track_ids). As métricas de ROI " - "no topo do relatório são de OCUPAÇÃO da região (semântica any_track: " - "a ROI conta como ocupada enquanto qualquer animal estiver dentro). " - "As métricas por animal estão na aba 'por_animal' da planilha de " - "resumo (_summary.xlsx). As métricas " - "de comportamento geral que dependem da ORDEM dos frames " - "(curvas_acentuadas) agrupam todos os animais e devem ser lidas com " - "cautela." + _( + "Trajectory with {count} animals (track_ids). The ROI metrics at the " + "top of the report describe region OCCUPANCY (any_track semantics: " + "the ROI counts as occupied while any animal is inside). The " + "per-animal metrics are in the 'por_animal' sheet of the summary " + "spreadsheet (_summary.xlsx). The general behaviour metrics that " + "depend on frame ORDER (curvas_acentuadas) pool every animal " + "together and must be read with caution." + ).format(count=unique_tracks) ) smoothing_cfg = self.settings.trajectory_smoothing @@ -1189,7 +1191,7 @@ def process_videos_batch( root_tk.after( 0, lambda: controller.view.set_status( - f"Iniciando processamento para {total_videos} vídeos..." + _("Starting processing for {count} videos...").format(count=total_videos) ), ) project_manager.set_active_zone_video(None) @@ -1283,7 +1285,8 @@ def process_videos_batch( root_tk.after( 0, lambda e=exc: controller.view.show_error( - "Erro na Análise", f"Ocorreu um erro inesperado: {e}" + _("Analysis Error"), + _("An unexpected error occurred: {error}").format(error=e), ), ) finally: @@ -1326,13 +1329,15 @@ def _finalize_batch_processing( if was_cancelled: root_tk.after( 0, - lambda: controller.view.show_info("Cancelado", "A análise de vídeo foi cancelada."), + lambda: controller.view.show_info( + _("Cancelled"), _("Video analysis was cancelled.") + ), ) elif videos_to_process: - msg = f"Análise concluída. Resultados salvos em:\n{final_output_dir}" - root_tk.after(0, lambda: controller.view.show_info("Sucesso", msg)) + msg = _("Analysis finished. Results saved to:\n{path}").format(path=final_output_dir) + root_tk.after(0, lambda: controller.view.show_info(_("Success"), msg)) - root_tk.after(0, lambda: controller.view.set_status("Pronto.")) + root_tk.after(0, lambda: controller.view.set_status(_("Ready."))) controller._publish_processing_mode(source="processing.finalize", force=True) controller.ui_event_bus.publish( Event( diff --git a/src/zebtrack/analysis/data_transformer.py b/src/zebtrack/analysis/data_transformer.py index f3bd0dd3..ca941483 100644 --- a/src/zebtrack/analysis/data_transformer.py +++ b/src/zebtrack/analysis/data_transformer.py @@ -674,14 +674,18 @@ def rename_geotaxis_columns( bottom_cm = i * zone_height top_cm = (i + 1) * zone_height + # English like every other exported column header (see + # DISPLAY_COLUMN_MAPPING): a spreadsheet header that changed with + # ui.language would give the same analysis a different schema on + # each machine, breaking the researcher's downstream scripts. if i == 0: - name = "Fundo" + name = "Bottom" elif i == num_zones - 1: - name = "Superfície" + name = "Surface" else: - name = "Meio" + name = "Middle" if num_zones > 3: - name = f"Meio {i}" + name = f"Middle {i}" new_name = f"{name} ({bottom_cm:.1f}-{top_cm:.1f}cm) [%]" rename_map[col_name] = new_name diff --git a/src/zebtrack/analysis/roi.py b/src/zebtrack/analysis/roi.py index 314d079c..bbea58af 100644 --- a/src/zebtrack/analysis/roi.py +++ b/src/zebtrack/analysis/roi.py @@ -33,6 +33,7 @@ VALID_ROI_INCLUSION_RULES, RoiRuleConfig, ) +from zebtrack.i18n import _ # Padrão DE-9IM "interiores se tocam": é o predicado de sobreposição de área # NÃO-NULA. `intersects` não serve — devolve True para tangência (contato só de @@ -814,9 +815,11 @@ def _calculate_bbox_intersects( missing_cols = [col for col in required_cols if col not in self._trajectory.columns] if missing_cols: raise ValueError( - f"Regra bbox_intersects requer colunas de bbox: {missing_cols}. " - f"Essas colunas não estão disponíveis no dataset. " - f"Considere usar 'centroid_in' ou 'centroid_in_on_buffered_roi'." + _( + "The bbox_intersects rule requires the bbox columns: {columns}. " + "They are not available in this dataset. Consider using " + "'centroid_in' or 'centroid_in_on_buffered_roi'." + ).format(columns=missing_cols) ) prepare(roi_geometry) @@ -942,7 +945,7 @@ def _load_mask_geometries(self, mask_source: Any) -> np.ndarray | None: return mask_source self._degrade( "alinhamento_invalido", - "As máscaras pré-alinhadas não têm o mesmo número de linhas da trajetória.", + _("The pre-aligned masks do not have the same row count as the trajectory."), ) return None @@ -955,15 +958,19 @@ def _load_mask_geometries(self, mask_source: Any) -> np.ndarray | None: if missing: self._degrade( "colunas_ausentes", - f"O sidecar de máscaras não tem as colunas {sorted(missing)}.", + _("The mask sidecar is missing the columns {columns}.").format( + columns=sorted(missing) + ), ) return None if "frame" not in self._trajectory.columns: self._degrade( "trajetoria_sem_frame", - "A trajetória analisada não tem a coluna 'frame', que é metade " - "da chave de junção com as máscaras.", + _( + "The analysed trajectory has no 'frame' column, which is half of " + "the join key with the masks." + ), ) return None @@ -999,7 +1006,7 @@ def _load_mask_geometries(self, mask_source: Any) -> np.ndarray | None: if not geometries: self._degrade( "sidecar_vazio", - "O sidecar de máscaras não contém nenhuma geometria válida.", + _("The mask sidecar contains no valid geometry."), ) return None @@ -1039,8 +1046,7 @@ def _load_mask_geometries(self, mask_source: Any) -> np.ndarray | None: if matched == 0: self._degrade( "sem_correspondencia", - "Nenhuma máscara do sidecar corresponde a (frame, track_id) da " - "trajetória analisada.", + _("No mask in the sidecar matches a (frame, track_id) of the analysed trajectory."), ) return None @@ -1058,16 +1064,18 @@ def _read_mask_frame(self, mask_source: Any) -> pd.DataFrame | None: if mask_source is None: self._degrade( "sidecar_ausente", - "A regra 'seg_overlap' foi selecionada mas nenhum sidecar de " - "máscaras (3b_Mascaras_*.parquet) foi informado. Grave com " - "recorder.persist_masks ligado e um modelo de segmentação " - "(model_selection.animal_method='seg').", + _( + "The 'seg_overlap' rule was selected but no mask sidecar " + "(3b_Mascaras_*.parquet) was provided. Record with " + "recorder.persist_masks enabled and a segmentation model " + "(model_selection.animal_method='seg')." + ), ) return None if isinstance(mask_source, pd.DataFrame): if mask_source.empty: - self._degrade("sidecar_vazio", "O sidecar de máscaras está vazio.") + self._degrade("sidecar_vazio", _("The mask sidecar is empty.")) return None return mask_source @@ -1075,20 +1083,25 @@ def _read_mask_frame(self, mask_source: Any) -> pd.DataFrame | None: if not path.exists(): self._degrade( "sidecar_ausente", - f"O sidecar de máscaras não existe em '{path}'. Dados gravados " - "antes desta funcionalidade, ou com recorder.persist_masks " - "desligado, não o têm.", + _( + "The mask sidecar does not exist at '{path}'. Data recorded before " + "this feature, or with recorder.persist_masks disabled, does not " + "have one." + ).format(path=path), ) return None try: masks_df = pd.read_parquet(path) except Exception as exc: # except Exception justificado: degradar, nunca falhar - self._degrade("sidecar_ilegivel", f"O sidecar '{path}' não pôde ser lido: {exc}") + self._degrade( + "sidecar_ilegivel", + _("The sidecar '{path}' could not be read: {error}").format(path=path, error=exc), + ) return None if masks_df.empty: - self._degrade("sidecar_vazio", f"O sidecar '{path}' não tem linhas.") + self._degrade("sidecar_vazio", _("The sidecar '{path}' has no rows.").format(path=path)) return None return masks_df @@ -1096,7 +1109,9 @@ def _degrade(self, reason: str, detail: str) -> None: """Registra a queda de ``seg_overlap`` para ``bbox_intersects``.""" log.warning("roi.seg_overlap.fallback", reason=reason, detail=detail) self._degradation_warnings.append( - f"Regra de ROI 'seg_overlap' degradada para 'bbox_intersects': {detail}" + _("ROI rule 'seg_overlap' degraded to 'bbox_intersects': {detail}").format( + detail=detail + ) ) def get_time_spent_in_rois(self) -> dict[str, dict[str, float]]: diff --git a/src/zebtrack/analysis/visualization_generator.py b/src/zebtrack/analysis/visualization_generator.py index 7164fd4b..c7e42ce9 100644 --- a/src/zebtrack/analysis/visualization_generator.py +++ b/src/zebtrack/analysis/visualization_generator.py @@ -949,7 +949,7 @@ def _geometry_to_cm(roi_obj: ROI): ax.text( 0.5, 0.95, - "Área de processamento (sem ROIs definidas)", + "Processing area (no ROIs defined)", ha="center", va="top", transform=ax.transAxes, diff --git a/src/zebtrack/coordinators/live_camera_session_coordinator.py b/src/zebtrack/coordinators/live_camera_session_coordinator.py index e6389350..73ea2bb6 100644 --- a/src/zebtrack/coordinators/live_camera_session_coordinator.py +++ b/src/zebtrack/coordinators/live_camera_session_coordinator.py @@ -2021,7 +2021,7 @@ def _external_trigger_allows_start( Event( type=UIEvents.UI_SET_STATUS, data=payloads.StatusPayload( - message=f"Aguardando sinal externo... (porta {port})" + message=_("Waiting for external signal... (port {port})").format(port=port) ), source="LiveCameraSessionCoordinator._external_trigger_allows_start", ) diff --git a/src/zebtrack/coordinators/recording_session_coordinator.py b/src/zebtrack/coordinators/recording_session_coordinator.py index 3604ce19..c64212c8 100644 --- a/src/zebtrack/coordinators/recording_session_coordinator.py +++ b/src/zebtrack/coordinators/recording_session_coordinator.py @@ -547,7 +547,9 @@ def _handle_external_trigger(self, context: dict, arduino_enabled: bool) -> bool Event( type=UIEvents.UI_SET_STATUS, data=payloads.StatusPayload( - message=f"Aguardando sinal externo... (porta {port})" + message=_("Waiting for external signal... (port {port})").format( + port=port + ) ), ) ) diff --git a/src/zebtrack/core/detection/aquarium_detector.py b/src/zebtrack/core/detection/aquarium_detector.py index 7491881f..960d9738 100644 --- a/src/zebtrack/core/detection/aquarium_detector.py +++ b/src/zebtrack/core/detection/aquarium_detector.py @@ -610,7 +610,10 @@ def detect_multiple_aquariums( ValueError: If expected_count != 2. """ if expected_count != 2: - raise ValueError("Apenas 2 aquários são suportados") + # Internal API contract, not operator copy: the wizard already + # validates this count with a translated message. English without + # _(), like the other developer-facing guards. + raise ValueError("Only 2 aquariums are supported") video_path_str = str(Path(video_path) if isinstance(video_path, str) else video_path) conf = _clamp_confidence(confidence_threshold, default=0.05) @@ -783,7 +786,10 @@ def detect_multiple_aquariums( ValueError: If expected_count != 2. """ if expected_count != 2: - raise ValueError("Apenas 2 aquários são suportados") + # Internal API contract, not operator copy: the wizard already + # validates this count with a translated message. English without + # _(), like the other developer-facing guards. + raise ValueError("Only 2 aquariums are supported") video_path = str(Path(video_path) if isinstance(video_path, str) else video_path) log.info( @@ -856,7 +862,10 @@ def detect_multiple_aquariums_from_frame( ValueError: If expected_count != 2. """ if expected_count != 2: - raise ValueError("Apenas 2 aquários são suportados") + # Internal API contract, not operator copy: the wizard already + # validates this count with a translated message. English without + # _(), like the other developer-facing guards. + raise ValueError("Only 2 aquariums are supported") return self._detect_aquariums_by_contours(frame, expected_count) diff --git a/src/zebtrack/core/detection/multi_aquarium_detector.py b/src/zebtrack/core/detection/multi_aquarium_detector.py index 718fdd95..00bf59f6 100644 --- a/src/zebtrack/core/detection/multi_aquarium_detector.py +++ b/src/zebtrack/core/detection/multi_aquarium_detector.py @@ -138,8 +138,8 @@ def set_multi_aquarium_zones( polygon_points=polygon_count, ) raise ValueError( - f"Aquário {aq.id} possui polígono inválido: " - f"mínimo 3 pontos, encontrado {polygon_count}" + f"Aquarium {aq.id} has an invalid polygon: " + f"minimum 3 points, found {polygon_count}" ) # Sync scaled polygons into ZoneScaler so delegation methods @@ -227,7 +227,7 @@ def _init_bytetracker_for_aquarium(self, aq: AquariumData) -> None: error=str(e), exc_info=True, ) - raise RuntimeError(f"Falha ao inicializar ByteTracker para aquário {aq.id}: {e}") from e + raise RuntimeError(f"Failed to initialise ByteTracker for aquarium {aq.id}: {e}") from e log.debug( "multi_aquarium_detector.tracker_created", @@ -624,7 +624,7 @@ def process_aquarium( "multi_aquarium_detector.parallel.tracker_missing", aquarium_id=aq_id, ) - errors[aq_id] = f"ByteTracker não inicializado para aquário {aq_id}" + errors[aq_id] = f"ByteTracker not initialised for aquarium {aq_id}" results[aq_id] = [] continue @@ -681,8 +681,8 @@ def _track_partitioned(self, partitioned: dict[int, list]) -> dict[int, list[tup available=list(self._byte_trackers_multi.keys()), ) raise RuntimeError( - f"ByteTracker não inicializado para aquário {aq_id}. " - "Chame set_multi_aquarium_zones() primeiro." + f"ByteTracker not initialised for aquarium {aq_id}. " + "Call set_multi_aquarium_zones() first." ) tracked = self._apply_byte_tracking_multi(detections, tracker) else: diff --git a/src/zebtrack/core/project/asset_manager.py b/src/zebtrack/core/project/asset_manager.py index 20638319..272ab3b8 100644 --- a/src/zebtrack/core/project/asset_manager.py +++ b/src/zebtrack/core/project/asset_manager.py @@ -28,6 +28,7 @@ from zebtrack.core.detection import ZoneData from zebtrack.core.project.roi_template_manager import ROITemplateManager from zebtrack.core.project.types import AssetType +from zebtrack.i18n import _ log = structlog.get_logger() @@ -103,7 +104,7 @@ def ensure_roi_template_dir(project_path: Path | str) -> Path: ValueError: If project_path is None """ if not project_path: - raise ValueError("Projeto não inicializado para salvar templates de ROI.") + raise ValueError(_("Project not initialised for saving ROI templates.")) target = Path(project_path) / "roi_templates" target.mkdir(parents=True, exist_ok=True) return target @@ -227,13 +228,13 @@ def save_roi_template( """ normalized_name = (name or "").strip() if not normalized_name: - raise ValueError("O nome do template não pode ficar vazio.") + raise ValueError(_("The template name cannot be empty.")) if zone_data is None: - raise ValueError("Dados de zona inválidos para salvar o template.") + raise ValueError(_("Invalid zone data for saving the template.")) if not save_arena and not save_rois: - raise ValueError("Selecione ao menos arena ou ROIs para salvar.") + raise ValueError(_("Select at least the arena or the ROIs to save.")) target_location: Literal["project", "global", "custom"] target_location = save_location or "project" @@ -241,7 +242,7 @@ def save_roi_template( if target_location == "project": if not project_path: raise ValueError( - "Não é possível salvar o template no projeto atual: projeto não carregado." + _("Cannot save the template into the current project: no project loaded.") ) # Ensure zone structures @@ -254,7 +255,9 @@ def save_roi_template( project_data, normalized_name ) if existing_entry and not overwrite: - raise ValueError(f"Template '{normalized_name}' já existe.") + raise ValueError( + _("Template '{name}' already exists.").format(name=normalized_name) + ) if existing_entry: slug = existing_entry.get("slug") or self._slugify(normalized_name) @@ -372,7 +375,7 @@ def import_roi_template( data_block = payload.get("data") if isinstance(payload, dict) else None if not isinstance(data_block, dict): - raise ValueError("Arquivo de template inválido: bloco 'data' ausente.") + raise ValueError(_("Invalid template file: the 'data' block is missing.")) zone_data = zone_data_from_dict_fn(data_block) template_name = name or payload.get("name") or file_path.stem @@ -424,11 +427,11 @@ def load_roi_template( FileNotFoundError: If template file doesn't exist """ if location in (None, "project"): - _, entry = self._resolve_roi_template_entry(project_data, name) + _index, entry = self._resolve_roi_template_entry(project_data, name) if entry: relative_file = entry.get("file") if not relative_file: - raise ValueError("Arquivo do template não registrado no projeto.") + raise ValueError(_("The template file is not registered in the project.")) template_path = ( Path(project_path) / relative_file if project_path else Path(relative_file) @@ -441,12 +444,14 @@ def load_roi_template( data_block = payload.get("data") if isinstance(payload, dict) else None if not isinstance(data_block, dict): - raise ValueError("Conteúdo do template inválido.") + raise ValueError(_("Invalid template content.")) return zone_data_from_dict_fn(data_block) if location == "project": - raise ValueError(f"Template de ROI '{name}' não encontrado no projeto.") + raise ValueError( + _("ROI template '{name}' not found in the project.").format(name=name) + ) template_path = Path(file_path) if file_path else None @@ -459,7 +464,9 @@ def load_roi_template( break if template_path is None: - raise ValueError(f"Template de ROI '{name}' não encontrado para o contexto solicitado.") + raise ValueError( + _("ROI template '{name}' not found for the requested context.").format(name=name) + ) if not template_path.exists(): raise FileNotFoundError(str(template_path)) @@ -469,7 +476,7 @@ def load_roi_template( data_block = payload.get("data") if isinstance(payload, dict) else None if not isinstance(data_block, dict): - raise ValueError("Conteúdo do template inválido.") + raise ValueError(_("Invalid template content.")) return zone_data_from_dict_fn(data_block) @@ -688,7 +695,8 @@ def video_has_asset_for_aquarium( or parquet_files.get("report_docx") ) - raise ValueError(f"Asset type '{asset}' desconhecido para aquário.") + # Names a code-level asset type, never shown to the operator. + raise ValueError(f"Unknown asset type '{asset}' for aquarium.") def can_remove_asset(self, video_entry: dict, asset: AssetType) -> tuple[bool, str | None]: """Check if an asset can be removed (dependency validation). @@ -706,30 +714,41 @@ def can_remove_asset(self, video_entry: dict, asset: AssetType) -> tuple[bool, s if has_summary_outputs: return ( False, - ("Remova os relatórios e sumários antes de apagar arena, ROIs ou trajetórias."), + _( + "Remove the reports and summaries before deleting the arena, " + "the ROIs or the trajectory." + ), ) if not self.video_has_asset(video_entry, asset): - labels = { - "arena": "arena", - "rois": "ROIs", - "trajectory": "trajetória", + # One COMPLETE sentence per asset instead of splicing a noun + # into a template: the Portuguese original had to agree in + # gender with the spliced word ("registrada"), which no + # substitution-based phrasing can guarantee in translation. + missing = { + "arena": _("There is no arena recorded for this video."), + "rois": _("There are no ROIs recorded for this video."), + "trajectory": _("There is no trajectory recorded for this video."), } - missing_label = labels.get(asset, asset) - return False, f"Não há {missing_label} registrada para este vídeo." + return False, missing.get( + asset, _("There is no {asset} recorded for this video.").format(asset=asset) + ) if asset == "summary" and not has_summary_outputs: - return False, "Não há relatórios ou sumários para remover." + return False, _("There are no reports or summaries to remove.") if asset == "video": if has_summary_outputs: - return False, "Remova relatórios e sumários antes de excluir o vídeo." + return False, _("Remove the reports and summaries before deleting the video.") if any( self.video_has_asset(video_entry, cast(AssetType, dependency)) for dependency in ("trajectory", "rois", "arena") ): return ( False, - ("Remova arena, ROIs e trajetórias antes de excluir o vídeo do projeto."), + _( + "Remove the arena, the ROIs and the trajectory before deleting " + "the video from the project." + ), ) return True, None diff --git a/src/zebtrack/core/project/project_lifecycle_manager.py b/src/zebtrack/core/project/project_lifecycle_manager.py index 80c4f42c..07f82516 100644 --- a/src/zebtrack/core/project/project_lifecycle_manager.py +++ b/src/zebtrack/core/project/project_lifecycle_manager.py @@ -20,6 +20,7 @@ import yaml from zebtrack.core.exceptions import ProjectInvalidError +from zebtrack.i18n import _ from zebtrack.utils import IntegrityError, calculate_sha256 if TYPE_CHECKING: @@ -68,9 +69,11 @@ def load_project_data( if not os.path.exists(config_path): log_context.error("project.load.not_found") raise ProjectInvalidError( - message=f"Arquivo de configuração do projeto '{CONFIG_FILE_NAME}' não " - f"encontrado no diretório selecionado: {project_path}\n\n" - "Por favor, garanta que você selecionou uma pasta de projeto válida.", + message=_( + "Project configuration file '{filename}' not found in the selected " + "directory: {path}\n\nPlease make sure you selected a valid project " + "folder." + ).format(filename=CONFIG_FILE_NAME, path=project_path), path=project_path, ) @@ -87,8 +90,10 @@ def load_project_data( except (OSError, json.JSONDecodeError, IntegrityError) as e: log_context.error("project.load.error", exc_info=e) raise ProjectInvalidError( - message=f"Falha ao carregar ou analisar o arquivo de configuração do projeto: " - f"{config_path}\n\nO arquivo pode estar corrompido ou ilegível.\n\nErro: {e}", + message=_( + "Failed to load or parse the project configuration file: {path}\n\n" + "The file may be corrupted or unreadable.\n\nError: {error}" + ).format(path=config_path, error=e), path=project_path, cause=e, ) from e @@ -112,8 +117,10 @@ def save_project_data( if not project_path: log.debug("project.save.no_path", reason="project not yet created") raise ProjectInvalidError( - message="Não é possível salvar o projeto: caminho do projeto não definido.\n\n" - "O projeto deve ser criado antes de ser salvo.", + message=_( + "Cannot save the project: the project path is not set.\n\n" + "The project must be created before it can be saved." + ), ) try: @@ -122,26 +129,30 @@ def save_project_data( except PermissionError as e: log.error("project.save.permission_denied", path=project_path, exc_info=e) raise ProjectInvalidError( - message=( - f"Permissão negada ao salvar o projeto: {project_path}\n\n" - f"Verifique se você tem permissão de escrita na pasta.\n\nErro: {e}" - ), + message=_( + "Permission denied while saving the project: {path}\n\n" + "Check that you have write permission on the folder.\n\nError: {error}" + ).format(path=project_path, error=e), path=project_path, cause=e, ) from e except OSError as e: log.error("project.save.io_error", path=project_path, exc_info=e) raise ProjectInvalidError( - message=f"Erro de I/O ao salvar o projeto: " - f"{project_path}\n\nVerifique o espaço em disco e permissões.\n\nErro: {e}", + message=_( + "I/O error while saving the project: {path}\n\n" + "Check the disk space and the permissions.\n\nError: {error}" + ).format(path=project_path, error=e), path=project_path, cause=e, ) from e except (json.JSONDecodeError, TypeError, ValueError) as e: log.error("project.save.serialization_error", path=project_path, exc_info=e) raise ProjectInvalidError( - message=f"Erro ao serializar dados do projeto: " - f"{project_path}\n\nDados do projeto podem estar corrompidos.\n\nErro: {e}", + message=_( + "Error serialising the project data: {path}\n\n" + "The project data may be corrupted.\n\nError: {error}" + ).format(path=project_path, error=e), path=project_path, cause=e, ) from e @@ -149,8 +160,10 @@ def save_project_data( except Exception as e: log.error("project.save.unexpected_error", path=project_path, exc_info=e) raise ProjectInvalidError( - message=f"Erro inesperado ao salvar o projeto: " - f"{project_path}\n\nPor favor, verifique as permissões da pasta.\n\nErro: {e}", + message=_( + "Unexpected error while saving the project: {path}\n\n" + "Please check the folder permissions.\n\nError: {error}" + ).format(path=project_path, error=e), path=project_path, cause=e, ) from e @@ -192,26 +205,28 @@ def validate_project_parameters( """ # Validate aquarium count if num_aquariums < 1: - raise ValueError("num_aquariums deve ser >= 1") + # Developer-facing guards: they name the code parameter, are + # raised before any dialog exists, and are English without _(). + raise ValueError("num_aquariums must be >= 1") if num_aquariums > 100: - raise ValueError("num_aquariums deve ser <= 100 (limite prático)") + raise ValueError("num_aquariums must be <= 100 (practical limit)") # Validate animals per aquarium if animals_per_aquarium < 1: - raise ValueError("animals_per_aquarium deve ser >= 1") + raise ValueError("animals_per_aquarium must be >= 1") if animals_per_aquarium > 100: - raise ValueError("animals_per_aquarium deve ser <= 100 (limite prático)") + raise ValueError("animals_per_aquarium must be <= 100 (practical limit)") # Phase 1.2: Calibration dimensions — 0 means "no calibration", valid if aquarium_width_cm < 0: - raise ValueError("aquarium_width_cm deve ser >= 0 (0 = sem calibração)") + raise ValueError("aquarium_width_cm must be >= 0 (0 = no calibration)") if aquarium_width_cm > 500: - raise ValueError("aquarium_width_cm deve ser <= 500 cm (valor irreal)") + raise ValueError("aquarium_width_cm must be <= 500 cm (unrealistic value)") if aquarium_height_cm < 0: - raise ValueError("aquarium_height_cm deve ser >= 0 (0 = sem calibração)") + raise ValueError("aquarium_height_cm must be >= 0 (0 = no calibration)") if aquarium_height_cm > 500: - raise ValueError("aquarium_height_cm deve ser <= 500 cm (valor irreal)") + raise ValueError("aquarium_height_cm must be <= 500 cm (unrealistic value)") # Validate frame intervals if analysis_interval_frames < 1: @@ -379,10 +394,10 @@ def create_new_project( except OSError as e: log.error("project.create.dir_error", error=str(e)) raise ProjectInvalidError( - message=( - f"Não foi possível criar o diretório do projeto: {e}\n\n" - "Por favor, verifique as permissões da pasta e se o caminho é válido." - ), + message=_( + "Could not create the project directory: {error}\n\n" + "Please check the folder permissions and that the path is valid." + ).format(error=e), path=project_path, cause=e, ) from e @@ -617,9 +632,13 @@ def apply_project_migrations( default_profile = ( default_analysis_profile_fn() if default_analysis_profile_fn + # STORED in project.json. Kept English and aligned with the + # canonical AssetManager._default_analysis_profile(), which + # already writes "default": a stored profile name that changed + # with ui.language would differ per machine for the same project. else { - "name": "Padrão", - "description": "Perfil padrão de análise", + "name": "default", + "description": "Default analysis profile", "settings": {}, } ) diff --git a/src/zebtrack/core/project/project_manager.py b/src/zebtrack/core/project/project_manager.py index 0973237b..ce9e3199 100644 --- a/src/zebtrack/core/project/project_manager.py +++ b/src/zebtrack/core/project/project_manager.py @@ -26,6 +26,7 @@ from zebtrack.core.project.zone_manager import ZoneManager from zebtrack.core.project.zone_orchestration_manager import ZoneOrchestrationManager from zebtrack.core.state_manager import StateManager +from zebtrack.i18n import _ # Re-export for backward compatibility — external code imports ProjectInvalidError from here __all__ = ["ProjectInvalidError", "ProjectManager"] @@ -134,7 +135,7 @@ def save_roi_template( persist_callback = self.save_project if persist else None if save_location in (None, "project") and self.project_path is None: raise ValueError( - "Não é possível salvar o template no projeto atual: projeto não carregado." + _("Cannot save the template into the current project: no project loaded.") ) return self.asset_manager.save_roi_template( project_data=self.project_data, @@ -363,7 +364,7 @@ def clone_zone_data_from_video(self, video_path: Path | str) -> ZoneData: """Return a deep copy of zone data stored for another video.""" video_path = str(Path(video_path) if isinstance(video_path, str) else video_path) - _, stored = self._resolve_zone_entry(video_path) + _key, stored = self._resolve_zone_entry(video_path) return self._zone_data_from_dict(stored) def export_zones_to_parquet( @@ -522,7 +523,7 @@ def _apply_project_migrations( def load_project(self, project_path: Path | str) -> None: """Load project data from disk (thread-safe). Delegates to ProjectLifecycleManager.""" project_path = Path(project_path) if isinstance(project_path, str) else project_path - loaded_data, migration_applied, _ = ProjectLifecycleManager.load_project_data( + loaded_data, migration_applied, _fields = ProjectLifecycleManager.load_project_data( project_path, load_config_fn=self.project_service.load_project_config, apply_migrations_fn=self._apply_project_migrations, @@ -825,7 +826,7 @@ def can_remove_asset(self, video_path: Path | str, asset: AssetType) -> tuple[bo video_path = str(Path(video_path) if isinstance(video_path, str) else video_path) video_entry = self.find_video_entry(path=video_path) if not video_entry: - return False, "Vídeo não encontrado no projeto." + return False, _("Video not found in the project.") return self.asset_manager.can_remove_asset(video_entry, asset) def remove_asset( diff --git a/src/zebtrack/core/project/project_workflow_service.py b/src/zebtrack/core/project/project_workflow_service.py index a4202616..4bdd590c 100644 --- a/src/zebtrack/core/project/project_workflow_service.py +++ b/src/zebtrack/core/project/project_workflow_service.py @@ -25,6 +25,7 @@ from zebtrack.core.exceptions import ProjectInvalidError from zebtrack.core.services.weight_manager import TARGET_AQUARIUM, TARGET_ZEBRAFISH, VALID_METHODS +from zebtrack.i18n import _ if TYPE_CHECKING: from zebtrack.core.project.project_manager import ProjectManager @@ -124,14 +125,12 @@ def validate_project_parameters(self, **kwargs: Any) -> tuple[bool, str | None]: # Validate detection mode compatibility if animal_method == "det" and animals_per_aquarium != 1: - error_msg = ( - "O modo de detecção (det) para animais só é compatível com 1 " - f"animal por aquário.\n" - f"Configuração atual: {animals_per_aquarium} " - "animais por aquário.\n\n" - "Para usar múltiplos animais por aquário, altere o método de " - "detecção de animais para 'seg' (segmentação) nas configurações." - ) + error_msg = _( + "The detection mode (det) for animals is only compatible with 1 animal " + "per aquarium.\nCurrent configuration: {count} animals per aquarium.\n\n" + "To use multiple animals per aquarium, change the animal detection " + "method to 'seg' (segmentation) in the settings." + ).format(count=animals_per_aquarium) log.warning( "project_workflow_service.validation_failed", reason="det_mode_incompatible_with_multi_animal", @@ -1181,7 +1180,8 @@ def open_project( "videos_count": videos_count, "zone_status": zone_status, "roi_count": roi_count, - "active_weight": resolved_weight or "Padrão", + # Display value pushed into StateManager, not a stored key. + "active_weight": resolved_weight or _("Default"), "use_openvino": resolved_openvino, } @@ -1319,49 +1319,60 @@ def _feature_available(video: dict, feature_key: str) -> bool: ) # Build message + # Every quoted tab/button name is interpolated from the WIDGET's own + # msgid instead of being retyped here. Retyping is how this guide drifted: + # it still told the operator to open a "Relatórios" tab (really + # "Processamento e Relatórios") and to click "Adicionar e Processar Novos + # Vídeos", a button label that exists nowhere in the codebase. lines: list[str] = [] - lines.append("🎉 Projeto criado com sucesso!") + lines.append(_("🎉 Project created successfully!")) lines.append("") - lines.append("📊 Status dos vídeos:") - lines.append(f" • Total de vídeos: {total_videos}") - lines.append(f" • Com arena definida: {videos_with_arena}") - lines.append(f" • Com ROIs definidas: {videos_with_rois}") - lines.append(f" • Com trajetória pronta: {videos_with_trajectory}") - lines.append(f" • Pendentes de processamento: {videos_pending}") + lines.append(_("📊 Video status:")) + lines.append(_(" • Total videos: {count}").format(count=total_videos)) + lines.append(_(" • With arena defined: {count}").format(count=videos_with_arena)) + lines.append(_(" • With ROIs defined: {count}").format(count=videos_with_rois)) + lines.append(_(" • With trajectory ready: {count}").format(count=videos_with_trajectory)) + lines.append(_(" • Pending processing: {count}").format(count=videos_pending)) lines.append("") - lines.append("🚀 Próximos passos recomendados:") + lines.append(_("🚀 Recommended next steps:")) lines.append("") step_num = 1 if videos_with_arena > 0 or videos_with_rois > 0: - lines.append(f"{step_num}. Visualizar e ajustar zonas importadas") - lines.append(" - Abra a aba 'Configuração de Zonas'") - lines.append(" - Use o painel 'Selecionar Vídeo para Desenho'") - lines.append(" - Clique duas vezes ou use 'Carregar Frame' para revisar") - lines.append(" - Ajuste arena e ROIs conforme necessário") + lines.append(_("{step}. Review and adjust the imported zones").format(step=step_num)) + lines.append(_(" - Open the '{tab}' tab").format(tab=_("Zone Configuration"))) + lines.append( + _(" - Use the '{panel}' panel").format(panel=_("📹 Select Video for Drawing")) + ) + lines.append( + _(" - Double-click, or use '{button}', to review").format( + button=_("📹 Load Frame from the Selected Video") + ) + ) + lines.append(_(" - Adjust the arena and the ROIs as needed")) lines.append("") step_num += 1 if videos_pending > 0: - lines.append(f"{step_num}. Processar vídeos pendentes") - lines.append(" - Vá até a aba 'Controle Principal'") - lines.append(" - Confirme os intervalos de processamento") - lines.append(" - Clique em 'Adicionar e Processar Novos Vídeos'") + lines.append(_("{step}. Process the pending videos").format(step=step_num)) + lines.append(_(" - Open the '{tab}' tab").format(tab=_("Main Control"))) + lines.append(_(" - Confirm the processing intervals")) + lines.append(_(" - Click '{button}'").format(button=_("Process Pending Videos..."))) lines.append("") step_num += 1 if videos_with_trajectory > 0: - lines.append(f"{step_num}. Gerar relatórios") - lines.append(" - Acesse a aba 'Relatórios'") - lines.append(" - Navegue pela hierarquia de grupos, dias e sujeitos") - lines.append(" - Gere relatórios individuais ou unificados conforme necessário") + lines.append(_("{step}. Generate reports").format(step=step_num)) + lines.append(_(" - Open the '{tab}' tab").format(tab=_("Processing and Reports"))) + lines.append(_(" - Browse the hierarchy of groups, days and subjects")) + lines.append(_(" - Generate individual or unified reports as needed")) lines.append("") - lines.append("💡 Dicas:") - lines.append(" • Use a busca para localizar vídeos rapidamente") - lines.append(" • Os símbolos de status indicam arenas, ROIs e trajetórias disponíveis") - lines.append(" • Ajuste zonas antes de processar se necessário") + lines.append(_("💡 Tips:")) + lines.append(_(" • Use the search box to locate videos quickly")) + lines.append(_(" • The status symbols show which arenas, ROIs and trajectories exist")) + lines.append(_(" • Adjust the zones before processing if necessary")) message = "\n".join(lines) diff --git a/src/zebtrack/core/project/roi_template_manager.py b/src/zebtrack/core/project/roi_template_manager.py index 521a54a9..dc4a0c19 100644 --- a/src/zebtrack/core/project/roi_template_manager.py +++ b/src/zebtrack/core/project/roi_template_manager.py @@ -18,6 +18,7 @@ from zebtrack.core.detection import ZoneData from zebtrack.core.project.schemas import InvalidTemplateError, ROITemplateSchema +from zebtrack.i18n import _ log = structlog.get_logger() @@ -99,30 +100,32 @@ def save_template( """ # Validações if not name or not name.strip(): - raise ValueError("O nome do template não pode ficar vazio.") + raise ValueError(_("The template name cannot be empty.")) if not save_arena and not save_rois: - raise ValueError("Selecione ao menos arena ou ROIs para salvar.") + raise ValueError(_("Select at least the arena or the ROIs to save.")) if not zone_data: - raise ValueError("Dados de zona não podem ser vazios.") + raise ValueError(_("Zone data cannot be empty.")) if save_arena and (not zone_data.polygon or len(zone_data.polygon) < 3): - raise ValueError("Arena inválida: é necessário ao menos 3 pontos.") + raise ValueError(_("Invalid arena: at least 3 points are required.")) if save_rois and not zone_data.roi_polygons: - raise ValueError("Nenhuma ROI disponível para salvar.") + raise ValueError(_("No ROI available to save.")) # Determine target directory if save_location == "global": target_dir = self.global_templates_dir elif save_location == "project": if not project_path: - raise ValueError("Caminho do projeto é necessário para salvar template no projeto.") + raise ValueError( + _("A project path is required to save a template into the project.") + ) target_dir = Path(project_path) / "roi_templates" elif save_location == "custom": if not custom_path: - raise ValueError("Caminho personalizado é necessário para save_location='custom'.") + raise ValueError(_("A custom path is required for save_location='custom'.")) custom_path = Path(custom_path) if custom_path.is_dir(): target_dir = custom_path @@ -130,7 +133,9 @@ def save_template( # If custom_path is a file, use its parent directory target_dir = custom_path.parent else: - raise ValueError(f"save_location inválido: {save_location}") + # Names a code-level parameter, never shown to the operator: + # English without _(), like the other developer-facing guards. + raise ValueError(f"Invalid save_location: {save_location}") # Create directory if it doesn't exist target_dir.mkdir(parents=True, exist_ok=True) @@ -145,8 +150,9 @@ def save_template( # Check if it already exists if template_path.exists() and not overwrite: raise ValueError( - f"Template '{name}' já existe em {target_dir}. " - f"Use overwrite=True para sobrescrever." + _("Template '{name}' already exists in {directory}.").format( + name=name, directory=target_dir + ) ) # Prepare template data (include only selected components) @@ -221,7 +227,7 @@ def load_template(self, template_path: str | Path) -> ZoneData: template_path = Path(template_path) if not template_path.exists(): - raise FileNotFoundError(f"Template não encontrado: {template_path}") + raise FileNotFoundError(_("Template not found: {path}").format(path=template_path)) try: with open(template_path, encoding="utf-8") as f: @@ -239,12 +245,16 @@ def load_template(self, template_path: str | Path) -> ZoneData: except json.JSONDecodeError as e: log.error("roi_template_manager.load.json_error", file=str(template_path), error=str(e)) - raise InvalidTemplateError(f"JSON inválido em {template_path}: {e}") from e + raise InvalidTemplateError( + _("Invalid JSON in {path}: {error}").format(path=template_path, error=e) + ) from e except ValidationError as e: log.error( "roi_template_manager.load.validation_error", file=str(template_path), error=str(e) ) - raise InvalidTemplateError(f"Template inválido em {template_path}: {e}") from e + raise InvalidTemplateError( + _("Invalid template in {path}: {error}").format(path=template_path, error=e) + ) from e # Reconstruir ZoneData a partir dos dados validados data_block = validated.data diff --git a/src/zebtrack/core/project/schemas.py b/src/zebtrack/core/project/schemas.py index ffb15007..4948339c 100644 --- a/src/zebtrack/core/project/schemas.py +++ b/src/zebtrack/core/project/schemas.py @@ -18,9 +18,12 @@ class AssetType(Enum): class ROITemplateSchema(BaseModel): """Schema para templates de ROI.""" - version: int = Field(ge=1, le=2, description="Versão do template") - name: str = Field(min_length=1, max_length=200, description="Nome do template") - data: dict[str, Any] = Field(description="Dados do template") + # Field descriptions are Pydantic metadata evaluated at class-body + # time, so they can never be _() calls. They are developer-facing + # schema documentation, not interface copy. + version: int = Field(ge=1, le=2, description="Template version") + name: str = Field(min_length=1, max_length=200, description="Template name") + data: dict[str, Any] = Field(description="Template data") @field_validator("version") @classmethod @@ -28,7 +31,9 @@ def validate_version(cls, v: int) -> int: """Validate that the version is supported.""" CURRENT_VERSION = 1 if v > CURRENT_VERSION: - raise ValueError(f"Template version {v} não suportado. Versão atual: {CURRENT_VERSION}") + raise ValueError( + f"Template version {v} is not supported. Current version: {CURRENT_VERSION}" + ) return v @field_validator("data") @@ -41,7 +46,7 @@ def validate_data_structure(cls, v: dict) -> dict: if not has_polygon and not has_rois: raise ValueError( - "Template deve conter pelo menos arena (polygon) ou ROIs " + "Template must contain at least an arena (polygon) or ROIs " "(roi_polygons, roi_names, roi_colors)" ) diff --git a/src/zebtrack/core/project/zone_manager.py b/src/zebtrack/core/project/zone_manager.py index babcc2c0..5dae5ac5 100644 --- a/src/zebtrack/core/project/zone_manager.py +++ b/src/zebtrack/core/project/zone_manager.py @@ -21,6 +21,7 @@ import structlog from zebtrack.core.detection import AquariumData, MultiAquariumZoneData, ZoneData +from zebtrack.i18n import _ log = structlog.get_logger() @@ -422,7 +423,7 @@ def has_zone_data(self, project_data: dict, video_path: Path | str | None) -> bo video_path = Path(video_path) if isinstance(video_path, str) else video_path self.ensure_zone_structures(project_data) - _, stored = self.resolve_zone_entry(project_data, video_path) + _key, stored = self.resolve_zone_entry(project_data, video_path) if not stored: return False @@ -471,7 +472,7 @@ def save_zone_data( if target_video: normalized = self.normalize_video_path(target_video) - existing_key, _ = self.resolve_zone_entry(project_data, target_video) + existing_key, _entry = self.resolve_zone_entry(project_data, target_video) # Ensure store_key is always a string (not Path object) for JSON serialization store_key = normalized or existing_key or str(Path(target_video).as_posix()) @@ -502,7 +503,7 @@ def clear_zone_data_for_video( self.ensure_zone_structures(project_data) - key, _ = self.resolve_zone_entry(project_data, video_path_str) + key, _entry = self.resolve_zone_entry(project_data, video_path_str) if key and key in project_data["zones_by_video"]: del project_data["zones_by_video"][key] @@ -528,7 +529,7 @@ def clone_zone_data_from_video(self, project_data: dict, video_path: Path | str) """ video_path_str = str(Path(video_path) if isinstance(video_path, str) else video_path) - _, stored = self.resolve_zone_entry(project_data, video_path_str) + _key, stored = self.resolve_zone_entry(project_data, video_path_str) return self.zone_data_from_dict(stored) def get_zone_data( @@ -583,7 +584,7 @@ def update_main_polygon( # Validation if not project_data: log.error("zone_manager.polygon.no_project_data") - raise ValueError("Dados do projeto não inicializados") + raise ValueError(_("Project data not initialised")) # Get current zone data zone_data = self.get_zone_data(project_data) diff --git a/src/zebtrack/core/recording/frame_processing_pipeline.py b/src/zebtrack/core/recording/frame_processing_pipeline.py index 053dfd18..971e2823 100644 --- a/src/zebtrack/core/recording/frame_processing_pipeline.py +++ b/src/zebtrack/core/recording/frame_processing_pipeline.py @@ -16,6 +16,8 @@ import numpy as np import structlog +from zebtrack.i18n import _ + if TYPE_CHECKING: from zebtrack.core.detection.multi_aquarium_detector import MultiAquariumDetector from zebtrack.core.main_view_model import MainViewModel @@ -679,16 +681,19 @@ def _processing_loop(self) -> None: # noqa: C901 # Update preview status (Phase 5 / M3 — main-thread bounce) if self.preview_window and frame_number % 5 == 0: - status_msg = ( - f"🔍 Detectando aquário... " - f"({self._aquarium_detection_frames}/{self._aquarium_detection_max_frames})" + status_msg = _("🔍 Detecting aquarium... ({current}/{total})").format( + current=self._aquarium_detection_frames, + total=self._aquarium_detection_max_frames, ) self._post_preview_status(status_msg, color="yellow") # Run detection to find aquarium (class_id=0) detector = self.detector_service.detector if detector: - detections, _ = detector.detect(frame, "live", conf_threshold=0.05) + # NOT `detections, _ =`: binding `_` locally shadows the + # gettext alias for the WHOLE function, including the + # status message a few lines above. + detections, _annotated = detector.detect(frame, "live", conf_threshold=0.05) # Collect aquarium bboxes if detector: diff --git a/src/zebtrack/core/recording/live_analysis_post_processor.py b/src/zebtrack/core/recording/live_analysis_post_processor.py index ee0c0c0f..109a7dbc 100644 --- a/src/zebtrack/core/recording/live_analysis_post_processor.py +++ b/src/zebtrack/core/recording/live_analysis_post_processor.py @@ -18,6 +18,7 @@ import structlog from zebtrack.core.services.roi_rule_resolver import resolve_roi_rule +from zebtrack.i18n import _ if TYPE_CHECKING: from zebtrack.core.detection.multi_aquarium_detector import MultiAquariumDetector @@ -623,36 +624,41 @@ def _show_completion_message( from zebtrack.ui.payloads import MessagePayload if analysis_success and stats: - message = ( - f"✅ Análise de câmera concluída com sucesso!\n\n" - f"📊 Estatísticas:\n" - f" • Frames processados: {stats['frames']}\n" - f" • Detecções totais: {stats['detections']}\n" - f" • Trilhas únicas: {stats['tracks']}\n\n" - f"📁 Dados salvos em:\n{output_dir}\n\n" - f"💡 Arquivos gerados:\n" - f" • *_trajectory.parquet (trajetória)\n" - f" • *_zones.parquet (zonas)\n" - f" • *.mp4/.avi (vídeo gravado)" + message = _( + "✅ Camera analysis completed successfully!\n\n" + "📊 Statistics:\n" + " • Frames processed: {frames}\n" + " • Total detections: {detections}\n" + " • Unique tracks: {tracks}\n\n" + "📁 Data saved to:\n{output_dir}\n\n" + "💡 Files generated:\n" + " • *_trajectory.parquet (trajectory)\n" + " • *_zones.parquet (zones)\n" + " • *.mp4/.avi (recorded video)" + ).format( + frames=stats["frames"], + detections=stats["detections"], + tracks=stats["tracks"], + output_dir=output_dir, ) - title = "Análise Concluída" + title = _("Analysis Completed") elif reason == "no_detections": - message = ( - f"⚠️ Gravação concluída, mas nenhuma detecção foi encontrada.\n\n" - f"Possíveis causas:\n" - f" • Nenhum objeto detectável no campo de visão\n" - f" • Arena muito restritiva\n" - f" • Limiar de confiança muito alto\n\n" - f"📁 Dados salvos em:\n{output_dir}" - ) - title = "Análise Concluída - Sem Detecções" + message = _( + "⚠️ Recording completed, but no detection was found.\n\n" + "Possible causes:\n" + " • No detectable object in the field of view\n" + " • Arena too restrictive\n" + " • Confidence threshold too high\n\n" + "📁 Data saved to:\n{output_dir}" + ).format(output_dir=output_dir) + title = _("Analysis Completed - No Detections") else: - message = ( - f"⚠️ Gravação concluída, mas a análise automática falhou.\n\n" - f"📁 Dados brutos salvos em:\n{output_dir}\n\n" - f"Você pode analisar manualmente pela interface." - ) - title = "Gravação Concluída" + message = _( + "⚠️ Recording completed, but the automatic analysis failed.\n\n" + "📁 Raw data saved to:\n{output_dir}\n\n" + "You can analyse it manually through the interface." + ).format(output_dir=output_dir) + title = _("Recording Completed") self.event_bus.publish( Event( diff --git a/src/zebtrack/core/recording/live_camera_mode.py b/src/zebtrack/core/recording/live_camera_mode.py index 5e0b08a1..e7ac3a50 100644 --- a/src/zebtrack/core/recording/live_camera_mode.py +++ b/src/zebtrack/core/recording/live_camera_mode.py @@ -20,6 +20,8 @@ import structlog +from zebtrack.i18n import _ + if TYPE_CHECKING: from zebtrack.settings import Settings from zebtrack.utils.hardware_capability import HardwareCapabilityReport @@ -49,13 +51,18 @@ class LiveCameraModeRecommendation: warnings: list[str] def __str__(self) -> str: - """Human-readable summary.""" + """Developer-facing summary for logs and debugging. + + Deliberately NOT translated: no call site renders it, and the first + line prints ``recommended_mode.value`` — a persisted enum value that + must stay readable next to the raw log records. + """ return ( - f"Modo Recomendado: {self.recommended_mode.value}\n" - f"Aquários Solicitados: {self.requested_aquariums}\n" - f"Aquários Suportados: {self.max_aquariums_supported}\n" - f"Razão: {self.reason}\n" - f"Alternativas: {len(self.alternative_options)}" + f"Recommended mode: {self.recommended_mode.value}\n" + f"Aquariums requested: {self.requested_aquariums}\n" + f"Aquariums supported: {self.max_aquariums_supported}\n" + f"Reason: {self.reason}\n" + f"Alternatives: {len(self.alternative_options)}" ) @@ -121,16 +128,16 @@ def recommend_mode( if requested_aquariums > 1 else LiveCameraMode.SINGLE_AQUARIUM_REALTIME ) - reason = ( - f"Sistema suporta {max_supported} aquários simultaneamente. " - f"Processamento em tempo real habilitado." - ) + reason = _( + "The system supports {count} aquariums simultaneously. " + "Real-time processing enabled." + ).format(count=max_supported) # Still offer record-only as alternative (for better quality) alternatives.append( ( LiveCameraMode.RECORD_ONLY, - "Gravar sem detecção (melhor qualidade, processar depois)", + _("Record without detection (better quality, process later)"), ) ) @@ -152,42 +159,55 @@ def recommend_mode( else LiveCameraMode.SINGLE_AQUARIUM_REALTIME ) - reason = ( - f"Sistema suporta apenas {max_supported} aquário(s) simultaneamente, " - f"mas {requested_aquariums} foram solicitados. " - ) + # Two COMPLETE sentences, never fragments: the original built this + # by appending half a clause, which leaves a translator unable to + # reorder. "aquário(s)" is gone for the same reason — see the + # singular/plural pair in the warnings below. + reason = _( + "The system supports only {supported} of the {requested} aquariums " + "requested for simultaneous processing. " + ).format(supported=max_supported, requested=requested_aquariums) if allow_sequential: - reason += f"Recomendado: gravar {requested_aquariums} sessões separadas." + reason += _("Recommended: record {count} separate sessions.").format( + count=requested_aquariums + ) else: - reason += "Recomendado: processar apenas 1 aquário nesta sessão." + reason += _("Recommended: process only 1 aquarium in this session.") # Build alternatives if allow_sequential: alternatives.append( ( LiveCameraMode.SINGLE_AQUARIUM_REALTIME, - "Processar apenas 1 aquário agora (ignorar demais)", + _("Process only 1 aquarium now (ignore the rest)"), ) ) else: alternatives.append( ( LiveCameraMode.SEQUENTIAL_AQUARIUM, - f"Dividir em {requested_aquariums} sessões separadas", + _("Split into {count} separate sessions").format(count=requested_aquariums), ) ) alternatives.append( ( LiveCameraMode.RECORD_ONLY, - "Gravar sem detecção (processar offline depois)", + _("Record without detection (process offline later)"), ) ) warnings = [ - f"⚠️ Sistema não suporta {requested_aquariums} aquários simultaneamente.", - f"Máximo suportado: {max_supported} aquário(s).", + _("⚠️ The system does not support {count} aquariums simultaneously.").format( + count=requested_aquariums + ), + # Two msgids instead of "aquário(s)": max_supported is 1 in the + # common case, which is exactly when the parenthetical reads + # worst. No ngettext — the pair files carry no plural forms. + _("Maximum supported: 1 aquarium.") + if max_supported == 1 + else _("Maximum supported: {count} aquariums.").format(count=max_supported), ] return LiveCameraModeRecommendation( @@ -204,22 +224,24 @@ def recommend_mode( if not can_realtime: recommended_mode = LiveCameraMode.RECORD_ONLY - reason = ( - "Sistema insuficiente para processamento em tempo real. " - "Recomendado: gravar vídeo e processar offline." + reason = _( + "The system is not sufficient for real-time processing. " + "Recommended: record the video and process it offline." ) warnings = [ - "⚠️ HARDWARE INSUFICIENTE para detecção em tempo real.", - f"CPU: {hardware_report.cpu_cores} cores (mínimo 2)", - f"RAM: {hardware_report.available_memory_gb:.1f}GB disponível (mínimo 4GB)", + _("⚠️ INSUFFICIENT HARDWARE for real-time detection."), + _("CPU: {cores} cores (minimum 2)").format(cores=hardware_report.cpu_cores), + _("RAM: {gb:.1f}GB available (minimum 4GB)").format( + gb=hardware_report.available_memory_gb + ), ] # Only viable alternative is to abort alternatives.append( ( LiveCameraMode.RECORD_ONLY, - "Gravar vídeo sem detecção (única opção viável)", + _("Record video without detection (only viable option)"), ) ) @@ -239,7 +261,7 @@ def recommend_mode( requested_aquariums=requested_aquariums, max_aquariums_supported=max_supported, can_process_realtime=can_realtime, - reason="Modo padrão selecionado.", + reason=_("Default mode selected."), alternative_options=[], warnings=[], ) @@ -265,7 +287,9 @@ def create_sequential_session_plan( "aquarium_index": aq_idx, "aquarium_count_total": total_aquariums, "mode": LiveCameraMode.SINGLE_AQUARIUM_REALTIME, - "notes": f"Sessão {aq_idx + 1} de {total_aquariums} (aquário individual)", + "notes": _("Session {index} of {total} (individual aquarium)").format( + index=aq_idx + 1, total=total_aquariums + ), } plan.append(session) @@ -285,14 +309,16 @@ def get_mode_description(mode: LiveCameraMode) -> str: mode: Live camera mode Returns: - Portuguese description + Description in the active interface language """ + # Built inside the function, never at module level: a dict of _() calls in + # a module body freezes the translation at import time. descriptions = { - LiveCameraMode.MULTI_AQUARIUM_REALTIME: ( - "Processar múltiplos aquários simultaneamente em tempo real" + LiveCameraMode.MULTI_AQUARIUM_REALTIME: _( + "Process multiple aquariums simultaneously in real time" ), - LiveCameraMode.SINGLE_AQUARIUM_REALTIME: "Processar um aquário em tempo real", - LiveCameraMode.RECORD_ONLY: "Gravar vídeo sem detecção (processar offline depois)", - LiveCameraMode.SEQUENTIAL_AQUARIUM: "Gravar múltiplas sessões, uma por aquário", + LiveCameraMode.SINGLE_AQUARIUM_REALTIME: _("Process one aquarium in real time"), + LiveCameraMode.RECORD_ONLY: _("Record video without detection (process offline later)"), + LiveCameraMode.SEQUENTIAL_AQUARIUM: _("Record multiple sessions, one per aquarium"), } - return descriptions.get(mode, "Modo desconhecido") + return descriptions.get(mode, _("Unknown mode")) diff --git a/src/zebtrack/core/recording/live_session_manager.py b/src/zebtrack/core/recording/live_session_manager.py index 51a7ce90..dc6761a2 100644 --- a/src/zebtrack/core/recording/live_session_manager.py +++ b/src/zebtrack/core/recording/live_session_manager.py @@ -17,6 +17,8 @@ import structlog +from zebtrack.i18n import _ + if TYPE_CHECKING: from zebtrack.core.detection.multi_aquarium_detector import MultiAquariumDetector from zebtrack.core.main_view_model import MainViewModel @@ -328,27 +330,27 @@ def start_session( # noqa: C901 # Show initialization status if self.preview_window: - self.preview_window.update_status_text("⏳ Aquecendo câmera...", color="orange") + self.preview_window.update_status_text(_("⏳ Warming up camera..."), color="orange") # Setup camera if not self._setup_camera(camera_index): import os if os.environ.get("PYTEST_CURRENT_TEST") is None: - error_msg = ( - f"Falha ao abrir câmera {camera_index}.\n\n" - f"Possíveis causas:\n" - f"• Câmera está em uso por outro programa\n" - f"• Hardware com defeito\n" - f"• Driver incompatível\n\n" - f"Tente:\n" - f"• Fechar outros programas de câmera\n" - f"• Reconectar o dispositivo USB\n" - f"• Selecionar outra câmera" - ) + error_msg = _( + "Failed to open camera {index}.\n\n" + "Possible causes:\n" + "• The camera is in use by another program\n" + "• Faulty hardware\n" + "• Incompatible driver\n\n" + "Try:\n" + "• Closing other camera programs\n" + "• Reconnecting the USB device\n" + "• Selecting a different camera" + ).format(index=camera_index) import tkinter.messagebox as messagebox - messagebox.showerror("Erro na Câmera", error_msg) + messagebox.showerror(_("Camera Error"), error_msg) return False # Store camera properties for later use (post-analysis) @@ -387,7 +389,7 @@ def start_session( # noqa: C901 # Show detector setup status if self.preview_window: - self.preview_window.update_status_text("⏳ Carregando detector...", color="orange") + self.preview_window.update_status_text(_("⏳ Loading detector..."), color="orange") resolved_weight, resolved_openvino, resolution_source = ( self._resolve_session_detector_config() @@ -454,7 +456,9 @@ def start_session( # noqa: C901 use_openvino=resolved_openvino, ) - success, _ = self.detector_service.initialize_detector( + # NOT `success, _ =`: binding `_` locally shadows the gettext alias + # for the WHOLE function, including the calls above it. + success, _detector = self.detector_service.initialize_detector( animal_method=animal_method, use_openvino=resolved_openvino, perspective=perspective, @@ -577,7 +581,7 @@ def start_session( # noqa: C901 countdown_seconds = int(countdown_duration_s) if self.preview_window: self.preview_window.update_status_text( - f"⏳ Iniciando em {countdown_seconds}s...", + _("⏳ Starting in {seconds}s...").format(seconds=countdown_seconds), color="yellow", ) @@ -608,7 +612,7 @@ def _on_countdown_complete() -> None: # Show thread startup status if self.preview_window: - self.preview_window.update_status_text("⏳ Iniciando captura...", color="orange") + self.preview_window.update_status_text(_("⏳ Starting capture..."), color="orange") # Start threads before recording service if not self._start_threads(): @@ -684,9 +688,11 @@ def _on_countdown_complete() -> None: # Update status if self.preview_window: if self._aquarium_detection_phase: - self.preview_window.update_status_text("🔍 Procurando aquário...", color="yellow") + self.preview_window.update_status_text( + _("🔍 Looking for the aquarium..."), color="yellow" + ) else: - self.preview_window.update_status_text("⏳ Aguardando vídeo...", color="orange") + self.preview_window.update_status_text(_("⏳ Waiting for video..."), color="orange") log.info("live_camera_service.session_started", output_dir=str(output_dir)) return True @@ -1022,7 +1028,7 @@ def _on_session_active(self) -> None: if self.preview_window: self.preview_window.start_timer() - self.preview_window.update_status_text("● Gravando", color="red") + self.preview_window.update_status_text(_("● Recording"), color="red") else: log.info( "live_camera_service.timer_delayed_for_aquarium_detection", @@ -1111,8 +1117,12 @@ def _publish_video_drop_status(self) -> None: from zebtrack.ui.payloads import StatusPayload message = ( - f"⚠️ Vídeo: {self._dropped_frames_video} frame(s) descartado(s) — " - "verifique o disco / OneDrive (gravação pode ter falhas)" + _("⚠️ Video: 1 frame dropped — check the disk / OneDrive (the recording may have gaps)") + if self._dropped_frames_video == 1 + else _( + "⚠️ Video: {count} frames dropped — check the disk / OneDrive " + "(the recording may have gaps)" + ).format(count=self._dropped_frames_video) ) log.warning( "live_camera_service.video_drop_status", @@ -1135,13 +1145,17 @@ def _publish_analysis_lag_status(self, lag_seconds: float) -> None: from zebtrack.ui.payloads import StatusPayload if lag_seconds < 2.0: - status_msg = f"⏳ Analisando... ({lag_seconds:.1f}s atrás) - Gravação OK" + status_msg = _("⏳ Analysing... ({seconds:.1f}s behind) - Recording OK").format( + seconds=lag_seconds + ) elif lag_seconds < 5.0: - status_msg = f"⏳ Análise atrasada ({lag_seconds:.1f}s) - Gravação continua normalmente" + status_msg = _( + "⏳ Analysis is behind ({seconds:.1f}s) - Recording continues normally" + ).format(seconds=lag_seconds) else: - status_msg = ( - f"⚠️ Análise muito atrasada ({lag_seconds:.1f}s) - Gravação OK, análise em fila" - ) + status_msg = _( + "⚠️ Analysis is far behind ({seconds:.1f}s) - Recording OK, analysis queued" + ).format(seconds=lag_seconds) log.debug( "live_camera_service.analysis_lag_status", diff --git a/src/zebtrack/core/recording/recording_service.py b/src/zebtrack/core/recording/recording_service.py index 600a0c91..8e1f5941 100644 --- a/src/zebtrack/core/recording/recording_service.py +++ b/src/zebtrack/core/recording/recording_service.py @@ -23,6 +23,8 @@ import structlog +from zebtrack.i18n import _ + if TYPE_CHECKING: from tkinter import Misc @@ -149,8 +151,8 @@ def start_session( if camera_width is None or camera_height is None: self._show_error( - "Erro", - "Configuração da câmera indisponível para iniciar a gravação.", + _("Error"), + _("Camera configuration unavailable to start the recording."), ) self._update_button_state("start_rec", "normal") return @@ -202,7 +204,7 @@ def start_session( ) if not recording_started: - self._show_error("Erro", "Não foi possível iniciar a gravação.") + self._show_error(_("Error"), _("Could not start the recording.")) self._update_button_state("start_rec", "normal") self._update_button_state("stop_rec", "disabled") return diff --git a/src/zebtrack/core/services/arduino_ack_semantics.py b/src/zebtrack/core/services/arduino_ack_semantics.py index 91de4de0..d819beb6 100644 --- a/src/zebtrack/core/services/arduino_ack_semantics.py +++ b/src/zebtrack/core/services/arduino_ack_semantics.py @@ -23,6 +23,8 @@ import re from typing import Literal +from zebtrack.i18n import _ + AckState = Literal["on", "off"] # Word-anchored so "ON" does not match inside "COMMAND" and, crucially, so @@ -74,9 +76,15 @@ def edge_ack_is_inverted(edge: str | None, ack_text: str | None) -> bool: def describe_inversion(roi: str | None, edge: str | None, token: int | None, ack_text: str) -> str: """Human-readable one-liner for logs and the bindings panel.""" - expected = "ligar" if edge == "enter" else "desligar" - edge_pt = "entrada" if edge == "enter" else "saída" - return ( - f"{roi} {edge_pt} → token {token} → o firmware respondeu " - f'"{ack_text}", mas uma {edge_pt} deveria {expected}' - ) + # Two COMPLETE sentences chosen by `edge` instead of splicing the same + # noun into a template twice. The Portuguese got away with it because + # "entrada" and "saída" are both feminine; no other language owes us that. + if edge == "enter": + return _( + '{roi} enter → token {token} → the firmware replied "{ack}", ' + "but an enter should switch it ON" + ).format(roi=roi, token=token, ack=ack_text) + return _( + '{roi} exit → token {token} → the firmware replied "{ack}", ' + "but an exit should switch it OFF" + ).format(roi=roi, token=token, ack=ack_text) diff --git a/src/zebtrack/core/services/arduino_bindings.py b/src/zebtrack/core/services/arduino_bindings.py index 2e5c3225..c2918899 100644 --- a/src/zebtrack/core/services/arduino_bindings.py +++ b/src/zebtrack/core/services/arduino_bindings.py @@ -22,6 +22,8 @@ import structlog from pydantic import BaseModel, ConfigDict, Field, field_validator +from zebtrack.i18n import _ + log = structlog.get_logger() # Key under which bindings are stored inside ``project_data``. @@ -49,9 +51,10 @@ class TokenConflict(NamedTuple): def describe(self) -> str: """Human-readable one-liner for logs and the UI status line.""" - return ( - f"token {self.token}: entrada de {', '.join(self.enter_rois)} " - f"e saída de {', '.join(self.exit_rois)}" + return _("token {token}: enter for {enter_rois} and exit for {exit_rois}").format( + token=self.token, + enter_rois=", ".join(self.enter_rois), + exit_rois=", ".join(self.exit_rois), ) diff --git a/src/zebtrack/core/services/detector_service.py b/src/zebtrack/core/services/detector_service.py index f2d3cd2b..c3ecee70 100644 --- a/src/zebtrack/core/services/detector_service.py +++ b/src/zebtrack/core/services/detector_service.py @@ -18,6 +18,7 @@ from zebtrack.core.detection import Detector, MultiAquariumZoneData, ZoneData from zebtrack.core.detection.detection_post_processor import DetectionPostProcessor from zebtrack.core.detection.zone_scaler import ZoneScaler +from zebtrack.i18n import _ from zebtrack.settings import save_settings from zebtrack.utils import IntegrityError @@ -127,14 +128,18 @@ def initialize_detector( ) if not model_path: - error_msg = f"Nenhum modelo {animal_method} está disponível para detecção de animais." + error_msg = _("No {method} model is available for animal detection.").format( + method=animal_method + ) log.error("detector_service.no_model_path", error=error_msg) return False, error_msg # Find weight and get correct model path weight_name, weight_details = self.model_service.find_weight_by_path(model_path) if not weight_name: - error_msg = f"Não foi possível encontrar o peso correspondente ao caminho: {model_path}" + error_msg = _("Could not find the weight matching the path: {path}").format( + path=model_path + ) log.error("detector_service.weight_not_found", error=error_msg) return False, error_msg @@ -147,14 +152,16 @@ def initialize_detector( ) if not final_model_path: raise ValueError( - "Caminho do modelo OpenVINO não encontrado ou inválido. " - "Por favor, converta o modelo primeiro." + _( + "OpenVINO model path not found or invalid. " + "Please convert the model first." + ) ) model_path = final_model_path else: plugin_name = "YOLO (Ultralytics)" if not os.path.exists(model_path): - raise ValueError("Caminho do modelo YOLO .pt não encontrado ou inválido.") + raise ValueError(_("YOLO .pt model path not found or invalid.")) # Get plugin class if detector_plugins is None: @@ -500,14 +507,18 @@ def _validate_range( if value is not None: 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 + # inside it, which meant the except had to tell a range violation + # from a parse failure by matching the message prose + # (`"deve estar entre" in str(e)`) — a branch that translating this + # very line would have broken in silence, and in the worst + # direction: a range error re-raised as "is not a number". + if val < min_val or val > max_val: + raise ValueError( + f"{param_name} must be between {min_val} and {max_val}, got {val}" + ) # Validate parameters _validate_range("conf_threshold", params_dict.get("conf_threshold")) @@ -517,11 +528,17 @@ def _validate_range( _validate_range("iou_threshold", params_dict.get("iou_threshold")) if "track_buffer" in params_dict: + # Same shape as _validate_range above, and for the same reason: the + # `< 1` check used to sit INSIDE the try, so `except ValueError` + # caught the range violation it had just raised and relabelled it + # "must be an integer". Passing 0 — an integer — was answered with + # "track_buffer must be an integer". try: - if int(params_dict["track_buffer"]) < 1: - raise ValueError("track_buffer deve ser pelo menos 1") + track_buffer_value = int(params_dict["track_buffer"]) 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 + if track_buffer_value < 1: + raise ValueError(f"track_buffer must be at least 1, got {track_buffer_value}") plugin = self.detector.plugin if self.detector else None clear_project_overrides = scope_normalized == "project" and reset_overrides diff --git a/src/zebtrack/core/services/model_override_service.py b/src/zebtrack/core/services/model_override_service.py index 85eb34c1..e0a3d392 100644 --- a/src/zebtrack/core/services/model_override_service.py +++ b/src/zebtrack/core/services/model_override_service.py @@ -14,6 +14,7 @@ import structlog +from zebtrack.i18n import _ from zebtrack.ui import payloads if TYPE_CHECKING: @@ -154,8 +155,8 @@ def copy_global_model_settings_to_project( self._publish_event( UIEvents.UI_SHOW_WARNING, payloads.MessagePayload( - title="Nenhum Projeto", - message="Abra um projeto antes de copiar configurações globais.", + title=_("No Project"), + message=_("Open a project before copying the global settings."), ), ) return None @@ -191,7 +192,7 @@ def copy_global_model_settings_to_project( if apply_runtime_callback is not None: apply_runtime_callback(resolved_weight, resolved_openvino) - message = "Configurações globais aplicadas ao projeto." + message = _("Global settings applied to the project.") self._publish_event(UIEvents.UI_SET_STATUS, payloads.StatusPayload(message=message)) if refresh_callback: @@ -308,7 +309,7 @@ def copy_global_model_settings_to_project_path( self._publish_event( UIEvents.UI_SET_STATUS, payloads.StatusPayload( - message=f"Configurações globais aplicadas ao projeto em {target}." + message=_("Global settings applied to the project at {path}.").format(path=target) ), ) return resolved_weight, use_openvino @@ -348,8 +349,8 @@ def save_current_calibration_to_project( self._publish_event( UIEvents.UI_SHOW_WARNING, payloads.MessagePayload( - title="Nenhum Projeto", - message="Abra um projeto antes de salvar overrides de calibração.", + title=_("No Project"), + message=_("Open a project before saving calibration overrides."), ), ) return None @@ -366,7 +367,7 @@ def save_current_calibration_to_project( use_openvino_setter=lambda v: None, # Will be set by caller ) - message = "Overrides do projeto atualizados a partir desta calibração." + message = _("Project overrides updated from this calibration.") self._publish_event(UIEvents.UI_SET_STATUS, payloads.StatusPayload(message=message)) if refresh_callback: diff --git a/src/zebtrack/core/services/model_service.py b/src/zebtrack/core/services/model_service.py index 7ee1f641..8547b09c 100644 --- a/src/zebtrack/core/services/model_service.py +++ b/src/zebtrack/core/services/model_service.py @@ -15,6 +15,8 @@ import structlog +from zebtrack.i18n import _ + if TYPE_CHECKING: from zebtrack.core.services.weight_manager import WeightManager @@ -110,20 +112,20 @@ def get_openvino_status(self, weight_name: str, use_openvino: bool) -> str: str: Human-readable status message """ if not weight_name: - return "Nenhum peso selecionado." + return _("No weight selected.") details = self.weight_manager.get_weight_details(weight_name) if not details: - return "Detalhes do peso não encontrados." + return _("Weight details not found.") if use_openvino: openvino_path = details.get("openvino_path") if openvino_path and Path(openvino_path).exists(): - return "O modelo OpenVINO está pronto." + return _("The OpenVINO model is ready.") else: - return "Necessita de conversão para OpenVINO." + return _("Conversion to OpenVINO is required.") else: - return "O OpenVINO está desativado." + return _("OpenVINO is disabled.") def list_available_weights(self) -> list[str]: """ @@ -485,7 +487,9 @@ def inspect_model(self, weight_name: str) -> dict[str, Any]: """ details = self.weight_manager.get_weight_details(weight_name) if not details: - raise ValueError(f"Peso '{weight_name}' não encontrado na configuração.") + raise ValueError( + _("Weight '{name}' not found in the configuration.").format(name=weight_name) + ) model_path = details.get("path") weight_type = details.get("type", "unknown") diff --git a/src/zebtrack/core/services/weight_manager.py b/src/zebtrack/core/services/weight_manager.py index 21331006..d1f2ebd2 100644 --- a/src/zebtrack/core/services/weight_manager.py +++ b/src/zebtrack/core/services/weight_manager.py @@ -14,6 +14,7 @@ import structlog +from zebtrack.i18n import _ from zebtrack.settings import Settings from zebtrack.utils import calculate_sha256 @@ -591,7 +592,7 @@ def _coerce_path(candidate: Any, *, source: str) -> str | None: legacy_type = self._classify_weight_type(legacy_name) # Add legacy path if it's not already in potential_weights legacy_already_added = any( - filename == legacy_path for _, filename, *_ in potential_weights + filename == legacy_path for _label, filename, *_rest in potential_weights ) if not legacy_already_added: potential_weights.append((legacy_type or "seg", legacy_path, None)) @@ -667,7 +668,9 @@ def save_weights(self) -> None: log.info("weights.config.saved", path=self.config_path) except OSError as e: log.error("weights.config.save_error", error=str(e)) - raise OSError(f"Não foi possível salvar o arquivo de configuração de pesos: {e}") from e + raise OSError( + _("Could not save the weights configuration file: {error}").format(error=e) + ) from e def get_all_weights(self) -> list[str]: """Return a list of names of all available weights.""" @@ -868,11 +871,17 @@ def add_weight( ) else: raise ValueError( - f"Um arquivo de peso com o nome '{model_path.name}' já existe " - f"no diretório de pesos.\n\n" - f"Arquivo existente: {target_path}\n" - f"Arquivo sendo adicionado: {model_path}\n\n" - f"Por favor, renomeie um dos arquivos antes de adicionar." + _( + "A weight file named '{name}' already exists in the " + "weights directory.\n\n" + "Existing file: {existing}\n" + "File being added: {incoming}\n\n" + "Please rename one of them before adding." + ).format( + name=model_path.name, + existing=target_path, + incoming=model_path, + ) ) else: shutil.copy2(model_path, target_path) @@ -884,18 +893,24 @@ def add_weight( ) except OSError as e: log.error("weights.add.external_file.copy_failed", error=str(e)) - raise ValueError(f"Falha ao copiar o arquivo de peso externo: {e}") from e + raise ValueError( + _("Failed to copy the external weight file: {error}").format(error=e) + ) from e except FileNotFoundError: log.error("weights.add.not_found", path=new_path) - raise FileNotFoundError(f"O arquivo de modelo não foi encontrado: {new_path}") from None + raise FileNotFoundError( + _("The model file was not found: {path}").format(path=new_path) + ) from None except OSError as e: log.error("weights.add.invalid_path", path=new_path, error=str(e)) - raise ValueError(f"O caminho do modelo é inválido ou inacessível: {e}") from e + raise ValueError( + _("The model path is invalid or inaccessible: {error}").format(error=e) + ) from e # --- End Security Check --- new_name = os.path.basename(model_path) if new_name in self.weights: - raise ValueError(f"Um peso com o nome '{new_name}' já existe.") + raise ValueError(_("A weight named '{name}' already exists.").format(name=new_name)) # Determine weight type if weight_type is None: @@ -917,7 +932,7 @@ def add_weight( if set_as_default: # Unset legacy global default - _, current_default = self.get_default_weight() + _name, current_default = self.get_default_weight() if current_default: current_default["is_default"] = False @@ -958,11 +973,11 @@ def delete_weight(self, name_to_delete: str) -> None: """Delete a weight from the configuration.""" if name_to_delete not in self.weights: log.warning("weights.delete.not_found", name=name_to_delete) - raise ValueError(f"Peso '{name_to_delete}' não encontrado.") + raise ValueError(_("Weight '{name}' not found.").format(name=name_to_delete)) if len(self.weights) <= 1: log.error("weights.delete.last_weight", name=name_to_delete) - raise ValueError("Você não pode excluir o último peso disponível.") + raise ValueError(_("You cannot delete the last available weight.")) details = self.weights[name_to_delete] was_default = details.get("is_default") @@ -1408,8 +1423,11 @@ def convert_to_openvino(self, name: str) -> str | None: "class_names": class_names, "task": "segment", "weight_type": "seg", + # WRITTEN to metadata.json — English like every other value in + # this dict. A description that changed with ui.language would + # give the same converted model a different file per machine. "description": ( - "Modelo de segmentação convertido (classes extraídas do modelo)" + "Converted segmentation model (class names extracted from the model)" ), "original_model": os.path.basename(pt_path), "conversion_date": time.strftime("%Y-%m-%d %H:%M:%S"), @@ -1421,7 +1439,9 @@ def convert_to_openvino(self, name: str) -> str | None: "class_names": class_names, "task": "detect", "weight_type": "det", - "description": "Modelo de detecção convertido (classes extraídas do modelo)", + "description": ( + "Converted detection model (class names extracted from the model)" + ), "original_model": os.path.basename(pt_path), "conversion_date": time.strftime("%Y-%m-%d %H:%M:%S"), } @@ -1445,12 +1465,15 @@ def convert_to_openvino(self, name: str) -> str | None: # Clean up the corrupted cache dir shutil.rmtree(cached_model_dir, ignore_errors=True) details["openvino_status"] = OPENVINO_STATUS_FAILED + # STORED in the weights config (English, like the rest of that + # file) vs. RAISED to the operator (translated). Same sentence, + # two different jobs. details["last_conversion_error"] = ( - "Arquivo .xml do modelo OpenVINO não encontrado após a conversão." + "OpenVINO model .xml file not found after the conversion." ) self.save_weights() raise OpenVINOExportError( - "Arquivo .xml do modelo OpenVINO não encontrado após a conversão." + _("OpenVINO model .xml file not found after the conversion.") ) xml_path = xml_files[0] @@ -1565,7 +1588,7 @@ def convert_to_openvino_int8(self, name: str) -> str | None: "task": "segment" if weight_type == "seg" else "detect", "weight_type": weight_type, "quantization": "INT8", - "description": f"Modelo {weight_type} convertido com quantização INT8", + "description": f"Converted {weight_type} model with INT8 quantisation", "original_model": os.path.basename(pt_path), "conversion_date": time.strftime("%Y-%m-%d %H:%M:%S"), } @@ -1587,12 +1610,10 @@ def convert_to_openvino_int8(self, name: str) -> str | None: shutil.rmtree(cached_model_dir, ignore_errors=True) details["openvino_int8_status"] = OPENVINO_STATUS_FAILED details["last_conversion_error"] = ( - "Arquivo .xml do modelo INT8 não encontrado após conversão." + "INT8 model .xml file not found after the conversion." ) self.save_weights() - raise OpenVINOExportError( - "Arquivo .xml do modelo INT8 não encontrado após conversão." - ) + raise OpenVINOExportError(_("INT8 model .xml file not found after the conversion.")) xml_path = xml_files[0] model_hash = calculate_sha256(str(xml_path)) diff --git a/src/zebtrack/core/services/wizard_service.py b/src/zebtrack/core/services/wizard_service.py index 9e75c404..bb93aedc 100644 --- a/src/zebtrack/core/services/wizard_service.py +++ b/src/zebtrack/core/services/wizard_service.py @@ -20,6 +20,7 @@ import serial.tools.list_ports import structlog +from zebtrack.i18n import _ from zebtrack.io.arduino import Arduino from zebtrack.utils.cache import TTLCache @@ -65,38 +66,44 @@ def validate_live_config(data: dict[str, Any]) -> tuple[bool, str]: # Camera validation camera_index = data.get("camera_index") if camera_index is None or not isinstance(camera_index, int): - return (False, "Índice de câmera inválido") + return (False, _("Invalid camera index")) if camera_index < 0 or camera_index > 10: - return (False, "Índice de câmera deve estar entre 0 e 10") + return (False, _("The camera index must be between 0 and 10")) # Arduino validation if data.get("use_arduino", False): arduino_port = data.get("arduino_port") if not arduino_port: - return (False, "Porta Arduino deve ser especificada quando Arduino está ativado") + return ( + False, + _("The Arduino port must be specified when Arduino is enabled"), + ) # External trigger validation if data.get("external_trigger_mode", False): if not data.get("use_arduino", False): return ( False, - "Modo de trigger externo requer Arduino ativado", + _("External trigger mode requires Arduino to be enabled"), ) # Timed recording validation if data.get("use_timed_recording", False): duration = data.get("recording_duration_s", 0) if not isinstance(duration, int | float) or duration <= 0: - return (False, "Duração de gravação deve ser maior que zero") + return (False, _("The recording duration must be greater than zero")) if duration > 7200: # 2 hours max - return (False, "Duração de gravação não pode exceder 2 horas (7200 segundos)") + return ( + False, + _("The recording duration cannot exceed 2 hours (7200 seconds)"), + ) # Countdown validation if data.get("use_countdown", False): countdown = data.get("countdown_duration_s", 0) if not isinstance(countdown, int) or countdown < 1 or countdown > 60: - return (False, "Contagem regressiva deve estar entre 1 e 60 segundos") + return (False, _("The countdown must be between 1 and 60 seconds")) return (True, "") @@ -366,11 +373,15 @@ def try_read( friendly_name = friendly_names[i] if i < len(friendly_names) else "" if friendly_name: - description = f"{friendly_name} [índice {i}] - {resolution_desc}" + description = _("{name} [index {index}] - {resolution}").format( + name=friendly_name, index=i, resolution=resolution_desc + ) else: # Fallback: numbered description (non-Windows or pygrabber missing) camera_count = len(cameras) + 1 - description = f"Câmera #{camera_count} [índice {i}] - {resolution_desc}" + description = _( + "Camera #{number} [index {index}] - {resolution}" + ).format(number=camera_count, index=i, resolution=resolution_desc) cameras.append( { @@ -666,12 +677,12 @@ def validate_experimental_design(data: dict[str, Any]) -> tuple[bool, str]: # Days validation days = data.get("experiment_days") if not isinstance(days, int) or days < 1 or days > 365: - return (False, "Número de dias deve estar entre 1 e 365") + return (False, _("The number of days must be between 1 and 365")) # Groups validation num_groups = data.get("num_groups") if not isinstance(num_groups, int) or num_groups < 1 or num_groups > 6: - return (False, "Número de grupos deve estar entre 1 e 6") + return (False, _("The number of groups must be between 1 and 6")) # Subjects validation subjects = data.get("subjects_per_group") @@ -717,21 +728,24 @@ def validate_calibration_data(data: dict[str, Any]) -> tuple[bool, str]: # Aquariums validation num_aquariums = data.get("num_aquariums") if not isinstance(num_aquariums, int) or num_aquariums < 1 or num_aquariums > 100: - return (False, "Número de aquários deve estar entre 1 e 100") + return (False, _("The number of aquariums must be between 1 and 100")) # Animals per aquarium validation animals = data.get("animals_per_aquarium") if not isinstance(animals, int) or animals < 1 or animals > 100: - return (False, "Número de animais por aquário deve estar entre 1 e 100") + return ( + False, + _("The number of animals per aquarium must be between 1 and 100"), + ) # Dimensions validation width = data.get("aquarium_width_cm") if not isinstance(width, int | float) or width <= 0: - return (False, "Largura do aquário deve ser maior que zero") + return (False, _("The aquarium width must be greater than zero")) height = data.get("aquarium_height_cm") if not isinstance(height, int | float) or height <= 0: - return (False, "Altura do aquário deve ser maior que zero") + return (False, _("The aquarium height must be greater than zero")) # Intervals validation analysis_interval = data.get("analysis_interval_frames") @@ -740,11 +754,11 @@ def validate_calibration_data(data: dict[str, Any]) -> tuple[bool, str]: or analysis_interval < 1 or analysis_interval > 30 ): - return (False, "Intervalo de análise deve estar entre 1 e 30 frames") + return (False, _("The analysis interval must be between 1 and 30 frames")) display_interval = data.get("display_interval_frames") if not isinstance(display_interval, int) or display_interval < 1 or display_interval > 30: - return (False, "Intervalo de exibição deve estar entre 1 e 30 frames") + return (False, _("The display interval must be between 1 and 30 frames")) # ROI inclusion rule validation valid_rules = [ @@ -757,7 +771,9 @@ def validate_calibration_data(data: dict[str, Any]) -> tuple[bool, str]: if roi_rule not in valid_rules: return ( False, - f"Regra de inclusão ROI inválida. Deve ser uma de: {', '.join(valid_rules)}", + _("Invalid ROI inclusion rule. It must be one of: {rules}").format( + rules=", ".join(valid_rules) + ), ) return (True, "") @@ -783,27 +799,33 @@ def validate_basic_calibration(data: dict[str, Any]) -> tuple[bool, str]: # Aquariums validation num_aquariums = data.get("num_aquariums") if not isinstance(num_aquariums, int) or num_aquariums < 1: - return (False, "O número de aquários deve ser pelo menos 1") + return (False, _("The number of aquariums must be at least 1")) if num_aquariums > 100: - return (False, "O número de aquários não pode exceder 100") + return (False, _("The number of aquariums cannot exceed 100")) # Animals per aquarium validation animals = data.get("animals_per_aquarium") if not isinstance(animals, int) or animals < 1: - return (False, "O número de animais por aquário deve ser pelo menos 1") + return ( + False, + _("The number of animals per aquarium must be at least 1"), + ) if animals > 100: - return (False, "O número de animais por aquário não pode exceder 100") + return ( + False, + _("The number of animals per aquarium cannot exceed 100"), + ) # Dimensions validation width = data.get("aquarium_width_cm") if not isinstance(width, int | float) or width <= 0: - return (False, "A largura do aquário deve ser maior que zero") + return (False, _("The aquarium width must be greater than zero")) height = data.get("aquarium_height_cm") if not isinstance(height, int | float) or height <= 0: - return (False, "A altura do aquário deve ser maior que zero") + return (False, _("The aquarium height must be greater than zero")) return (True, "") @@ -849,7 +871,7 @@ def validate_multi_aquarium_config( # noqa: C901 else getattr(config, "enabled", False) ) except (AttributeError, TypeError): - errors.append("Configuração multi-aquário inválida") + errors.append(_("Invalid multi-aquarium configuration")) return False, errors, warnings else: enabled = config.enabled @@ -871,7 +893,7 @@ def validate_multi_aquarium_config( # noqa: C901 regex_subject_field = config.get("regex_subject_field", "subject") if len(aquarium_configs) != 2: - errors.append("Exatamente 2 aquários devem ser configurados") + errors.append(_("Exactly 2 aquariums must be configured")) # Phase 3.2: Check aquarium configurations for potential issues for i, aq_config in enumerate(aquarium_configs): @@ -894,8 +916,10 @@ def validate_multi_aquarium_config( # noqa: C901 ) if area < 10000: # Less than ~100x100 pixels warnings.append( - f"Aquário {i} tem área muito pequena ({int(area)} px²). " - "Pode afetar precisão da detecção." + _( + "Aquarium {index} has a very small area ({area} px²). " + "It may affect detection accuracy." + ).format(index=i, area=int(area)) ) # Check for polygon overlap (only if 2 aquariums) @@ -921,8 +945,10 @@ def validate_multi_aquarium_config( # noqa: C901 # Check for intersection if not (x1 + w1 < x2 or x2 + w2 < x1 or y1 + h1 < y2 or y2 + h2 < y1): warnings.append( - "Polígonos dos aquários parecem se sobrepor. " - "Isso pode causar detecções duplicadas." + _( + "The aquarium polygons appear to overlap. " + "This may cause duplicate detections." + ) ) # Check 2: Validate regex if provided @@ -935,7 +961,11 @@ def validate_multi_aquarium_config( # noqa: C901 expected = {regex_group_field, regex_subject_field} missing = expected - groups if missing: - errors.append(f"Regex não captura campos esperados: {missing}") + errors.append( + _("The regex does not capture the expected fields: {fields}").format( + fields=missing + ) + ) # Check 3: Test against sample filenames if sample_filenames: @@ -947,11 +977,13 @@ def validate_multi_aquarium_config( # noqa: C901 if unmatched: errors.append( - f"Regex não corresponde a arquivos: {', '.join(unmatched[:2])}" + _("The regex does not match these files: {files}").format( + files=", ".join(unmatched[:2]) + ) ) except re.error as e: - errors.append(f"Padrão regex inválido: {e}") + errors.append(_("Invalid regex pattern: {error}").format(error=e)) log.debug( "wizard_service.validate_multi_aquarium_config", diff --git a/src/zebtrack/core/video/analysis_pipeline_runner.py b/src/zebtrack/core/video/analysis_pipeline_runner.py index d9b7462c..bdcb9ea8 100644 --- a/src/zebtrack/core/video/analysis_pipeline_runner.py +++ b/src/zebtrack/core/video/analysis_pipeline_runner.py @@ -45,6 +45,7 @@ DEFAULT_SOCIAL_RADIUS_CM, SocialAnalysisOutcome, ) +from zebtrack.i18n import _ from zebtrack.ui import payloads from zebtrack.ui.event_bus_v2 import Event, UIEvents @@ -692,8 +693,8 @@ def _run_analysis_pipeline( Event( type=UIEvents.SHOW_ERROR, data=payloads.ErrorOccurredPayload( - title="Erro de Processamento", - message="Dados de calibração incompletos.", + title=_("Processing Error"), + message=_("Incomplete calibration data."), ), ) ) @@ -726,8 +727,8 @@ def _run_analysis_pipeline( Event( type=UIEvents.SHOW_ERROR, data=payloads.ErrorOccurredPayload( - title="Erro de Processamento", - message="Falha ao preparar dados de calibração.", + title=_("Processing Error"), + message=_("Failed to prepare the calibration data."), ), ) ) diff --git a/src/zebtrack/core/video/processing_mode.py b/src/zebtrack/core/video/processing_mode.py index 74062b2a..5090c56d 100644 --- a/src/zebtrack/core/video/processing_mode.py +++ b/src/zebtrack/core/video/processing_mode.py @@ -9,6 +9,8 @@ from dataclasses import dataclass from enum import StrEnum +from zebtrack.i18n import _ + class ProcessingMode(StrEnum): """Enumerates the tracking pipelines available to the application.""" @@ -24,8 +26,8 @@ def display_name(self) -> str: Localized string representation of the processing mode. """ if self is ProcessingMode.SINGLE_SUBJECT: - return "Individual" - return "Multi-indivíduos" + return _("Individual") + return _("Multi-individual") @dataclass(frozen=True) diff --git a/src/zebtrack/core/video/progress_notifier.py b/src/zebtrack/core/video/progress_notifier.py index 15586951..7319528a 100644 --- a/src/zebtrack/core/video/progress_notifier.py +++ b/src/zebtrack/core/video/progress_notifier.py @@ -23,6 +23,7 @@ from zebtrack.settings import Settings from zebtrack.ui.event_bus_v2 import EventBusV2 +from zebtrack.i18n import _ from zebtrack.ui.event_bus_v2 import Event, UIEvents from zebtrack.ui.payloads import ( AnalysisMetadataPayload, @@ -77,8 +78,10 @@ def progress_callback( if self.cancel_event.is_set(): return - overall_progress = f"Processando {index + 1}/{total_videos}: {experiment_id}" - step_status = f"Etapa: {status_message}" + overall_progress = _("Processing {current}/{total}: {experiment_id}").format( + current=index + 1, total=total_videos, experiment_id=experiment_id + ) + step_status = _("Step: {status}").format(status=status_message) self.ui_event_bus.publish( Event( diff --git a/src/zebtrack/core/video/tracking_session_runner.py b/src/zebtrack/core/video/tracking_session_runner.py index 5443d56e..667d1e98 100644 --- a/src/zebtrack/core/video/tracking_session_runner.py +++ b/src/zebtrack/core/video/tracking_session_runner.py @@ -32,6 +32,7 @@ from zebtrack.core.detection import ZoneData from zebtrack.core.detection.calibration import Calibration +from zebtrack.i18n import _ from zebtrack.ui import payloads as payloads from zebtrack.ui.event_bus_v2 import Event, UIEvents @@ -246,7 +247,11 @@ def _finalize_tracking_session( self.ui_event_bus.publish( Event( type=UIEvents.SET_STATUS, - data=payloads.StatusPayload(message=f"Trajetória para {experiment_id} gerada."), + data=payloads.StatusPayload( + message=_("Trajectory for {experiment_id} generated.").format( + experiment_id=experiment_id + ) + ), ) ) return True, arena_polygon diff --git a/src/zebtrack/core/video/video_context_factory.py b/src/zebtrack/core/video/video_context_factory.py index 707de62a..65b137d8 100644 --- a/src/zebtrack/core/video/video_context_factory.py +++ b/src/zebtrack/core/video/video_context_factory.py @@ -28,6 +28,7 @@ from zebtrack.settings import Settings from zebtrack.ui.event_bus_v2 import EventBusV2 +from zebtrack.i18n import _ from zebtrack.ui.event_bus_v2 import Event, UIEvents from zebtrack.ui.payloads import FrameDisplayPayload @@ -293,9 +294,11 @@ def _publish_error(payload: dict[str, str]) -> None: if not trajectory_path.exists(): _publish_error( { - "title": "Erro de Processamento", - "message": (f"Falha ao gerar arquivo de trajetória para {experiment_id}."), - "details": f"Arquivo não encontrado: {trajectory_path}", + "title": _("Processing Error"), + "message": _( + "Failed to generate the trajectory file for {experiment_id}." + ).format(experiment_id=experiment_id), + "details": _("File not found: {path}").format(path=trajectory_path), } ) return None @@ -311,8 +314,10 @@ def _publish_error(payload: dict[str, str]) -> None: ) _publish_error( { - "title": "Erro de Processamento", - "message": f"Falha ao ler arquivo de trajetória para {experiment_id}.", + "title": _("Processing Error"), + "message": _("Failed to read the trajectory file for {experiment_id}.").format( + experiment_id=experiment_id + ), "details": str(exc), } ) diff --git a/src/zebtrack/core/viewmodels/analysis_control_view_model.py b/src/zebtrack/core/viewmodels/analysis_control_view_model.py index 7a0b364d..cdcedf19 100644 --- a/src/zebtrack/core/viewmodels/analysis_control_view_model.py +++ b/src/zebtrack/core/viewmodels/analysis_control_view_model.py @@ -7,6 +7,7 @@ import structlog from zebtrack.analysis.analysis_service import resolve_mask_sidecar +from zebtrack.i18n import _ from zebtrack.ui import payloads as payloads from zebtrack.ui.event_bus_v2 import Event, UIEvents from zebtrack.ui.payloads import StatusPayload @@ -82,12 +83,18 @@ def start_single_video_workflow( Event( type=UIEvents.SHOW_ERROR, data=payloads.ErrorOccurredPayload( - title="Configuração Inválida", - message=( - f"O modo de detecção (det) suporta apenas 1 animal por aquário.\n" - f"Você configurou {animals_per_aquarium} animais por aquário.\n" - "Para múltiplos animais, use o modo de segmentação (seg)." - ), + # SAME msgid as project_workflow_service: this was a + # second, differently worded copy of one validation. + # Two msgids would mean the operator reads different + # advice depending on which path tripped the check. + title=_("Invalid Configuration"), + message=_( + "The detection mode (det) for animals is only compatible " + "with 1 animal per aquarium.\nCurrent configuration: {count} " + "animals per aquarium.\n\nTo use multiple animals per " + "aquarium, change the animal detection method to 'seg' " + "(segmentation) in the settings." + ).format(count=animals_per_aquarium), ), ) ) @@ -205,7 +212,9 @@ def cancel_current_analysis(self) -> None: self.ui_event_bus.publish( Event( type=UIEvents.SET_STATUS, - data=payloads.StatusPayload(message="Cancelando análise em andamento..."), + data=payloads.StatusPayload( + message=_("Cancelling the analysis in progress...") + ), ) ) @@ -289,7 +298,9 @@ def _generate_summaries_impl(self, video_paths: list[str]) -> None: # noqa: C90 Event( type=UIEvents.SET_STATUS, data=payloads.StatusPayload( - message=f"Gerando relatório {i + 1}/{total}..." + message=_("Generating report {current}/{total}...").format( + current=i + 1, total=total + ) ), ) ) @@ -519,7 +530,7 @@ def _san(s): self.ui_event_bus.publish( Event( type=UIEvents.SET_STATUS, - data=StatusPayload(message="Geração de relatórios concluída."), + data=StatusPayload(message=_("Report generation finished.")), ) ) diff --git a/src/zebtrack/core/viewmodels/hardware_status_view_model.py b/src/zebtrack/core/viewmodels/hardware_status_view_model.py index 5f1a19c1..d688b0a2 100644 --- a/src/zebtrack/core/viewmodels/hardware_status_view_model.py +++ b/src/zebtrack/core/viewmodels/hardware_status_view_model.py @@ -1,10 +1,12 @@ from __future__ import annotations from pathlib import Path -from typing import TYPE_CHECKING, Any, ClassVar +from typing import TYPE_CHECKING, Any import structlog +from zebtrack.i18n import _ + if TYPE_CHECKING: from zebtrack.core.application_bootstrapper import BootstrapResult from zebtrack.core.dependency_container import MainViewModelDependencies @@ -232,12 +234,21 @@ def get_weight_names_for_slot(self, method: str, target: str) -> list[str]: # Returns one entry per (method, target) slot. ``scope="project"`` filters # to the two slots actually consumed by runtime processing of the open # project, picked from ``settings.model_selection.{aquarium,animal}_method``. - _SLOT_LABELS: ClassVar[dict[tuple[str, str], tuple[str, str, str]]] = { - ("det", "aquarium"): ("🐠 Aquário (det)", "det", "aquarium"), - ("seg", "aquarium"): ("🐠 Aquário (seg)", "seg", "aquarium"), - ("det", "zebrafish"): ("🐟 Animal (det)", "det", "zebrafish"), - ("seg", "zebrafish"): ("🐟 Animal (seg)", "seg", "zebrafish"), - } + @staticmethod + def _slot_labels() -> dict[tuple[str, str], tuple[str, str, str]]: + """Slot labels, built per call. + + A method rather than the ClassVar it used to be: a dict of ``_()`` + calls in a class body is evaluated at IMPORT time, before + ``i18n.install()`` has run, so every label would freeze in the source + language no matter what ``ui.language`` said. + """ + return { + ("det", "aquarium"): (_("🐠 Aquarium (det)"), "det", "aquarium"), + ("seg", "aquarium"): (_("🐠 Aquarium (seg)"), "seg", "aquarium"), + ("det", "zebrafish"): (_("🐟 Animal (det)"), "det", "zebrafish"), + ("seg", "zebrafish"): (_("🐟 Animal (seg)"), "seg", "zebrafish"), + } def get_default_weights_summary( self, *, scope: str = "global" @@ -253,7 +264,8 @@ def get_default_weights_summary( Falls back to the full 4-slot view when settings are unavailable. """ wm = self.weight_manager - all_slots = list(self._SLOT_LABELS.values()) + slot_labels = self._slot_labels() + all_slots = list(slot_labels.values()) if scope == "project": try: @@ -266,8 +278,8 @@ def get_default_weights_summary( animal_method = None if aquarium_method and animal_method: all_slots = [ - self._SLOT_LABELS[(aquarium_method, "aquarium")], - self._SLOT_LABELS[(animal_method, "zebrafish")], + slot_labels[(aquarium_method, "aquarium")], + slot_labels[(animal_method, "zebrafish")], ] result: list[tuple[str, str, str, str | None]] = [] diff --git a/src/zebtrack/core/viewmodels/main_view_model_runtime.py b/src/zebtrack/core/viewmodels/main_view_model_runtime.py index af41964a..2bb96b85 100644 --- a/src/zebtrack/core/viewmodels/main_view_model_runtime.py +++ b/src/zebtrack/core/viewmodels/main_view_model_runtime.py @@ -9,6 +9,7 @@ import structlog from zebtrack.core.state_manager import StateCategory +from zebtrack.i18n import _ from zebtrack.ui import payloads from zebtrack.ui.event_bus_v2 import Event, UIEvents @@ -521,10 +522,10 @@ def join_threads(self) -> None: Event( UIEvents.ERROR_OCCURRED, payloads.ErrorOccurredPayload( - title="Erro Crítico", - message=( - "A thread da câmera não foi finalizada corretamente. " - "O aplicativo será encerrado." + title=_("Critical Error"), + message=_( + "The camera thread did not shut down correctly. " + "The application will close." ), ), ) diff --git a/src/zebtrack/locales/_pairs/pr3-analysis.json b/src/zebtrack/locales/_pairs/pr3-analysis.json new file mode 100644 index 00000000..4bf4ec06 --- /dev/null +++ b/src/zebtrack/locales/_pairs/pr3-analysis.json @@ -0,0 +1,22 @@ +{ + "_domain": "zebtrack", + "_comment": "Phase 3, batch 1: analysis/roi.py and analysis/analysis_service.py. Values are the relocated Portuguese literals, not new translations. Five msgids this batch needs already exist with the SAME Portuguese and are deliberately omitted so they are reused instead of conflicting: 'Starting processing for {count} videos...', 'Analysis Error', 'Cancelled', 'Success', 'Ready.'. The seg_overlap degradation warnings are report content: they reach the reader through ROIAnalyzer.degradation_warnings -> AnalysisService.validation_warnings -> the .docx validation appendix.", + + "The bbox_intersects rule requires the bbox columns: {columns}. They are not available in this dataset. Consider using 'centroid_in' or 'centroid_in_on_buffered_roi'.": "A regra bbox_intersects requer colunas de bbox: {columns}. Essas colunas não estão disponíveis no dataset. Considere usar 'centroid_in' ou 'centroid_in_on_buffered_roi'.", + "The pre-aligned masks do not have the same row count as the trajectory.": "As máscaras pré-alinhadas não têm o mesmo número de linhas da trajetória.", + "The mask sidecar is missing the columns {columns}.": "O sidecar de máscaras não tem as colunas {columns}.", + "The analysed trajectory has no 'frame' column, which is half of the join key with the masks.": "A trajetória analisada não tem a coluna 'frame', que é metade da chave de junção com as máscaras.", + "The mask sidecar contains no valid geometry.": "O sidecar de máscaras não contém nenhuma geometria válida.", + "No mask in the sidecar matches a (frame, track_id) of the analysed trajectory.": "Nenhuma máscara do sidecar corresponde a (frame, track_id) da trajetória analisada.", + "The 'seg_overlap' rule was selected but no mask sidecar (3b_Mascaras_*.parquet) was provided. Record with recorder.persist_masks enabled and a segmentation model (model_selection.animal_method='seg').": "A regra 'seg_overlap' foi selecionada mas nenhum sidecar de máscaras (3b_Mascaras_*.parquet) foi informado. Grave com recorder.persist_masks ligado e um modelo de segmentação (model_selection.animal_method='seg').", + "The mask sidecar is empty.": "O sidecar de máscaras está vazio.", + "The mask sidecar does not exist at '{path}'. Data recorded before this feature, or with recorder.persist_masks disabled, does not have one.": "O sidecar de máscaras não existe em '{path}'. Dados gravados antes desta funcionalidade, ou com recorder.persist_masks desligado, não o têm.", + "The sidecar '{path}' could not be read: {error}": "O sidecar '{path}' não pôde ser lido: {error}", + "The sidecar '{path}' has no rows.": "O sidecar '{path}' não tem linhas.", + "ROI rule 'seg_overlap' degraded to 'bbox_intersects': {detail}": "Regra de ROI 'seg_overlap' degradada para 'bbox_intersects': {detail}", + + "Trajectory with {count} animals (track_ids). The ROI metrics at the top of the report describe region OCCUPANCY (any_track semantics: the ROI counts as occupied while any animal is inside). The per-animal metrics are in the 'por_animal' sheet of the summary spreadsheet (_summary.xlsx). The general behaviour metrics that depend on frame ORDER (curvas_acentuadas) pool every animal together and must be read with caution.": "Trajetória com {count} animais (track_ids). As métricas de ROI no topo do relatório são de OCUPAÇÃO da região (semântica any_track: a ROI conta como ocupada enquanto qualquer animal estiver dentro). As métricas por animal estão na aba 'por_animal' da planilha de resumo (_summary.xlsx). As métricas de comportamento geral que dependem da ORDEM dos frames (curvas_acentuadas) agrupam todos os animais e devem ser lidas com cautela.", + "An unexpected error occurred: {error}": "Ocorreu um erro inesperado: {error}", + "Video analysis was cancelled.": "A análise de vídeo foi cancelada.", + "Analysis finished. Results saved to:\n{path}": "Análise concluída. Resultados salvos em:\n{path}" +} diff --git a/src/zebtrack/locales/_pairs/pr3-core-project.json b/src/zebtrack/locales/_pairs/pr3-core-project.json new file mode 100644 index 00000000..51e65f26 --- /dev/null +++ b/src/zebtrack/locales/_pairs/pr3-core-project.json @@ -0,0 +1,72 @@ +{ + "_domain": "zebtrack", + "_comment": "Phase 3, batch 3b: core/project/**. The post-creation guide in project_workflow_service quoted six tab/button names as retyped literals; they now interpolate the widgets' own msgids (Zone Configuration, Main Control, Processing and Reports, Select Video for Drawing, Load Frame from the Selected Video, Process Pending Videos...), all of which already existed and are reused. Two of those quotes were already WRONG before this batch, which is what retyping buys you. The three 'There is no X recorded for this video.' msgids replace a single template with a spliced noun, whose Portuguese needed gender agreement no substitution can guarantee.", + + "Project not initialised for saving ROI templates.": "Projeto não inicializado para salvar templates de ROI.", + "The template name cannot be empty.": "O nome do template não pode ficar vazio.", + "Invalid zone data for saving the template.": "Dados de zona inválidos para salvar o template.", + "Select at least the arena or the ROIs to save.": "Selecione ao menos arena ou ROIs para salvar.", + "Zone data cannot be empty.": "Dados de zona não podem ser vazios.", + "Invalid arena: at least 3 points are required.": "Arena inválida: é necessário ao menos 3 pontos.", + "No ROI available to save.": "Nenhuma ROI disponível para salvar.", + "A project path is required to save a template into the project.": "Caminho do projeto é necessário para salvar template no projeto.", + "A custom path is required for save_location='custom'.": "Caminho personalizado é necessário para save_location='custom'.", + "Template '{name}' already exists in {directory}.": "Template '{name}' já existe em {directory}.", + "Template '{name}' already exists.": "Template '{name}' já existe.", + "Template not found: {path}": "Template não encontrado: {path}", + "Invalid JSON in {path}: {error}": "JSON inválido em {path}: {error}", + "Invalid template in {path}: {error}": "Template inválido em {path}: {error}", + "Invalid template file: the 'data' block is missing.": "Arquivo de template inválido: bloco 'data' ausente.", + "Invalid template content.": "Conteúdo do template inválido.", + "The template file is not registered in the project.": "Arquivo do template não registrado no projeto.", + "ROI template '{name}' not found in the project.": "Template de ROI '{name}' não encontrado no projeto.", + "ROI template '{name}' not found for the requested context.": "Template de ROI '{name}' não encontrado para o contexto solicitado.", + "Cannot save the template into the current project: no project loaded.": "Não é possível salvar o template no projeto atual: projeto não carregado.", + + "Remove the reports and summaries before deleting the arena, the ROIs or the trajectory.": "Remova os relatórios e sumários antes de apagar arena, ROIs ou trajetórias.", + "There is no arena recorded for this video.": "Não há arena registrada para este vídeo.", + "There are no ROIs recorded for this video.": "Não há ROIs registradas para este vídeo.", + "There is no trajectory recorded for this video.": "Não há trajetória registrada para este vídeo.", + "There is no {asset} recorded for this video.": "Não há {asset} registrado para este vídeo.", + "There are no reports or summaries to remove.": "Não há relatórios ou sumários para remover.", + "Remove the reports and summaries before deleting the video.": "Remova relatórios e sumários antes de excluir o vídeo.", + "Remove the arena, the ROIs and the trajectory before deleting the video from the project.": "Remova arena, ROIs e trajetórias antes de excluir o vídeo do projeto.", + "Video not found in the project.": "Vídeo não encontrado no projeto.", + "Project data not initialised": "Dados do projeto não inicializados", + + "Project configuration file '{filename}' not found in the selected directory: {path}\n\nPlease make sure you selected a valid project folder.": "Arquivo de configuração do projeto '{filename}' não encontrado no diretório selecionado: {path}\n\nPor favor, garanta que você selecionou uma pasta de projeto válida.", + "Failed to load or parse the project configuration file: {path}\n\nThe file may be corrupted or unreadable.\n\nError: {error}": "Falha ao carregar ou analisar o arquivo de configuração do projeto: {path}\n\nO arquivo pode estar corrompido ou ilegível.\n\nErro: {error}", + "Cannot save the project: the project path is not set.\n\nThe project must be created before it can be saved.": "Não é possível salvar o projeto: caminho do projeto não definido.\n\nO projeto deve ser criado antes de ser salvo.", + "Permission denied while saving the project: {path}\n\nCheck that you have write permission on the folder.\n\nError: {error}": "Permissão negada ao salvar o projeto: {path}\n\nVerifique se você tem permissão de escrita na pasta.\n\nErro: {error}", + "I/O error while saving the project: {path}\n\nCheck the disk space and the permissions.\n\nError: {error}": "Erro de I/O ao salvar o projeto: {path}\n\nVerifique o espaço em disco e permissões.\n\nErro: {error}", + "Error serialising the project data: {path}\n\nThe project data may be corrupted.\n\nError: {error}": "Erro ao serializar dados do projeto: {path}\n\nDados do projeto podem estar corrompidos.\n\nErro: {error}", + "Unexpected error while saving the project: {path}\n\nPlease check the folder permissions.\n\nError: {error}": "Erro inesperado ao salvar o projeto: {path}\n\nPor favor, verifique as permissões da pasta.\n\nErro: {error}", + "Could not create the project directory: {error}\n\nPlease check the folder permissions and that the path is valid.": "Não foi possível criar o diretório do projeto: {error}\n\nPor favor, verifique as permissões da pasta e se o caminho é válido.", + + "The detection mode (det) for animals is only compatible with 1 animal per aquarium.\nCurrent configuration: {count} animals per aquarium.\n\nTo use multiple animals per aquarium, change the animal detection method to 'seg' (segmentation) in the settings.": "O modo de detecção (det) para animais só é compatível com 1 animal por aquário.\nConfiguração atual: {count} animais por aquário.\n\nPara usar múltiplos animais por aquário, altere o método de detecção de animais para 'seg' (segmentação) nas configurações.", + "Default": "Padrão", + + "🎉 Project created successfully!": "🎉 Projeto criado com sucesso!", + "📊 Video status:": "📊 Status dos vídeos:", + " • Total videos: {count}": " • Total de vídeos: {count}", + " • With arena defined: {count}": " • Com arena definida: {count}", + " • With ROIs defined: {count}": " • Com ROIs definidas: {count}", + " • With trajectory ready: {count}": " • Com trajetória pronta: {count}", + " • Pending processing: {count}": " • Pendentes de processamento: {count}", + "🚀 Recommended next steps:": "🚀 Próximos passos recomendados:", + "{step}. Review and adjust the imported zones": "{step}. Visualizar e ajustar zonas importadas", + " - Open the '{tab}' tab": " - Abra a aba '{tab}'", + " - Use the '{panel}' panel": " - Use o painel '{panel}'", + " - Double-click, or use '{button}', to review": " - Clique duas vezes, ou use '{button}', para revisar", + " - Adjust the arena and the ROIs as needed": " - Ajuste arena e ROIs conforme necessário", + "{step}. Process the pending videos": "{step}. Processar vídeos pendentes", + " - Confirm the processing intervals": " - Confirme os intervalos de processamento", + " - Click '{button}'": " - Clique em '{button}'", + "{step}. Generate reports": "{step}. Gerar relatórios", + " - Browse the hierarchy of groups, days and subjects": " - Navegue pela hierarquia de grupos, dias e sujeitos", + " - Generate individual or unified reports as needed": " - Gere relatórios individuais ou unificados conforme necessário", + "💡 Tips:": "💡 Dicas:", + " • Use the search box to locate videos quickly": " • Use a busca para localizar vídeos rapidamente", + " • The status symbols show which arenas, ROIs and trajectories exist": " • Os símbolos de status indicam arenas, ROIs e trajetórias disponíveis", + " • Adjust the zones before processing if necessary": " • Ajuste zonas antes de processar se necessário" +} diff --git a/src/zebtrack/locales/_pairs/pr3-core-recording.json b/src/zebtrack/locales/_pairs/pr3-core-recording.json new file mode 100644 index 00000000..a0ec8b1e --- /dev/null +++ b/src/zebtrack/locales/_pairs/pr3-core-recording.json @@ -0,0 +1,50 @@ +{ + "_domain": "zebtrack", + "_comment": "Phase 3, batch 3: core/recording/**. 'Camera Error' and 'Error' already existed with identical Portuguese and are reused. Three msgids depart from a literal relocation: the two 'Maximum supported' forms and the two 'Video: N frames dropped' forms replace a 'aquário(s)' / 'frame(s) descartado(s)' parenthetical, and 'The system supports only {supported} of the {requested} aquariums...' replaces a half-clause that was concatenated with a recommendation, so a translator can now reorder both sentences.", + + "Process multiple aquariums simultaneously in real time": "Processar múltiplos aquários simultaneamente em tempo real", + "Process one aquarium in real time": "Processar um aquário em tempo real", + "Record video without detection (process offline later)": "Gravar vídeo sem detecção (processar offline depois)", + "Record multiple sessions, one per aquarium": "Gravar múltiplas sessões, uma por aquário", + "Unknown mode": "Modo desconhecido", + + "The system supports {count} aquariums simultaneously. Real-time processing enabled.": "Sistema suporta {count} aquários simultaneamente. Processamento em tempo real habilitado.", + "Record without detection (better quality, process later)": "Gravar sem detecção (melhor qualidade, processar depois)", + "The system supports only {supported} of the {requested} aquariums requested for simultaneous processing. ": "O sistema suporta apenas {supported} dos {requested} aquários solicitados para processamento simultâneo. ", + "Recommended: record {count} separate sessions.": "Recomendado: gravar {count} sessões separadas.", + "Recommended: process only 1 aquarium in this session.": "Recomendado: processar apenas 1 aquário nesta sessão.", + "Process only 1 aquarium now (ignore the rest)": "Processar apenas 1 aquário agora (ignorar demais)", + "Split into {count} separate sessions": "Dividir em {count} sessões separadas", + "Record without detection (process offline later)": "Gravar sem detecção (processar offline depois)", + "⚠️ The system does not support {count} aquariums simultaneously.": "⚠️ Sistema não suporta {count} aquários simultaneamente.", + "Maximum supported: 1 aquarium.": "Máximo suportado: 1 aquário.", + "Maximum supported: {count} aquariums.": "Máximo suportado: {count} aquários.", + "The system is not sufficient for real-time processing. Recommended: record the video and process it offline.": "Sistema insuficiente para processamento em tempo real. Recomendado: gravar vídeo e processar offline.", + "⚠️ INSUFFICIENT HARDWARE for real-time detection.": "⚠️ HARDWARE INSUFICIENTE para detecção em tempo real.", + "CPU: {cores} cores (minimum 2)": "CPU: {cores} cores (mínimo 2)", + "RAM: {gb:.1f}GB available (minimum 4GB)": "RAM: {gb:.1f}GB disponível (mínimo 4GB)", + "Record video without detection (only viable option)": "Gravar vídeo sem detecção (única opção viável)", + "Default mode selected.": "Modo padrão selecionado.", + "Session {index} of {total} (individual aquarium)": "Sessão {index} de {total} (aquário individual)", + + "⏳ Warming up camera...": "⏳ Aquecendo câmera...", + "🔍 Looking for the aquarium...": "🔍 Procurando aquário...", + "⏳ Waiting for video...": "⏳ Aguardando vídeo...", + "Failed to open camera {index}.\n\nPossible causes:\n• The camera is in use by another program\n• Faulty hardware\n• Incompatible driver\n\nTry:\n• Closing other camera programs\n• Reconnecting the USB device\n• Selecting a different camera": "Falha ao abrir câmera {index}.\n\nPossíveis causas:\n• Câmera está em uso por outro programa\n• Hardware com defeito\n• Driver incompatível\n\nTente:\n• Fechar outros programas de câmera\n• Reconectar o dispositivo USB\n• Selecionar outra câmera", + "⚠️ Video: 1 frame dropped — check the disk / OneDrive (the recording may have gaps)": "⚠️ Vídeo: 1 frame descartado — verifique o disco / OneDrive (gravação pode ter falhas)", + "⚠️ Video: {count} frames dropped — check the disk / OneDrive (the recording may have gaps)": "⚠️ Vídeo: {count} frames descartados — verifique o disco / OneDrive (gravação pode ter falhas)", + "⏳ Analysing... ({seconds:.1f}s behind) - Recording OK": "⏳ Analisando... ({seconds:.1f}s atrás) - Gravação OK", + "⏳ Analysis is behind ({seconds:.1f}s) - Recording continues normally": "⏳ Análise atrasada ({seconds:.1f}s) - Gravação continua normalmente", + "⚠️ Analysis is far behind ({seconds:.1f}s) - Recording OK, analysis queued": "⚠️ Análise muito atrasada ({seconds:.1f}s) - Gravação OK, análise em fila", + + "✅ Camera analysis completed successfully!\n\n📊 Statistics:\n • Frames processed: {frames}\n • Total detections: {detections}\n • Unique tracks: {tracks}\n\n📁 Data saved to:\n{output_dir}\n\n💡 Files generated:\n • *_trajectory.parquet (trajectory)\n • *_zones.parquet (zones)\n • *.mp4/.avi (recorded video)": "✅ Análise de câmera concluída com sucesso!\n\n📊 Estatísticas:\n • Frames processados: {frames}\n • Detecções totais: {detections}\n • Trilhas únicas: {tracks}\n\n📁 Dados salvos em:\n{output_dir}\n\n💡 Arquivos gerados:\n • *_trajectory.parquet (trajetória)\n • *_zones.parquet (zonas)\n • *.mp4/.avi (vídeo gravado)", + "Analysis Completed": "Análise Concluída", + "⚠️ Recording completed, but no detection was found.\n\nPossible causes:\n • No detectable object in the field of view\n • Arena too restrictive\n • Confidence threshold too high\n\n📁 Data saved to:\n{output_dir}": "⚠️ Gravação concluída, mas nenhuma detecção foi encontrada.\n\nPossíveis causas:\n • Nenhum objeto detectável no campo de visão\n • Arena muito restritiva\n • Limiar de confiança muito alto\n\n📁 Dados salvos em:\n{output_dir}", + "Analysis Completed - No Detections": "Análise Concluída - Sem Detecções", + "⚠️ Recording completed, but the automatic analysis failed.\n\n📁 Raw data saved to:\n{output_dir}\n\nYou can analyse it manually through the interface.": "⚠️ Gravação concluída, mas a análise automática falhou.\n\n📁 Dados brutos salvos em:\n{output_dir}\n\nVocê pode analisar manualmente pela interface.", + "Recording Completed": "Gravação Concluída", + + "Camera configuration unavailable to start the recording.": "Configuração da câmera indisponível para iniciar a gravação.", + "Could not start the recording.": "Não foi possível iniciar a gravação.", + "🔍 Detecting aquarium... ({current}/{total})": "🔍 Detectando aquário... ({current}/{total})" +} diff --git a/src/zebtrack/locales/_pairs/pr3-core-services.json b/src/zebtrack/locales/_pairs/pr3-core-services.json new file mode 100644 index 00000000..ee7f5a47 --- /dev/null +++ b/src/zebtrack/locales/_pairs/pr3-core-services.json @@ -0,0 +1,67 @@ +{ + "_domain": "zebtrack", + "_comment": "Phase 3, batch 3c: core/services/**. 'No Project' already existed with identical Portuguese and is reused. The two arduino_ack_semantics msgids replace a template that spliced 'entrada'/'saída' into the same sentence twice — it only read correctly because both nouns are feminine in Portuguese. Not included here on purpose: the detector parameter guards (conf_threshold, track_buffer) and the metadata.json 'description' values, which are English fixed — the first are developer guards naming code parameters whose errors are swallowed by the EventBus, the second are written to disk and must not vary with ui.language.", + + "{roi} enter → token {token} → the firmware replied \"{ack}\", but an enter should switch it ON": "{roi} entrada → token {token} → o firmware respondeu \"{ack}\", mas uma entrada deveria ligar", + "{roi} exit → token {token} → the firmware replied \"{ack}\", but an exit should switch it OFF": "{roi} saída → token {token} → o firmware respondeu \"{ack}\", mas uma saída deveria desligar", + "token {token}: enter for {enter_rois} and exit for {exit_rois}": "token {token}: entrada de {enter_rois} e saída de {exit_rois}", + + "No {method} model is available for animal detection.": "Nenhum modelo {method} está disponível para detecção de animais.", + "Could not find the weight matching the path: {path}": "Não foi possível encontrar o peso correspondente ao caminho: {path}", + "OpenVINO model path not found or invalid. Please convert the model first.": "Caminho do modelo OpenVINO não encontrado ou inválido. Por favor, converta o modelo primeiro.", + "YOLO .pt model path not found or invalid.": "Caminho do modelo YOLO .pt não encontrado ou inválido.", + + "Open a project before copying the global settings.": "Abra um projeto antes de copiar configurações globais.", + "Global settings applied to the project.": "Configurações globais aplicadas ao projeto.", + "Global settings applied to the project at {path}.": "Configurações globais aplicadas ao projeto em {path}.", + "Open a project before saving calibration overrides.": "Abra um projeto antes de salvar overrides de calibração.", + "Project overrides updated from this calibration.": "Overrides do projeto atualizados a partir desta calibração.", + + "No weight selected.": "Nenhum peso selecionado.", + "Weight details not found.": "Detalhes do peso não encontrados.", + "The OpenVINO model is ready.": "O modelo OpenVINO está pronto.", + "Conversion to OpenVINO is required.": "Necessita de conversão para OpenVINO.", + "OpenVINO is disabled.": "O OpenVINO está desativado.", + "Weight '{name}' not found in the configuration.": "Peso '{name}' não encontrado na configuração.", + + "Could not save the weights configuration file: {error}": "Não foi possível salvar o arquivo de configuração de pesos: {error}", + "A weight file named '{name}' already exists in the weights directory.\n\nExisting file: {existing}\nFile being added: {incoming}\n\nPlease rename one of them before adding.": "Um arquivo de peso com o nome '{name}' já existe no diretório de pesos.\n\nArquivo existente: {existing}\nArquivo sendo adicionado: {incoming}\n\nPor favor, renomeie um dos arquivos antes de adicionar.", + "Failed to copy the external weight file: {error}": "Falha ao copiar o arquivo de peso externo: {error}", + "The model file was not found: {path}": "O arquivo de modelo não foi encontrado: {path}", + "The model path is invalid or inaccessible: {error}": "O caminho do modelo é inválido ou inacessível: {error}", + "A weight named '{name}' already exists.": "Um peso com o nome '{name}' já existe.", + "Weight '{name}' not found.": "Peso '{name}' não encontrado.", + "You cannot delete the last available weight.": "Você não pode excluir o último peso disponível.", + "OpenVINO model .xml file not found after the conversion.": "Arquivo .xml do modelo OpenVINO não encontrado após a conversão.", + "INT8 model .xml file not found after the conversion.": "Arquivo .xml do modelo INT8 não encontrado após conversão.", + + "Invalid camera index": "Índice de câmera inválido", + "The camera index must be between 0 and 10": "Índice de câmera deve estar entre 0 e 10", + "The Arduino port must be specified when Arduino is enabled": "Porta Arduino deve ser especificada quando Arduino está ativado", + "External trigger mode requires Arduino to be enabled": "Modo de trigger externo requer Arduino ativado", + "The recording duration must be greater than zero": "Duração de gravação deve ser maior que zero", + "The recording duration cannot exceed 2 hours (7200 seconds)": "Duração de gravação não pode exceder 2 horas (7200 segundos)", + "The countdown must be between 1 and 60 seconds": "Contagem regressiva deve estar entre 1 e 60 segundos", + "{name} [index {index}] - {resolution}": "{name} [índice {index}] - {resolution}", + "Camera #{number} [index {index}] - {resolution}": "Câmera #{number} [índice {index}] - {resolution}", + "The number of days must be between 1 and 365": "Número de dias deve estar entre 1 e 365", + "The number of groups must be between 1 and 6": "Número de grupos deve estar entre 1 e 6", + "The number of aquariums must be between 1 and 100": "Número de aquários deve estar entre 1 e 100", + "The number of animals per aquarium must be between 1 and 100": "Número de animais por aquário deve estar entre 1 e 100", + "The aquarium width must be greater than zero": "Largura do aquário deve ser maior que zero", + "The aquarium height must be greater than zero": "Altura do aquário deve ser maior que zero", + "The analysis interval must be between 1 and 30 frames": "Intervalo de análise deve estar entre 1 e 30 frames", + "The display interval must be between 1 and 30 frames": "Intervalo de exibição deve estar entre 1 e 30 frames", + "Invalid ROI inclusion rule. It must be one of: {rules}": "Regra de inclusão ROI inválida. Deve ser uma de: {rules}", + "The number of aquariums must be at least 1": "O número de aquários deve ser pelo menos 1", + "The number of aquariums cannot exceed 100": "O número de aquários não pode exceder 100", + "The number of animals per aquarium must be at least 1": "O número de animais por aquário deve ser pelo menos 1", + "The number of animals per aquarium cannot exceed 100": "O número de animais por aquário não pode exceder 100", + "Invalid multi-aquarium configuration": "Configuração multi-aquário inválida", + "Exactly 2 aquariums must be configured": "Exatamente 2 aquários devem ser configurados", + "Aquarium {index} has a very small area ({area} px²). It may affect detection accuracy.": "Aquário {index} tem área muito pequena ({area} px²). Pode afetar precisão da detecção.", + "The aquarium polygons appear to overlap. This may cause duplicate detections.": "Polígonos dos aquários parecem se sobrepor. Isso pode causar detecções duplicadas.", + "The regex does not capture the expected fields: {fields}": "Regex não captura campos esperados: {fields}", + "The regex does not match these files: {files}": "Regex não corresponde a arquivos: {files}", + "Invalid regex pattern: {error}": "Padrão regex inválido: {error}" +} diff --git a/src/zebtrack/locales/_pairs/pr3-core-tail.json b/src/zebtrack/locales/_pairs/pr3-core-tail.json new file mode 100644 index 00000000..c6f1b5cc --- /dev/null +++ b/src/zebtrack/locales/_pairs/pr3-core-tail.json @@ -0,0 +1,28 @@ +{ + "_domain": "zebtrack", + "_comment": "Phase 3, batch 3d: core/detection, core/video, core/viewmodels — closes core/. Seven msgids are reused rather than repeated: 'Processing Error' (×4, whose Portuguese was 'Erro no Processamento' while these sites said 'Erro de Processamento' — one dialog title, one msgid), 'Multi-individual', 'Invalid Configuration', and the det/seg validation from pr3-core-project.json, which analysis_control_view_model carried as a SECOND, differently worded copy. Not here on purpose: the aquarium_detector and multi_aquarium_detector guards, which name internal state and stay English without _().", + + "Incomplete calibration data.": "Dados de calibração incompletos.", + "Failed to prepare the calibration data.": "Falha ao preparar dados de calibração.", + + "Individual": "Individual", + + "Processing {current}/{total}: {experiment_id}": "Processando {current}/{total}: {experiment_id}", + "Step: {status}": "Etapa: {status}", + "Trajectory for {experiment_id} generated.": "Trajetória para {experiment_id} gerada.", + "Failed to generate the trajectory file for {experiment_id}.": "Falha ao gerar arquivo de trajetória para {experiment_id}.", + "File not found: {path}": "Arquivo não encontrado: {path}", + "Failed to read the trajectory file for {experiment_id}.": "Falha ao ler arquivo de trajetória para {experiment_id}.", + + "Cancelling the analysis in progress...": "Cancelando análise em andamento...", + "Generating report {current}/{total}...": "Gerando relatório {current}/{total}...", + "Report generation finished.": "Geração de relatórios concluída.", + + "🐠 Aquarium (det)": "🐠 Aquário (det)", + "🐠 Aquarium (seg)": "🐠 Aquário (seg)", + "🐟 Animal (det)": "🐟 Animal (det)", + "🐟 Animal (seg)": "🐟 Animal (seg)", + + "Critical Error": "Erro Crítico", + "The camera thread did not shut down correctly. The application will close.": "A thread da câmera não foi finalizada corretamente. O aplicativo será encerrado." +} diff --git a/src/zebtrack/locales/_pairs/pr3-dialogs-5a.json b/src/zebtrack/locales/_pairs/pr3-dialogs-5a.json new file mode 100644 index 00000000..d00341a1 --- /dev/null +++ b/src/zebtrack/locales/_pairs/pr3-dialogs-5a.json @@ -0,0 +1,42 @@ +{ + "_domain": "zebtrack", + "_comment": "Phase 3, batch 5a: the small dialogs in ui/dialogs. color_selection_dialog's colour name was doing four jobs — radio label, radio VALUE (name.lower()), the key compared in apply(), and result['name'] — so translating it would have left StringVar(value='verde') matching nothing and no colour pre-selected; the table now carries a stable key beside the translated label. Only result['rgb'] is persisted and result['name'] is only rendered into a status message, so the label is safe to translate. The six colour names reuse the msgids already created for ui/components/canvas/zone_editor.py, which maps the same six BGR tuples. preview_polygon_dialog's _BADGE_AUTO_TEXT/_BADGE_MANUAL_TEXT were module constants and became functions; the _BADGE_*_BG colours stayed constants, because a colour is not language. pending_videos_dialog's TAG_STYLES ClassVar was flagged as an import-time risk in the handoff but holds only hex colours — nothing to translate there. subject_selection_dialog spliced 'Concluído'/'Pendente' into 'Cobaia N: ...'; both had to agree with the noun, so they became one finished sentence each.", + + "Select Area Colour": "Selecionar Cor da Área", + "Choose the colour for this region of interest:": "Escolha a cor para esta área de interesse:", + + "Method:": "Método:", + "Distance from the edge (cm)": "Distância da Borda (cm)", + "Area ratio (0.0-1.0)": "Razão da Área (0.0-1.0)", + "Value:": "Valor:", + + "Diagnostics in Progress": "Diagnóstico em Progresso", + "Starting...": "Iniciando...", + "Please wait...": "Aguarde...", + "Running Model Diagnostics": "Executando Diagnóstico do Modelo", + "Status:": "Status:", + + "Global Model Diagnostics": "Diagnóstico Global do Modelo", + + "Subject {number}: done": "Cobaia {number}: Concluído", + "Subject {number}: pending": "Cobaia {number}: Pendente", + + "Process Pending Videos": "Processar Vídeos Pendentes", + "Review the hierarchical list and confirm the items you want to process.": "Revise a lista hierárquica e confirme os itens que deseja processar.", + "Legend:": "Legenda:", + "Ready": "Pronto", + "Partial": "Parcial", + "Skipped": "Ignorado", + "Include 1 video with only an arena in the processing.": "Incluir 1 vídeo com apenas arena no processamento.", + "Include {count} videos with only an arena in the processing.": "Incluir {count} vídeos com apenas arena no processamento.", + + "✓ Auto-detected": "✓ Auto-detectado", + "✎ Manually edited": "✎ Editado manualmente", + "Aquarium Detected - Confirm?": "Aquário Detectado - Confirmar?", + "✓ Aquarium detected successfully!": "✓ Aquário detectado com sucesso!", + "Approve this polygon (you will still be able to adjust it) or reject it and draw one manually?": "Aprovar este polígono (você ainda poderá ajustá-lo) ou rejeitar e desenhar manualmente?", + "Auto-detection confidence threshold": "Limiar de confiança da auto-detecção", + "Detection failed: {error}": "Falha na detecção: {error}", + "No aquarium found — adjust the threshold and try again.": "Nenhum aquário encontrado — ajuste o limiar e tente novamente.", + "Aquarium re-detected with threshold {value}": "Aquário re-detectado com limiar {value}" +} diff --git a/src/zebtrack/locales/_pairs/pr3-dialogs-5b.json b/src/zebtrack/locales/_pairs/pr3-dialogs-5b.json new file mode 100644 index 00000000..3307ac6f --- /dev/null +++ b/src/zebtrack/locales/_pairs/pr3-dialogs-5b.json @@ -0,0 +1,174 @@ +{ + "_domain": "zebtrack", + "_comment": "Phase 3, batch 5b: block_detail_dialog.py and live_analysis_dialog.py. 'Dia {n} - {grupo}' was retyped inside seventeen separate messages and collapses into one _block_label() helper; it is DISPLAY only, and the persisted key for the same block is built separately as 'Dia_{n}_{grupo}' (see add_note) which stays Portuguese because it is a dict key inside project_data. The 'Sem Sessões'/'Sem Relatórios'/duration-warning trio is intentionally shared between generate_partial_report and mark_batch_complete (the source comment says 'mesmos textos'), so both call sites keep one msgid each. block_detail_dialog also writes a .docx partial report; those headings live in the zebtrack domain rather than the reporter domain because babel extracts by directory (--ignore-dirs reporters) and this file is not under analysis/reporters -- importing the reporter _() here would extract into zebtrack.pot but look up in the reporter catalogue, i.e. always fall back to English. Two msgids were dropped from this batch after the corpus check flagged them: 'Detection Error' and 'Detection failed' already exist in pr2-coordinators-tail.json with slightly different Portuguese ('Erro na Detecção', 'Detecção falhou'). Writing a second, conflicting pair would make update_translations.py drop one of them silently, so both call sites reuse the existing msgid instead. Two `_` discards inside _write_partial_report_word had to be renamed: they sat below the _() call that renders the report heading, and ruff F823 caught it. The ValueErrors raised in live_analysis_dialog.validate() are wrapped because they are interpolated straight into a messagebox, not merely logged.", + + "Live Camera Analysis": "Análise de Câmera ao Vivo", + "Analyze Live Camera": "Analisar Câmera ao Vivo", + "Set up and start a real-time analysis session.": "Configure e inicie uma sessão de análise em tempo real.", + "Camera Selection": "Seleção de Câmera", + "Device:": "Dispositivo:", + "Select the camera for live analysis.": "Selecione a câmera para análise ao vivo.", + "Timing and Processing": "Tempo e Processamento", + "Duration (s):": "Duração (s):", + "Recording/Analysis Time\n\nSets how long the live session will last, in seconds.\n• 60s = 1 minute.\n• 300s = 5 minutes.": "Tempo de Gravação/Análise\n\nDefine quanto tempo a sessão ao vivo irá durar em segundos.\n• 60s = 1 minuto.\n• 300s = 5 minutos.", + "Analysis interval:": "Intervalo Análise:", + "Analysis Interval (frames)\n\nProcesses 1 frame out of every N frames from the camera.\n• Low values require a powerful computer.\n• Recommended for live: 1 or 2.": "Intervalo de Análise (frames)\n\nProcessa 1 frame a cada N frames da câmera.\n• Valores baixos exigem um computador potente.\n• Recomendado para Live: 1 ou 2.", + "Display interval:": "Intervalo Exibição:", + "Display Interval (frames)\n\nHow often the video on screen is refreshed.\n• Raising this helps if the interface feels slow.": "Intervalo de Exibição (frames)\n\nFrequência de atualização do vídeo na tela.\n• Aumentar este valor ajuda se a interface estiver lenta.", + "Session Options": "Opções da Sessão", + "Experiment ID:": "ID Experimento:", + "Experiment Identifier\n\nName used to organize the output files.\n• If left blank, the system generates a name from the date and time.": "Identificador do Experimento\n\nNome usado para organizar os arquivos de saída.\n• Se deixado em branco, o sistema gerará um nome com a data e hora.", + "Record video with overlay": "Gravar vídeo com overlay", + "Use OpenVINO acceleration": "Usar aceleração OpenVINO", + "Output folder:": "Pasta de Saída:", + "Directory where the results will be saved.\n• If left blank, uses the default folder 'live_analysis_sessions/'.": "Diretório onde os resultados serão salvos.\n• Se deixado em branco, usa a pasta padrão 'live_analysis_sessions/'.", + "Advanced AI and Setup Parameters": "Parâmetros Avançados de IA e Setup", + "Aquarium AI:": "IA Aquário:", + "Segmentation or Detection model for the tank.\n• seg: slower, but outlines the edges better.\n• det: very fast.": "Modelo de Segmentação ou Detecção do tanque.\n• seg: Mais lento, mas delimita melhor as bordas.\n• det: Muito rápido.", + "Fish AI:": "IA Peixe:", + "Model for the fish.\n• Use 'seg' if there is more than one fish per aquarium.": "Modelo para o peixe.\n• Use 'seg' se tiver mais de um peixe por aquário.", + "No. of aquariums:": "Num. Aquários:", + "Number of tanks in the field of view (1 or 2).": "Quantidade de tanques no campo de visão (1 ou 2).", + "Animals/aquarium:": "Animais/Aquário:", + "Number of fish inside each aquarium.": "Quantidade de peixes dentro de cada aquário.", + "Behavioural Analysis": "Análise Comportamental", + "Start Analysis": "Iniciar Análise", + "Select the output folder for the results": "Selecione a pasta de saída para os resultados", + "✗ Error detecting cameras": "✗ Erro ao detectar câmeras", + "Failed to detect cameras:\n{error}": "Falha ao detectar câmeras:\n{error}", + "No Camera Selected": "Câmera Não Selecionada", + "Please select a camera for the analysis.": "Por favor, selecione uma câmera para análise.", + "Invalid Camera": "Câmera Inválida", + "Camera index not found for: {camera}": "Índice de câmera não encontrado para: {camera}", + "Duration must be positive": "Duração deve ser positiva", + "Duration Too Long": "Duração Muito Longa", + "Maximum allowed duration: {value}s\nAdjusting to the maximum...": "Duração máxima permitida: {value}s\nAjustando para o máximo...", + "Invalid Duration": "Duração Inválida", + "Duration must be a positive number:\n{error}": "Duração deve ser um número positivo:\n{error}", + "Intervals must be >= 1": "Intervalos devem ser >= 1", + "Invalid Interval": "Intervalo Inválido", + "Intervals must be positive whole numbers:\n{error}": "Intervalos devem ser números inteiros positivos:\n{error}", + "The number of aquariums and animals must be >= 1": "Número de aquários e animais devem ser >= 1", + "The aquarium dimensions must be positive": "Dimensões do aquário devem ser positivas", + "Invalid Calibration Parameter": "Parâmetro de Calibração Inválido", + "Calibration error:\n{error}": "Erro na calibração:\n{error}", + "Behavioural parameters must be non-negative": "Parâmetros comportamentais devem ser não-negativos", + "Invalid Behavioural Parameter": "Parâmetro Comportamental Inválido", + "Error in the behavioural parameters:\n{error}": "Erro nos parâmetros comportamentais:\n{error}", + "The smoothing window must be >= 3": "Janela de suavização deve ser >= 3", + "The smoothing window must be odd": "Janela de suavização deve ser ímpar", + "The polynomial order must be >= 1": "Ordem do polinômio deve ser >= 1", + "The polynomial order must be smaller than the window": "Ordem do polinômio deve ser menor que a janela", + "Invalid Parameter": "Parâmetro Inválido", + "Validation error:\n{error}": "Erro na validação:\n{error}", + + "Day {day} - {group}": "Dia {day} - {group}", + "Sessions: {block}": "Sessões: {block}", + "📊 Progress: {done}/{total} sessions": "📊 Progresso: {done}/{total} sessões", + "🏟️ Project polygon: ✅ Defined": "🏟️ Polígono do projeto: ✅ Definido", + "🏟️ Project polygon: ⚠️ Not defined": "🏟️ Polígono do projeto: ⚠️ Não definido", + "🐟 Subjects": "🐟 Cobaias", + "📷 Camera:": "📷 Câmera:", + "Change...": "Trocar...", + "⏱️ Default duration for the block:": "⏱️ Duração padrão do bloco:", + "Edit...": "Alterar...", + "🛠️ Quick Actions": "🛠️ Ações Rápidas", + "▶️ Start Next Session": "▶️ Iniciar Próxima Sessão", + "📊 Generate Partial Report": "📊 Gerar Relatório Parcial", + "📝 Add Note": "📝 Adicionar Nota", + "Close": "Fechar", + "✅ Mark Batch as Complete": "✅ Marcar Lote Como Completo", + + "Animal {subject}": "Animal {subject}", + "🏟️ Auto-detected": "🏟️ Auto-detectado", + "✏️ Drawn manually": "✏️ Desenhado manualmente", + "🏟️ Project polygon ready (will be reused)": "🏟️ Polígono do projeto pronto (será reutilizado)", + "⏱️ {duration} (its own)": "⏱️ {duration} (própria)", + "⏱️ {duration} (block default)": "⏱️ {duration} (padrão do bloco)", + "📊 View Results": "📊 Ver Resultados", + "▶️ Start": "▶️ Iniciar", + "⏱️ Duration": "⏱️ Duração", + + "[Session] Index {index}{suffix}": "[Sessão] Índice {index}{suffix}", + "{name} (index {index})": "{name} (índice {index})", + "Index {index}": "Índice {index}", + "Could not detect cameras:\n\n{error}": "Não foi possível detectar câmeras:\n\n{error}", + "No camera": "Nenhuma câmera", + "No camera was detected on this system.": "Nenhuma câmera foi detectada no sistema.", + "Change the camera for this session": "Trocar câmera para esta sessão", + "Select the camera:": "Selecione a câmera:", + "Save as the default camera for this project": "Salvar como câmera padrão deste projeto", + "Failed to save camera": "Falha ao salvar câmera", + "Could not save the camera as the default:\n{error}": "Não foi possível salvar a câmera como padrão:\n{error}", + + "Invalid value": "Valor inválido", + "'{value}' is not a number of minutes.": "'{value}' não é um número de minutos.", + "The duration must be greater than zero.": "A duração deve ser maior que zero.", + "Project not saved": "Projeto não salvo", + "The project has no path defined, so the duration could not be written. Save the project and try again.": "O projeto não tem um caminho definido, então a duração não pôde ser gravada. Salve o projeto e tente de novo.", + "Failed to save": "Falha ao salvar", + "The duration could not be written to the project:\n{error}": "A duração não pôde ser gravada no projeto:\n{error}", + "Default duration for the block": "Duração padrão do bloco", + "Recording duration for {block}, in minutes:\n\nApplies to every subject in this block that has no duration of its own. Sessions already recorded are not affected.": "Duração das gravações de {block}, em minutos:\n\nVale para todas as cobaias deste bloco que não tenham duração própria. Sessões já gravadas não são afetadas.", + "Duration — Animal {subject}": "Duração — Animal {subject}", + "Recording duration for Animal {subject} ({block}), in minutes:\n\nLeave it equal to {default} to follow the block default.": "Duração da gravação do Animal {subject} ({block}), em minutos:\n\nDeixe igual a {default} para seguir o padrão do bloco.", + + "Failed to start the session for Animal {subject}\n{block}": "Falha ao iniciar sessão para Animal {subject}\n{block}", + "Error starting the session: {error}": "Erro ao iniciar sessão: {error}", + "Complete": "Completo", + "Every session in this block has been completed!": "Todas as sessões deste bloco foram concluídas!", + "Folder not found": "Pasta não encontrada", + "Could not find the results folder for Animal {subject}.\n{block}": "Não foi possível encontrar a pasta de resultados para Animal {subject}.\n{block}", + + "No valid data found in the summary files": "Nenhum dado válido encontrado nos arquivos de resumo", + "No valid data found (every file was empty)": "Nenhum dado válido encontrado (todos os arquivos estavam vazios)", + "The sessions in this block have different durations ({listed}).\n\nABSOLUTE metrics — total distance, number of entries, time in ROI — grow with recording time and are NOT directly comparable across these animals. The 'video_duration_s' column is in the report so you can normalize however you prefer.\n\nGenerate the report anyway?": "As sessões deste bloco têm durações diferentes ({listed}).\n\nMétricas ABSOLUTAS — distância total, número de entradas, tempo em ROI — crescem com o tempo de gravação e NÃO são diretamente comparáveis entre estes animais. A coluna 'video_duration_s' está no relatório para você normalizar como preferir.\n\nDeseja gerar o relatório mesmo assim?", + + "Partial Report - {block}": "Relatório Parcial - {block}", + "Generated at: {timestamp}": "Gerado em: {timestamp}", + "Aggregated sessions: {count}": "Sessões agregadas: {count}", + "Consolidated spreadsheet: {name}": "Planilha consolidada: {name}", + "WARNING — heterogeneous recording durations in this block ({listed}). Absolute metrics (total distance, number of entries, time in ROI) scale with recording time and are not directly comparable across these animals without normalization. Use the 'video_duration_s' column of the spreadsheet to normalize.": "ATENÇÃO — durações de gravação heterogêneas neste bloco ({listed}). Métricas absolutas (distância total, número de entradas, tempo em ROI) escalam com o tempo de gravação e não são diretamente comparáveis entre estes animais sem normalização. Use a coluna 'video_duration_s' da planilha para normalizar.", + "Sessions included": "Sessões incluídas", + "Animal": "Animal", + "Source file": "Arquivo-fonte", + "Summary per Animal": "Resumo por Animal", + + "File in use": "Arquivo em uso", + "The default file was locked by another program/sync service.\nThe reports were saved under new names:\n{excel}\n{word}": "O arquivo padrão estava bloqueado por outro programa/serviço de sincronização.\nOs relatórios foram salvos com novos nomes:\n{excel}\n{word}", + "Reports Generated": "Relatórios Gerados", + "Partial reports generated successfully!\n\n📊 Excel: {excel}\n📝 Word: {word}\n🐟 {count} aggregated sessions": "Relatórios parciais gerados com sucesso!\n\n📊 Excel: {excel}\n📝 Word: {word}\n🐟 {count} sessões agregadas", + "Open Partial Report": "Abrir Relatório Parcial", + "Open the partial spreadsheet in Excel?\n\n📊 {name}": "Deseja abrir a planilha parcial em Excel?\n\n📊 {name}", + "The Excel report was generated, but it could not be opened:\n{path}": "O relatório Excel foi gerado, mas não foi possível abri-lo:\n{path}", + "Open the partial report in Word?\n\n📝 {name}": "Deseja abrir o relatório parcial em Word?\n\n📝 {name}", + "The Word report was generated, but it could not be opened:\n{path}": "O relatório Word foi gerado, mas não foi possível abri-lo:\n{path}", + + "No Sessions": "Sem Sessões", + "No completed session found for\n{block}": "Nenhuma sessão concluída encontrada para\n{block}", + "No Reports": "Sem Relatórios", + "No summary file found in the sessions of\n{block}\n\nRun the session analysis first.": "Nenhum arquivo de resumo encontrado nas sessões de\n{block}\n\nExecute a análise das sessões primeiro.", + "Different durations in the block": "Durações diferentes no bloco", + "Partial reports updated: {block}": "Relatórios parciais atualizados: {block}", + "Failed to generate the partial report:\n{error}": "Falha ao gerar relatório parcial:\n{error}", + + "Add Experimental Note": "Adicionar Nota Experimental", + "Note for {block}:\n\n(leave blank to clear)": "Nota para {block}:\n\n(deixe vazio para limpar)", + "Note Saved": "Nota Salva", + "Experimental note saved successfully!": "Nota experimental salva com sucesso!", + "Note Removed": "Nota Removida", + "Experimental note removed.": "Nota experimental removida.", + "Failed to save the note:\n{error}": "Falha ao salvar nota:\n{error}", + + "Confirm — Mark batch as complete": "Confirmar — Marcar lote como completo", + "Mark the batch of Group '{group}' on Day {day} as complete?\n\nScope: ALL sessions already recorded for this group on this day will be consolidated into the block's partial report (Excel + Word) and the matching square in the Progress grid will turn green.\n\nThis action does NOT affect other groups, other days, nor does it close the project as a whole. You can carry on recording new subjects on other days/groups normally.\n\nContinue?": "Marcar o lote do Grupo '{group}' no Dia {day} como completo?\n\nEscopo: TODAS as sessões já gravadas deste grupo neste dia serão consolidadas no relatório parcial do bloco (Excel + Word) e o quadrado correspondente na grade do Progresso ficará verde.\n\nEsta ação NÃO afeta outros grupos, outros dias, nem encerra o projeto como um todo. Você poderá continuar gravando novos sujeitos em outros dias/grupos normalmente.\n\nDeseja continuar?", + "Batch complete: {block}": "Lote concluído: {block}", + "Batch '{block}' marked as complete.\n\n📊 Excel: {excel}\n📝 Word: {word}\n🐟 {count} aggregated sessions\n\nReports in: {folder}": "Lote '{block}' marcado como completo.\n\n📊 Excel: {excel}\n📝 Word: {word}\n🐟 {count} sessões agregadas\n\nRelatórios em: {folder}", + "\n\n⚠️ The default file was in use; the reports were saved with a date/time suffix.": "\n\n⚠️ O arquivo padrão estava em uso; os relatórios foram salvos com sufixo de data/hora.", + "\n\n⚠️ Could not record the completion in the project; check the log.": "\n\n⚠️ Não foi possível registrar a completude no projeto; verifique o log.", + "Batch Complete": "Lote Completo", + "Error — Batch not completed": "Erro — Lote não concluído", + "Failed to generate the report for batch '{block}':\n{error}\n\nThe batch was NOT marked as complete.": "Falha ao gerar o relatório do lote '{block}':\n{error}\n\nO lote NÃO foi marcado como completo.", + "Batch processing": "Lote em processamento", + "Batch '{block}': the consolidated report (Excel + Word) is being generated in the background.\n\nYou will be notified when it finishes.": "Lote '{block}': o relatório consolidado (Excel + Word) está sendo gerado em segundo plano.\n\nVocê será avisado quando terminar." +} diff --git a/src/zebtrack/locales/_pairs/pr3-dialogs-5c.json b/src/zebtrack/locales/_pairs/pr3-dialogs-5c.json new file mode 100644 index 00000000..0e47214a --- /dev/null +++ b/src/zebtrack/locales/_pairs/pr3-dialogs-5c.json @@ -0,0 +1,121 @@ +{ + "_domain": "zebtrack", + "_comment": "Phase 3, batch 5c: create_project_dialog, single_video_config_dialog and project_video_import_dialog. The 'Grupo'/'Dia'/'Sujeito' column labels in project_video_import_dialog reuse the singular msgids introduced by custom_regex_dialog in 4d, and the 'Validação' messagebox title (fourteen call sites) stays a single msgid. create_project_dialog's video summary used the same 'N arquivo(s) + N pasta(s) selecionado(s)' construction as file_selection_step, so it reuses the plural pairs and the 'Selected: {parts}' wrapper from 4d. 'Grupo {n}:' in create_project_dialog is a pure display label: group_name_vars start empty and the operator types the real name, so nothing persisted depends on it. Four msgids were dropped from this batch: 'Number of Groups:', 'Group Names' and 'Validation' already exist identically in pr3-wizard-shell.json, and 'Metadata' already exists in pr2-components-tail.json as 'Metadados' -- writing a conflicting pair would make update_translations.py drop one silently, so the tree heading reuses the existing msgid and now agrees with the rest of the app.", + + "Project Folder:": "Pasta do Projeto:", + "Number of Aquariums:": "Número de Aquários:", + "Animals per Aquarium:": "Animais por Aquário:", + "Aquarium Width (cm):": "Largura do Aquário (cm):", + "Aquarium Height (cm):": "Altura do Aquário (cm):", + "Method for Aquarium:": "Método para Aquário:", + "Method for Animals:": "Método para Animais:", + "Project Type:": "Tipo de Projeto:", + "Pre-recorded": "Pré-gravado", + "Live": "Ao Vivo", + "Select Videos...": "Selecionar Vídeos...", + "Select Folder...": "Selecionar Pasta...", + "Use timed recording?": "Usar gravação com tempo?", + "minutes": "minutos", + "Use a countdown?": "Usar contagem regressiva?", + "Experimental Design (Live Project)": "Design Experimental (Projeto ao Vivo)", + "Total Experiment Days:": "Total de Dias do Experimento:", + "Subjects per Group:": "Cobaias por Grupo:", + "Group {number}:": "Grupo {number}:", + "Select a Base Folder for the Project": "Selecione uma Pasta Principal para o Projeto", + "The selection contains invalid paths.": "Seleção contém caminhos inválidos.", + "Not applicable to live projects.": "Não aplicável para projetos ao vivo.", + "Please select a valid base folder.": "Por favor, selecione uma pasta principal válida.", + "The project name cannot be empty.": "O nome do projeto não pode estar vazio.", + "A project folder with this name already exists and is not empty.": "Uma pasta de projeto com este nome já existe e não está vazia.", + "Please select at least one video file or folder for the pre-recorded analysis.": "Por favor, selecione pelo menos um arquivo de vídeo ou pasta para análise pré-gravada.", + "The number of groups must be between 1 and 6.": "O número de grupos deve ser entre 1 e 6.", + "The name of Group {number} cannot be empty.": "O nome do Grupo {number} não pode estar vazio.", + "The experimental design parameters must be valid positive numbers.": "Os parâmetros do design experimental devem ser números positivos válidos.", + "The recording duration must be a positive number.": "A duração da gravação deve ser um número positivo.", + "The countdown duration must be a positive whole number.": "A duração da contagem regressiva deve ser um inteiro positivo.", + "The analysis and display intervals must be positive whole numbers.": "Os intervalos de análise e exibição devem ser números inteiros positivos.", + + "Single Video Analysis Configuration": "Configuração de Análise de Vídeo Único", + "Video File": "Arquivo de Vídeo", + "Experimental Configuration": "Configuração Experimental", + "Number of Aquariums\n\nSets whether the video contains 1 or 2 independent tanks.\n• 1: standard analysis.\n• 2: lets you draw two arenas and process them together.": "Número de Aquários\n\nDefine se o vídeo contém 1 ou 2 tanques independentes.\n• 1: Análise padrão.\n• 2: Permite desenhar duas arenas e processá-las em conjunto.", + "Animals per Aquarium\n\nHow many fish are in each tank.\n• 1: enables the tracker optimized for a single subject.\n• >1: requires Segmentation (seg) mode to avoid ID swaps.": "Animais por Aquário\n\nQuantidade de peixes em cada tanque.\n• 1: Ativa o rastreador otimizado para sujeito único.\n• >1: Exige o modo de Segmentação (seg) para evitar trocas de ID.", + "Real Width (cm)\n\nHorizontal size of the tank in centimetres.\n• Essential to compute speed in cm/s and total distance.": "Largura Real (cm)\n\nDimensão horizontal do tanque em centímetros.\n• Essencial para calcular velocidade em cm/s e distância total.", + "Real Height (cm)\n\nVertical size of the tank in centimetres.": "Altura Real (cm)\n\nDimensão vertical do tanque em centímetros.", + "Behaviour Metrics": "Métricas de Comportamento", + "Sharp turn (degrees/s):": "Curva Acentuada (graus/s):", + "Sharp Turn Threshold\n\nMinimum angular speed for an abrupt change of direction to count.\n• Raise it: makes turn detection stricter.\n• Default: 180.0 degrees/s.": "Limiar de Curva Acentuada (Sharp Turn)\n\nVelocidade angular mínima para contar uma mudança de direção brusca.\n• Aumentar: Torna a detecção de curvas mais restritiva.\n• Padrão: 180.0 graus/s.", + "Freezing (cm/s):": "Congelamento (cm/s):", + "Freezing Threshold (Speed)\n\nSpeed below which the fish is considered motionless.\n• Lower it: if small breathing movements are counting as swimming.\n• Default: 0.5 cm/s.": "Limiar de Congelamento (Velocidade)\n\nVelocidade abaixo da qual o peixe é considerado imóvel.\n• Diminuir: Se pequenos movimentos de respiração estiverem contando como nado.\n• Padrão: 0.5 cm/s.", + "Min. duration (s):": "Duração Mín. (s):", + "Minimum Freezing Duration\n\nHow long the fish must stay still for the event to be recorded.\n• E.g. 1.0s means brief pauses (<1s) are ignored.": "Duração Mínima de Congelamento\n\nTempo mínimo que o peixe deve ficar parado para registrar o evento.\n• Ex: 1.0s significa que paradas rápidas (<1s) serão ignoradas.", + "Trajectory Smoothing": "Suavização de Trajetória", + "Smoothing window:": "Janela Suavização:", + "Smoothing Window (frames)\n\nNumber of frames for the moving average. MUST BE ODD.\n• Raise it: removes jitter, but over-smooths corners.\n• Default: 5 (Express) or 7 (Full).": "Janela de Suavização (frames)\n\nNúmero de frames para média móvel. DEVE SER ÍMPAR.\n• Aumentar: Remove tremidos, mas suaviza demais os cantos.\n• Padrão: 5 (Express) ou 7 (Completo).", + "Polynomial order:": "Ordem Polinômio:", + "Polynomial Order\n\nComplexity of the curve fit. Must be SMALLER than the window.\n• Default: 2.": "Ordem do Polinômio\n\nComplexidade do ajuste de curva. Deve ser MENOR que a janela.\n• Padrão: 2.", + "ℹ️ Removes jitter without erasing real movement.": "ℹ️ Remove tremidos sem apagar movimentos reais.", + "Processing Optimization": "Otimização de Processamento", + "Analysis Interval (frames)\n\nProcesses 1 frame out of every N frames of the video.\n• 1: analyzes everything (slow).\n• 10: skips 9 frames (fast).\n• Recommended: 5 or 10 depending on the video speed.": "Intervalo de Análise (frames)\n\nProcessa 1 frame a cada N frames do vídeo.\n• 1: Analisa tudo (lento).\n• 10: Pula 9 frames (rápido).\n• Recomendado: 5 ou 10 conforme a velocidade do vídeo.", + "Display Interval (frames)\n\nHow often the on-screen image refreshes during processing.\n• Use high values (e.g. 30) to speed the analysis up by saving video resources.": "Intervalo de Exibição (frames)\n\nFrequência de atualização da imagem na tela durante o processo.\n• Use valores altos (ex: 30) para acelerar a análise economizando recursos de vídeo.", + "AI Models": "Modelos de IA", + "Model for Aquarium Detection\n\n• seg: segmentation (more precise at the corners).\n• det: box detection (faster).": "Modelo para Detecção do Aquário\n\n• seg: Segmentação (mais preciso nos cantos).\n• det: Detecção por caixa (mais rápido).", + "Model for Fish Tracking\n\n• seg: recommended for several fish (avoids confusion).\n• det: recommended for 1 fish (very fast).": "Modelo para Rastreamento do Peixe\n\n• seg: Recomendado para múltiplos peixes (evita confusão).\n• det: Recomendado para 1 peixe (muito rápido).", + "Use OpenVINO acceleration (Intel)": "Usar aceleração OpenVINO (Intel)", + "Select a Video File": "Selecione um Arquivo de Vídeo", + "All files": "Todos os arquivos", + "Please select a video file before continuing.": "Por favor, selecione um arquivo de vídeo antes de continuar.", + "The values must be positive.": "Os valores devem ser positivos.", + "The intervals must be positive whole numbers.": "Os intervalos devem ser números inteiros positivos.", + "The smoothing window must be positive.": "A janela de suavização deve ser positiva.", + "The smoothing window must be an odd number.": "A janela de suavização deve ser um número ímpar.", + "The polynomial order must be at least 1.": "A ordem do polinômio deve ser pelo menos 1.", + "The polynomial order must be smaller than the smoothing window.": "A ordem do polinômio deve ser menor que a janela de suavização.", + "Validation error: {error}\n\nCheck that every field is filled in correctly.": "Erro de validação: {error}\n\nVerifique se todos os campos estão preenchidos corretamente.", + + "Video Animals": "Animais do Vídeo", + "Set the group, day and subject for each animal in the video.": "Defina grupo, dia e sujeito para cada animal do vídeo.", + "Enter the group for animal {index}.": "Informe o grupo do animal {index}.", + "Enter the subject for animal {index}.": "Informe o sujeito do animal {index}.", + "Enter a valid day for animal {index}.": "Informe um dia válido para o animal {index}.", + "Edit Video Metadata": "Editar Metadata do Vídeo", + "File: {name}": "Arquivo: {name}", + "Edit Video Animals...": "Editar Animais do Vídeo...", + "Enter the group for the video.": "Informe o grupo do vídeo.", + "Enter a valid day.": "Informe um dia válido.", + "Enter the subject for the video.": "Informe o sujeito do vídeo.", + "Apply metadata changes to {kind} '{label}' in 1 video.": "Aplicar alterações de metadata ao {kind} '{label}' em 1 vídeo.", + "Apply metadata changes to {kind} '{label}' in {count} videos.": "Aplicar alterações de metadata ao {kind} '{label}' em {count} vídeos.", + "Update group": "Atualizar grupo", + "Update day": "Atualizar dia", + "Update subject": "Atualizar sujeito", + "Select at least one field to update in bulk.": "Selecione pelo menos um campo para atualizar em lote.", + "Enter the group to apply.": "Informe o grupo a aplicar.", + "Enter the subject to apply.": "Informe o sujeito a aplicar.", + "Import Videos into the Project": "Importar Vídeos ao Projeto", + "Review the videos found, set group, day and subject, and choose whether the batch is only added or also processed.": "Revise os vídeos encontrados, defina grupo, dia e sujeito, e escolha se o lote será apenas adicionado ou também processado.", + "Videos Found": "Vídeos Encontrados", + "File": "Arquivo", + "Data": "Dados", + "Animals": "Animais", + "Batch Defaults": "Padrões do Lote", + "Apply to Selected": "Aplicar aos Selecionados", + "Fill Blanks in the Batch": "Preencher Vazios no Lote", + "Selected Video": "Vídeo Selecionado", + "Save to This Video": "Salvar Neste Vídeo", + "Configure Video Animals...": "Configurar Animais do Vídeo...", + "After Importing": "Após Importar", + "Only add to the project": "Apenas adicionar ao projeto", + "Add and process pending items": "Adicionar e processar pendências", + "Add and reprocess everything": "Adicionar e reprocessar todos", + "Import": "Importar", + "No video available to import.": "Nenhum vídeo disponível para importação.", + "Enter the group for video {name}.": "Informe o grupo do vídeo {name}.", + "Enter a valid day for video {name}.": "Informe um dia válido para o vídeo {name}.", + "Enter the subject for video {name}.": "Informe o sujeito do vídeo {name}.", + "Total: {total}\nTrajectory ready: {with_trajectory}\nZones ready: {with_zones}\nArena only: {arena_only}\nNo arena: {without_arena}": "Total: {total}\nTrajetória pronta: {with_trajectory}\nZonas prontas: {with_zones}\nSó arena: {arena_only}\nSem arena: {without_arena}", + "Trajectory ready": "Trajetória pronta", + "Zones ready": "Zonas prontas", + "Arena only": "Só arena", + "No arena": "Sem arena" +} diff --git a/src/zebtrack/locales/_pairs/pr3-dialogs-5d.json b/src/zebtrack/locales/_pairs/pr3-dialogs-5d.json new file mode 100644 index 00000000..64b1ec51 --- /dev/null +++ b/src/zebtrack/locales/_pairs/pr3-dialogs-5d.json @@ -0,0 +1,81 @@ +{ + "_domain": "zebtrack", + "_comment": "Phase 3, batch 5d: live_camera_mode_selection_dialog, aquarium_detection_progress_dialog, calibration_dialog, start_recording_dialog, aquarium_assignment_dialog and multi_aquarium_live_preview_window. MODE_DESCRIPTIONS was a typing.ClassVar dict in a CLASS BODY -- the last import-time translation site in ui/dialogs -- and became a _mode_descriptions() staticmethod; its RECORD_ONLY short name reuses the 'Recording Only' msgid created in 4b, where live_config_step quotes that same mode by name in its insufficient-hardware warning. start_recording_dialog duplicates block_detail_dialog's entire camera chooser verbatim, so all of it reuses the 5b msgids and only three entries here are new. The ValueErrors in aquarium_assignment_dialog._collect_configs are wrapped because validate() renders them with messagebox.showerror(..., str(e)).", + + "Parallel Real-Time Processing\n• Detects 2-6 aquariums simultaneously\n• Requires a GPU and ≥4 CPU cores\n• Higher throughput, ideal for large experiments": "Processamento Paralelo em Tempo Real\n• Detecta 2-6 aquários simultaneamente\n• Requer GPU e ≥4 cores de CPU\n• Maior throughput, ideal para experimentos grandes", + "Single Aquarium in Real Time\n• Processes 1 aquarium at a time\n• Requires ≥2 CPU cores\n• Ideal for systems with limited resources": "Aquário Único em Tempo Real\n• Processa 1 aquário por vez\n• Requer ≥2 cores de CPU\n• Ideal para sistemas com recursos limitados", + "Sequential Sessions\n• Records N separate sessions, one at a time\n• Processes each aquarium in real time\n• Requires manual intervention between sessions": "Sessões Sequenciais\n• Grava N sessões separadas, uma de cada vez\n• Processa cada aquário em tempo real\n• Requer intervenção manual entre sessões", + "Recording Only (Offline)\n• Records video without real-time processing\n• Later analysis with every feature available\n• Always possible, no hardware requirements": "Apenas Gravação (Offline)\n• Grava vídeo sem processamento em tempo real\n• Análise posterior com todos os recursos\n• Sempre possível, sem requisitos de hardware", + "Processing Mode Selection": "Seleção de Modo de Processamento", + "⚠️ Processing Mode Adjustment Required": "⚠️ Ajuste de Modo de Processamento Necessário", + "Your system does not support real-time processing of {count} aquariums simultaneously.\n\nReview the options below and select a suitable mode.": "Seu sistema não suporta processamento em tempo real de {count} aquários simultaneamente.\n\nRevise as opções abaixo e selecione o modo adequado.", + "📊 Hardware Summary": "📊 Resumo de Hardware", + "Capability: {value}": "Capacidade: {value}", + "✗ Not detected": "✗ Não detectada", + "✓ Yes": "✓ Sim", + "✗ No": "✗ Não", + "CPU: {cores} cores ({usage}% used)\nRAM: {available} GB available of {total} GB\nGPU: {gpu}\nSupported aquariums: {aquariums}\nReal time: {realtime}": "CPU: {cores} cores ({usage}% uso)\nRAM: {available} GB disponível de {total} GB\nGPU: {gpu}\nAquários Suportados: {aquariums}\nTempo Real: {realtime}", + "🎯 Select the Processing Mode": "🎯 Selecione o Modo de Processamento", + "Alternatives:": "Alternativas:", + "✓ Confirm Selection": "✓ Confirmar Seleção", + "✗ Cancel": "✗ Cancelar", + "Parallel Multi-Aquarium": "Multi-Aquário Paralelo", + "Single Aquarium": "Aquário Único", + "Sequential Sessions": "Sessões Sequenciais", + + "Detecting Aquarium": "Detectando Aquário", + "⏳ Detecting the Aquarium Automatically": "⏳ Detectando Aquário Automaticamente", + "Session: {experiment}\nAnalyzing frames to identify the aquarium region...": "Sessão: {experiment}\nAnalisando frames para identificar região do aquário...", + "Detections:": "Detecções:", + "✓ Valid: {count}": "✓ Válidas: {count}", + "✗ Invalid: {count}": "✗ Inválidas: {count}", + "Last Frame Analyzed": "Último Frame Analisado", + "Starting detection...": "Iniciando detecção...", + "✓ Aquarium detected (valid area)": "✓ Aquário detectado (área válida)", + "✗ Detection ignored (insufficient area)": "✗ Detecção ignorada (área insuficiente)", + "VALID": "VÁLIDO", + "INVALID": "INVÁLIDO", + + "Project Model Tools": "Ferramentas de Modelos do Projeto", + "Calibration and Diagnostics": "Calibração e Diagnóstico", + "Global Model Configuration": "Configuração Global de Modelos", + "📐 Project Model Tools": "📐 Ferramentas de Modelos do Projeto", + "📐 Calibration and Diagnostics": "📐 Calibração e Diagnóstico", + "📐 Global Model Configuration": "📐 Configuração Global de Modelos", + "This project's configuration and diagnostic tools were split into two dedicated tabs. This fallback reuses the same split when the main window is not available yet.": "As ferramentas de configuração e diagnóstico deste projeto foram separadas em duas abas dedicadas. Este fallback reutiliza a mesma divisão quando a janela principal ainda não está disponível.", + "AI Model Config.": "Config. Modelo IA", + "AI Model Diagnostics": "Diagnóstico Modelo IA", + "Model Performance Diagnostics": "Diagnóstico de Desempenho do Modelo", + "Apply global defaults": "Aplicar padrões globais", + "Apply the global model defaults to which project?\n\nOpen project: {name}": "Aplicar os padrões globais de modelo a qual projeto?\n\nProjeto aberto: {name}", + "Project updated": "Projeto atualizado", + "The global defaults were copied to project {name}.": "Os padrões globais foram copiados para o projeto {name}.", + "Copy failed": "Falha ao copiar", + "Could not apply the global defaults to the project. Check the log for details.": "Não foi possível aplicar os padrões globais ao projeto. Verifique o log para detalhes.", + "The global defaults were copied to the project at:\n{path}": "Os padrões globais foram copiados para o projeto em:\n{path}", + "Could not apply the global defaults to the selected project.\nCheck that the folder holds a valid ZebTrack project (project_config.json).": "Não foi possível aplicar os padrões globais ao projeto selecionado.\nVerifique se a pasta contém um projeto ZebTrack válido (project_config.json).", + + "Start a New Recording Session": "Iniciar Nova Sessão de Gravação", + "Camera:": "Câmera:", + "All fields are required.": "Todos os campos são obrigatórios.", + + "Aquarium Configuration": "Configuração dos Aquários", + "Assign groups and identifiers to each aquarium": "Atribua grupos e identificadores para cada aquário", + "Apply to every video in the batch": "Aplicar para todos os vídeos do batch", + "Auto-fill": "Auto-Preencher", + "No match found with the current regex pattern:\n{pattern}": "Nenhuma correspondência encontrada com o padrão regex atual:\n{pattern}", + "Found {count} matches in the file name,\nbut only 2 aquariums are supported.\n\nUsing the first 2 matches.": "Encontradas {count} correspondências no nome do arquivo,\nmas apenas 2 aquários são suportados.\n\nUsando as 2 primeiras correspondências.", + "Filled successfully ({count} matches found).": "Preenchido com sucesso ({count} correspondências encontradas).", + "Left": "Esquerda", + "Right": "Direita", + "Aquarium {number} ({position})": "Aquário {number} ({position})", + "The group of Aquarium {number} cannot be empty": "Grupo do Aquário {number} não pode estar vazio", + "The subject of Aquarium {number} cannot be empty": "Sujeito do Aquário {number} não pode estar vazio", + + "Multi-Aquarium Live Analysis - Camera {index}": "Análise ao Vivo Multi-Aquário - Câmera {index}", + "Start: {clock}": "Início: {clock}", + "Camera: {camera} | Aquariums: {count}": "Câmera: {camera} | Aquários: {count}", + "Detections: {count}": "Detecções: {count}", + "Session": "Sessão", + "⏹ Stop Analysis": "⏹ Parar Análise" +} diff --git a/src/zebtrack/locales/_pairs/pr3-dialogs-5e.json b/src/zebtrack/locales/_pairs/pr3-dialogs-5e.json new file mode 100644 index 00000000..9df75970 --- /dev/null +++ b/src/zebtrack/locales/_pairs/pr3-dialogs-5e.json @@ -0,0 +1,99 @@ +{ + "_domain": "zebtrack", + "_comment": "Phase 3, batch 5e -- the last eight files of ui/dialogs: camera_disconnect_recovery_dialog, live_config_dialog, save_roi_template_dialog, live_preview_window, multi_aquarium_confirm_dialog, zone_calibration_dialog, zone_reuse_dialog and missing_metadata_dialog. Also core/recording/live_session_manager.py: that file is already inside the ratchet, yet four of the status strings it pushes into LivePreviewWindow's status label are UNACCENTED Portuguese, so the scanner never saw them -- they sat next to already-translated _() calls on the same label. 'Aquarium Setup' is deliberately NOT the existing 'Aquarium Configuration' msgid: that one belongs to aquarium_assignment_dialog and carries 'Configuracao dos Aquarios', a second spelling of a different dialog's title. live_config_dialog's 'Porta Arduino:' DID collapse into the wizard's existing 'Arduino Port:' msgid, since both label the same field.", + + "Missing Metadata": "Metadados Ausentes", + "Metadata could not be found automatically for:": "Não foi possível encontrar metadados automaticamente para:", + "Please enter the details manually:": "Por favor, insira os detalhes manualmente:", + "Day:": "Dia:", + "Group:": "Grupo:", + "Subject (ID):": "Cobaia (ID):", + "Day and Subject (ID) must be whole numbers.": "Dia e Cobaia (ID) devem ser números inteiros.", + "The group name cannot be empty.": "O nome do grupo não pode estar vazio.", + + "Zone Configuration for Recording": "Configuração de Zonas para Gravação", + "How do you want to define the aquarium zone?": "Como deseja definir a zona do aquário?", + "Auto-detection (recommended)": "Auto-detecção (recomendado)", + "Will try to detect the aquarium automatically over 10 frames": "Tentará detectar automaticamente o aquário em 10 frames", + "Manual Drawing": "Desenho Manual", + "You will draw the aquarium polygon manually": "Você desenhará manualmente o polígono do aquário", + "Proceed": "Prosseguir", + + "Reuse Existing Zones?": "Reutilizar Zonas Existentes?", + "Zones have already been defined.\n\nDo you want to reuse them for this recording?\n(Use this if the aquarium has not been moved)": "Zonas já foram definidas anteriormente.\n\nDeseja reutilizar para esta gravação?\n(Use se o aquário não foi movido)", + "Zone Information": "Informações das Zonas", + "✓ Main Arena: {count} vertices": "✓ Arena Principal: {count} vértices", + "✓ ROIs defined: {count}": "✓ ROIs definidas: {count}", + "✓ Calibrated: {width:.1f} x {height:.1f} cm": "✓ Calibrado: {width:.1f} x {height:.1f} cm", + "○ Metric calibration: not defined": "○ Calibração métrica: Não definida", + "• Method: auto-detected": "• Método: Auto-detectado", + "• Method: drawn manually": "• Método: Desenhado manualmente", + "⚠ Metric calibration is recommended for accurate analyses": "⚠ Recomenda-se calibração métrica para análises precisas", + "Redefine": "Redefinir", + "Reuse": "Reutilizar", + + "Aquarium Setup": "Configuração de Aquários", + "How many aquariums are in this video?": "Quantos aquários existem neste vídeo?", + "Select how many aquariums there are so that\nautomatic detection is configured correctly.": "Selecione a quantidade de aquários para configurar\na detecção automática corretamente.", + "1 aquarium (default)": "1 aquário (padrão)", + " Standard detection for videos with 1 aquarium": " Detecção padrão para vídeos com 1 aquário", + "2 aquariums": "2 aquários", + " Detects 2 separate aquariums in the same video\n (each aquarium can belong to a different group)": " Detecta 2 aquários separados no mesmo vídeo\n (cada aquário pode pertencer a grupos diferentes)", + "Confirm": "Confirmar", + + "Save zone template": "Salvar template de zonas", + "Template name:": "Nome do template:", + "Include in the template:": "Incluir no template:", + "Main arena": "Arena principal", + "Regions of Interest (ROIs)": "Regiões de Interesse (ROIs)", + "Save to:": "Salvar em:", + "Current project": "Projeto atual", + "Global settings": "Configurações globais", + "Custom location": "Local personalizado", + "Browse…": "Procurar…", + "Global templates are available to every project. Use a custom location to share one manually.": "Templates globais ficam disponíveis para todos os projetos. Use um local personalizado para compartilhar manualmente.", + "Name required": "Nome obrigatório", + "Enter the template name.": "Informe o nome do template.", + "Incomplete selection": "Seleção incompleta", + "Choose at least the arena or the ROIs to save in the template.": "Escolha ao menos a arena ou as ROIs para salvar no template.", + "Location not set": "Local não definido", + "Select the file where the template will be saved.": "Selecione o arquivo onde o template será salvo.", + "Zone template": "Template de zonas", + + "Camera Disconnected": "Câmera Desconectada", + "The camera was disconnected during session '{experiment}'.\n\nGap detected after {seconds:.1f}s without valid frames.\nRecording was paused automatically to avoid invalid data.": "A câmera foi desconectada durante a sessão '{experiment}'.\n\nGap detectado após {seconds:.1f}s sem frames válidos.\nA gravação foi pausada automaticamente para evitar dados inválidos.", + "Trying to reconnect automatically in {seconds}s...": "Tentando reconectar automaticamente em {seconds}s...", + "⏱️ Wait for Reconnection (30s)": "⏱️ Aguardar Reconexão (30s)", + "🔌 Resume Manually": "🔌 Retomar Manualmente", + "⏹️ Stop Session": "⏹️ Parar Sessão", + "\nOptions:\n• Wait: tries to reconnect automatically (30s)\n• Resume: carry on recording after reconnecting the camera manually\n• Stop: ends the session and saves the data collected so far": "\nOpções:\n• Aguardar: Tenta reconectar automaticamente (30s)\n• Retomar: Continue gravação após reconectar câmera manualmente\n• Parar: Finaliza sessão e salva dados coletados até agora", + "Waiting for the camera to reconnect...": "Aguardando reconexão da câmera...", + + "Live Analysis Configuration": "Configuração da Análise ao Vivo", + "No camera found": "Nenhuma câmera encontrada", + "Use Arduino": "Usar Arduino", + "No serial port found": "Nenhuma porta encontrada", + " [no handshake]": " [sem handshake]", + "No camera detected. A live session cannot be started.": "Nenhuma câmera detectada. Não é possível iniciar uma sessão ao vivo.", + "Arduino is enabled, but no serial port was found. Please check the connection or turn off the '{option}' option.": "O Arduino está ativado, mas nenhuma porta serial foi encontrada. Por favor, verifique a conexão ou desative a opção '{option}'.", + + "Live Analysis - Camera {index}": "Análise ao Vivo - Câmera {index}", + "Camera: {index}": "Câmera: {index}", + "Time: waiting...": "Tempo: Aguardando...", + "● Recording": "● Gravando", + "Waiting for frames...": "Aguardando frames...", + "Statistics": "Estatísticas", + "Frames processed:": "Frames processados:", + "Objects detected:": "Objetos detectados:", + "Frames recorded:": "Frames gravados:", + "Start:": "Início:", + "End:": "Fim:", + "⏹ Stop Recording": "⏹ Parar Gravação", + "Time: {elapsed:.1f}s / {total:.1f}s (Remaining: {remaining:.1f}s)": "Tempo: {elapsed:.1f}s / {total:.1f}s (Restante: {remaining:.1f}s)", + "● Time Expired": "● Tempo Expirado", + "● Stopped": "● Parado", + + "⏳ Loading detector...": "⏳ Carregando detector...", + "⏳ Starting in {seconds}s...": "⏳ Iniciando em {seconds}s...", + "⏳ Starting capture...": "⏳ Iniciando captura..." +} diff --git a/src/zebtrack/locales/_pairs/pr3-ui-shell.json b/src/zebtrack/locales/_pairs/pr3-ui-shell.json new file mode 100644 index 00000000..83355765 --- /dev/null +++ b/src/zebtrack/locales/_pairs/pr3-ui-shell.json @@ -0,0 +1,11 @@ +{ + "_domain": "zebtrack", + "_comment": "Phase 3, batch 2: ui/gui.py and ui/ui_coordinator.py. Ten of the sixteen msgids this batch needs already existed with IDENTICAL Portuguese (Error, No analysis in progress., No task in progress., Analysis configuration: {value}, No Project, Open a project before adjusting the specific calibration., Form reloaded, Values restored to reflect the current settings., Failed to save camera, Could not save the camera as the project default:) because gui.py duplicates dialogs and state defaults that dialog_manager / analysis_controls / state_synchronizer already own. They are reused, not repeated here. 'This project is not configured for live experimental tracking.' was already English in the source but never wrapped, so its Portuguese is a genuine translation rather than a relocated literal.", + + "An unknown error occurred.": "Ocorreu um erro desconhecido.", + "This project is not configured for live experimental tracking.": "Este projeto não está configurado para rastreamento experimental ao vivo.", + "Camera reconnected (gap: {seconds:.1f}s)": "Câmera reconectada (gap: {seconds:.1f}s)", + "Detecting aquarium: frame {frame}/100": "Detectando aquário: frame {frame}/100", + "Batch report generated ({count} session): {batch_id}": "Relatório de lote gerado ({count} sessão): {batch_id}", + "Batch report generated ({count} sessions): {batch_id}": "Relatório de lote gerado ({count} sessões): {batch_id}" +} diff --git a/src/zebtrack/locales/_pairs/pr3-wizard-4b.json b/src/zebtrack/locales/_pairs/pr3-wizard-4b.json new file mode 100644 index 00000000..c5204e40 --- /dev/null +++ b/src/zebtrack/locales/_pairs/pr3-wizard-4b.json @@ -0,0 +1,215 @@ +{ + "_domain": "zebtrack", + "_comment": "Phase 3, batch 4b: confirmation_step.py and live_config_step.py, plus two files they forced open. templates.py: format_template_banner grew a companion format_template_banner_details() because ConfirmationStep obtained the bare template identity by calling .replace('Template carregado: ', '') on the RENDERED banner — translating the prefix would have silently left the prefix in the summary. coordinators/{live_camera_session,recording_session}_coordinator.py: both publish 'Aguardando sinal externo... (porta N)' as a UI status; coordinators/ is already in MIGRATED_PATHS but the string has no accents, so the accent-only scanner and the ratchet built on it never saw it. The live_config tooltip used to retype that same status as prose; it now interpolates the msgid. Reused from the corpus and therefore absent here: Error, Error Saving, Enabled, Disabled, ' • Total videos: {count}' (whose stored Portuguese lowercases 'vídeos'; the two spellings are now one). '✗ pyserial não instalado' and '✗ pyserial não disponível' were two spellings of one failure and collapse onto one msgid.", + + "Project Confirmation and Creation": "Confirmação e Criação do Projeto", + "Review the settings and create your project.": "Revise as configurações e crie seu projeto.", + "Project Name:": "Nome do Projeto:", + "Location:": "Localização:", + "Browse...": "Procurar...", + "Project Summary": "Resumo do Projeto", + "💾 Save as Template": "💾 Salvar como Template", + "💡 Tip: Review every setting before creating the project. You can save it as a template to reuse later.": "💡 Dica: Verifique todas as configurações antes de criar o projeto. Você pode salvar como template para reutilizar.", + + "Experiment_{group}": "Experimento_{group}", + "Experimental_Project": "Projeto_Experimental", + "Exploratory_Project": "Projeto_Exploratorio", + "Select the Project Folder": "Selecione a Pasta do Projeto", + + "📝 Template Loaded:": "📝 Template Carregado:", + " • Created at: {value}": " • Criado em: {value}", + " • Template version: {value}": " • Versão do template: {value}", + + "📋 Project Type:": "📋 Tipo de Projeto:", + "Experimental (pre-recorded)": "Experimental (pré-gravado)", + "Exploratory (pre-recorded)": "Exploratório (pré-gravado)", + "Live (real time)": "Ao Vivo (tempo real)", + + "🔬 Experimental Design:": "🔬 Design Experimental:", + " • {groups} groups x {days} days x {subjects} animals/group": " • {groups} grupos x {days} dias x {subjects} animais/grupo", + " • Total: {sessions} recordings ({animals} animals)": " • Total: {sessions} gravações ({animals} animais)", + " • Groups: {groups}": " • Grupos: {groups}", + + "📹 Hardware:": "📹 Hardware:", + " • Camera: {name} (index {index})": " • Câmera: {name} (índice {index})", + " • Camera: index {index}": " • Câmera: Índice {index}", + " • Arduino: {port}": " • Arduino: {port}", + " • Mode: External Trigger ✓": " • Modo: Gatilho Externo (External Trigger) ✓", + + "⏱️ Recording Settings:": "⏱️ Configurações de Gravação:", + " • Timed recording: {minutes}min {seconds}s": " • Gravação temporizada: {minutes}min {seconds}s", + " • Countdown: {seconds}s": " • Contagem regressiva: {seconds}s", + + "⚙️ Processing Intervals:": "⚙️ Intervalos de Processamento:", + " • Analysis: every {count} frames": " • Análise: a cada {count} frames", + " • Display: every {count} frames": " • Exibição: a cada {count} frames", + + "🎯 Detection Configuration:": "🎯 Configuração de Detecção:", + " • Aquarium weight: {weight}": " • Peso aquário: {weight}", + " • Animal weight: {weight}": " • Peso animais: {weight}", + + "🔍 Detected Design:": "🔍 Design Detectado / Design:", + " • Groups: {count} ({preview}{suffix})": " • Grupos: {count} ({preview}{suffix})", + " • Days: {count}": " • Dias: {count}", + " • Confidence: {value}": " • Confiança: {value}", + + "Groups": "Grupos", + "Days": "Dias", + "Subjects": "Sujeitos", + "🧩 Custom Regex:": "🧩 Regex Personalizada:", + + "Segmentation (seg)": "Segmentação (seg)", + "Detection (det)": "Detecção (det)", + "🎯 Detection Settings:": "🎯 Configurações de Detecção:", + " • Aquarium method: {method}": " • Método aquário: {method}", + " • Animal method: {method}": " • Método animais: {method}", + " • OpenVINO: {status}": " • OpenVINO: {status}", + + "🌳 Folder Structure (preview):": "🌳 Estrutura de Pastas (prévia):", + " • (+ 1 additional selection)": " • (+ 1 seleção adicional)", + " • (+ {count} additional selections)": " • (+ {count} seleções adicionais)", + + "📏 Physical Calibration:": "📏 Calibração Física:", + " • Aquariums: {count}": " • Aquários: {count}", + " • Animals per aquarium: {count}": " • Animais por aquário: {count}", + " • Dimensions: {width} x {height} cm": " • Dimensões: {width} x {height} cm", + + "⚙️ Processing Plan:": "⚙️ Plano de Processamento:", + "Skip (complete data)": "Skip (dados completos)", + "Import Zones + track": "Import Zones + rastrear", + "Partial (arena only)": "Partial (arena apenas)", + "Full (process from scratch)": "Full (processar do zero)", + " • 1 video: {name}": " • 1 vídeo: {name}", + " • {count} videos: {name}": " • {count} vídeos: {name}", + "⏱️ Estimated time: ~{minutes} minutes": "⏱️ Tempo Estimado: ~{minutes} minutos", + " (1 video to process)": " (1 vídeo para processar)", + " ({count} videos to process)": " ({count} vídeos para processar)", + + "📦 Existing Parquets:": "📦 Parquets Existentes:", + " • Scope: {scope}": " • Escopo: {scope}", + " • Arena: {count}": " • Arena: {count}", + " • ROIs: {count}": " • ROIs: {count}", + " • Trajectory: {count}": " • Trajetória: {count}", + " • Complete: {count}": " • Completos: {count}", + + "📥 Import Configuration:": "📥 Configuração de Importação:", + " ✅ Arena: 1 video": " ✅ Arena: 1 vídeo", + " ✅ Arena: {count} videos": " ✅ Arena: {count} vídeos", + " ✅ ROIs: 1 video": " ✅ ROIs: 1 vídeo", + " ✅ ROIs: {count} videos": " ✅ ROIs: {count} vídeos", + " ✅ Trajectory: 1 video": " ✅ Trajetória: 1 vídeo", + " ✅ Trajectory: {count} videos": " ✅ Trajetória: {count} vídeos", + + "Replace existing ROIs": "Substituir ROIs existentes", + "Merge (keep both)": "Mesclar (manter ambos)", + "Manual conflict resolution": "Resolução manual de conflitos", + "🔀 ROI Strategy:": "🔀 Estratégia de ROIs:", + + "(selection)": "(seleção)", + "1 folder": "1 pasta", + "{count} folders": "{count} pastas", + "1 file": "1 arquivo", + "{count} files": "{count} arquivos", + "empty": "vazio", + " … Preview truncated (full details in step 2)": " … Prévia limitada (detalhes completos na etapa 2)", + + "Save Template": "Salvar Template", + "Enter a name for the template:": "Digite um nome para o template:", + "Save Wizard Template": "Salvar Template do Wizard", + "Wizard Templates": "Templates do Wizard", + "Template '{name}' saved successfully!\n\nFile: {path}\n\nYou will be able to load this template later to create similar projects quickly.": "Template '{name}' salvo com sucesso!\n\nArquivo: {path}\n\nVocê poderá carregar este template no futuro para criar projetos similares rapidamente.", + "Template Saved": "Template Salvo", + "Could not save the template '{name}'.\n\nCheck the logs for more details.": "Não foi possível salvar o template '{name}'.\n\nVerifique os logs para mais detalhes.", + + "Please enter a name for the project.": "Por favor, informe um nome para o projeto.", + "The project name contains invalid characters. Use only letters, digits, spaces, '_' and '-'.": "Nome do projeto contém caracteres inválidos. Use apenas letras, números, espaços, '_' e '-'.", + "Please select a location for the project.": "Por favor, selecione uma localização para o projeto.", + "Location does not exist: {location}": "Localização não existe: {location}", + "No write permission at the location: {location}": "Sem permissão de escrita na localização: {location}", + "The project name is too long for the file system.": "Nome do projeto é muito longo para o sistema de arquivos.", + "A file with that name already exists at: {location}": "Já existe um arquivo com esse nome em: {location}", + "A project with that name already exists at: {location}": "Já existe um projeto com esse nome em: {location}", + "No video selected. Go back and select videos.": "Nenhum vídeo selecionado. Volte e selecione vídeos.", + "Configure the camera in the previous step before creating the project.": "Configure a câmera na etapa anterior antes de criar o projeto.", + + "Template loaded: {details}": "Template carregado: {details}", + + "Live Recording Configuration": "Configuração de Gravação ao Vivo", + "Set up the camera and the recording options for the live project.": "Configure a câmera e as opções de gravação para o projeto ao vivo.", + "Camera Configuration": "Configuração de Câmera", + "Select Camera:": "Selecionar Câmera:", + "Select the camera for live recording.": "Selecione a câmera para gravação ao vivo.", + "🔍 Detect Cameras": "🔍 Detectar Câmeras", + + "Arduino Configuration (Optional)": "Configuração de Arduino (Opcional)", + "Use Arduino for synchronization": "Usar Arduino para sincronização", + "Enable Arduino to synchronize external events with the recording.": "Habilitar Arduino para sincronizar eventos externos com a gravação.", + "Arduino Port:": "Porta do Arduino:", + "🔍 Detect": "🔍 Detectar", + "🔌 Test": "🔌 Testar", + "External Trigger Mode": "Modo de Gatilho Externo (External Trigger)", + "Waiting for external signal": "Aguardando sinal externo", + "External Trigger Mode\n\nWhat starts the recording becomes the Arduino, not the operator.\n\nWhen a session starts the app does NOT record straight away: it shows '{waiting}' and stays idle until the Arduino sends code 1 over the serial line. Code 0 ends the recording.\n\nUseful to synchronize the start with a stimulus, a gate or another instrument.\n\nRequires a connected Arduino and the port selected above — without them the session is refused, not started blind.": "Modo de Gatilho Externo\n\nQuem dá a partida na gravação passa a ser o Arduino, não o operador.\n\nAo iniciar uma sessão, o app NÃO grava de imediato: ele exibe '{waiting}' e fica parado até o Arduino enviar o código 1 pela serial. O código 0 encerra a gravação.\n\nÚtil para sincronizar o início com um estímulo, um portão ou outro equipamento.\n\nRequer Arduino conectado e a porta selecionada acima — sem isso a sessão é recusada, não iniciada às cegas.", + + "Recording Settings": "Configurações de Gravação", + "Use timed recording": "Usar gravação temporizada", + "Enable recording with a fixed duration (stops automatically after the specified time).": "Habilitar gravação com duração fixa (desliga automaticamente após o tempo especificado).", + "Recording duration (seconds):": "Duração da gravação (segundos):", + "Total recording duration in seconds (e.g. 300 = 5 minutes).": "Duração total da gravação em segundos (ex: 300 = 5 minutos).", + "Use a countdown before starting": "Usar contagem regressiva antes de iniciar", + "Show a countdown before starting the recording.": "Mostrar contagem regressiva antes de iniciar a gravação.", + "Countdown duration (seconds):": "Duração da contagem (segundos):", + "Countdown duration in seconds.": "Duração da contagem regressiva em segundos.", + + "⚙️ Advanced Processing Settings": "⚙️ Configurações Avançadas de Processamento", + "Preserve the real aquarium shape (segmentation)": "Preservar formato real do aquário (segmentação)", + "Preserve the real aquarium shape\n\nWhen enabled, keeps the real polygon (N vertices) detected by the YOLO segmentation mask instead of reducing the aquarium to a 4-corner rectangle.\n\nRecommended for circular, hexagonal or irregularly shaped aquariums. Requires a segmentation (seg) model — for detection (det) models this option is ignored.": "Preservar formato real do aquário\n\nQuando habilitado, mantém o polígono real (N vértices) detectado pela máscara de segmentação YOLO, em vez de reduzir o aquário a um retângulo de 4 cantos.\n\nRecomendado para aquários circulares, hexagonais ou de formato irregular. Requer um modelo de segmentação (seg) — para modelos de detecção (det) esta opção é ignorada.", + + "About Live Projects": "Sobre Projetos ao Vivo", + "Live projects record straight from the camera in real time.\n\n• The camera must be connected before creating the project\n• Arduino is optional and used to synchronize external events\n• Timed recording stops automatically after the specified time\n• A countdown gives you time to prepare the experiment\n\n💡 Tip: Test the camera before starting the project to make sure it is working.": "Projetos ao vivo gravam diretamente da câmera em tempo real.\n\n• A câmera deve estar conectada antes de criar o projeto\n• Arduino é opcional e usado para sincronizar eventos externos\n• Gravação temporizada desliga automaticamente após o tempo especificado\n• Contagem regressiva dá tempo para preparar o experimento\n\n💡 Dica: Teste a câmera antes de iniciar o projeto para garantir que está funcionando.", + + "Hardware detected:\n\nCapability: {capability}\nCPU: {cores} cores\nRAM: {ram} GB\nGPU: {gpu}\n\n": "Hardware Detectado:\n\nCapacidade: {capability}\nCPU: {cores} cores\nRAM: {ram} GB\nGPU: {gpu}\n\n", + "Yes": "Sim", + "No": "Não", + "⚠️ Your system does NOT support real-time processing.\nRecommendation: use '{mode}' mode (offline).": "⚠️ Seu sistema NÃO suporta processamento em tempo real.\nRecomendação: Use modo '{mode}' (offline).", + "Recording Only": "Apenas Gravação", + "Supported aquariums: {count}\nMulti-aquarium in real time may not be possible.": "Aquários suportados: {count}\nMulti-aquário em tempo real pode não ser possível.", + "Hardware Detection": "Detecção de Hardware", + + "Detecting...": "Detectando...", + "Camera {index}": "Câmera {index}", + "✓ 1 camera detected": "✓ 1 câmera detectada", + "✓ {count} cameras detected": "✓ {count} câmeras detectadas", + "✗ No camera detected": "✗ Nenhuma câmera detectada", + "✓ 1 port detected": "✓ 1 porta detectada", + "✓ {count} ports detected": "✓ {count} portas detectadas", + "✓ 1 port — using {device}": "✓ 1 porta — usando {device}", + "✓ {count} ports — using {device}": "✓ {count} portas — usando {device}", + "✗ No port detected": "✗ Nenhuma porta detectada", + "✗ pyserial not installed": "✗ pyserial não instalado", + + "No Port Selected": "Nenhuma Porta Selecionada", + "Please detect and select an Arduino port first.": "Por favor, detecte e selecione uma porta Arduino primeiro.", + "Testing...": "Testando...", + "✓ Connection OK": "✓ Conexão OK", + "Connection Test": "Teste de Conexão", + "Connection to {port} established successfully!\n\nThe port is reachable and ready to use.": "Conexão com {port} estabelecida com sucesso!\n\nA porta está acessível e pronta para uso.", + "✗ Connection failed": "✗ Falha na conexão", + "Connection Error": "Erro de Conexão", + "Could not connect to port {port}.\n\nError: {error}\n\nCheck that:\n• The Arduino is connected\n• The port is not in use by another program\n• You have permission to access the port": "Não foi possível conectar à porta {port}.\n\nErro: {error}\n\nVerifique se:\n• O Arduino está conectado\n• A porta não está em uso por outro programa\n• Você tem permissão para acessar a porta", + "The pyserial library is not installed.\n\nRun: pip install pyserial": "A biblioteca pyserial não está instalada.\n\nExecute: pip install pyserial", + "✗ Unexpected error": "✗ Erro inesperado", + "Unexpected Error": "Erro Inesperado", + "An error occurred while testing the connection:\n\n{error}": "Ocorreu um erro ao testar a conexão:\n\n{error}", + + "Aquarium Limit": "Limitação de Aquários", + "⚠️ Simultaneous recording is limited to 2 aquariums.\n\nYour project has {count} aquariums configured.\n\nOptions:\n• Reduce to 2 aquariums in the zone configuration\n• Use sequential mode (process aquariums separately)\n• Process offline after recording without detection": "⚠️ Gravação simultânea limitada a 2 aquários.\n\nSeu projeto tem {count} aquários configurados.\n\nOpções:\n• Reduza para 2 aquários na configuração de zonas\n• Use modo sequencial (processar aquários separadamente)\n• Processe offline após gravação sem detecção", + "A project with {count} aquariums exceeds the limit of 2 for simultaneous recording.": "Projeto com {count} aquários excede o limite de 2 para gravação simultânea.", + "Mode selection cancelled. Adjust the number of aquariums or select a compatible mode.": "Seleção de modo cancelada. Por favor, ajuste o número de aquários ou selecione um modo compatível.", + "Error validating data: {error}": "Erro ao validar dados: {error}", + + "⚠ The template's camera is unavailable — selected '{fallback}'. Please check.": "⚠ Câmera do template indisponível — selecionada '{fallback}'. Confira.", + "⚠ The template's port ({port}) is unavailable — select another one or disable the Arduino.": "⚠ Porta do template ({port}) indisponível — selecione outra ou desative o Arduino.", + + "Waiting for external signal... (port {port})": "Aguardando sinal externo... (porta {port})" +} diff --git a/src/zebtrack/locales/_pairs/pr3-wizard-4c.json b/src/zebtrack/locales/_pairs/pr3-wizard-4c.json new file mode 100644 index 00000000..b4b751a2 --- /dev/null +++ b/src/zebtrack/locales/_pairs/pr3-wizard-4c.json @@ -0,0 +1,114 @@ +{ + "_domain": "zebtrack", + "_comment": "Phase 3, batch 4c: detection_step.py and model_selection_step.py. Both carried a module-level label dict (_METHOD_LABELS / _METHOD_OPTIONS) that had to become a function before it could be translated, because a dict literal resolves _() at import time. model_selection_step also had two structural defects: ' ⭐ Recomendado' was appended for display AND stripped back off by _strip_annotation()/the startswith check, so translating the display alone would have glued the marker to the weight name and made validate() reject a weight picked straight from the list -- it now has one definition, _recommended_suffix(); and _refresh_weight_dropdowns bound `_` as a throwaway tuple element two lines above where the marker needed _(), which would have called a WeightManager result as a function. Threshold errors stopped splicing a noun through .capitalize() and became one finished sentence per field. Reused and therefore absent here: Segmentation (seg), Detection (det), Status, Groups, Days, Subjects, Enabled, Disabled, 'OpenVINO device:', ' • Arena: {count}', ' • ROIs: {count}', ' • Trajectory: {count}', ' • Groups: {groups}', ' • Confidence: {value}', ' • Aquarium method: {method}', ' • Animal method: {method}', ' • Aquarium weight: {weight}', ' • Animal weight: {weight}', ' • OpenVINO: {status}'.", + + "Waiting for analysis...": "Aguardando análise...", + "Automatic Design Detection": "Detecção Automática de Design", + "Analyzing folder structure and parquet files...": "Analisando estrutura de pastas e arquivos parquet...", + "Detection Results": "Resultados da Detecção", + "Actions": "Ações", + "🔄 Re-analyze": "🔄 Re-analisar", + "✏️ Edit Design": "✏️ Editar Design", + "🔧 Custom Regex": "🔧 Regex Customizado", + "💡 Tip": "💡 Dica", + "Automatic detection identifies groups, days and subjects from the folder structure.": "A detecção automática identifica grupos, dias e sujeitos baseando-se na estrutura de pastas.", + + "Analyzing (using custom regex)...": "Analisando (usando regex personalizada)...", + "Analyzing...": "Analisando...", + "Analysis complete!": "Análise concluída!", + "Failed to complete detection: {error}": "Falha ao concluir a detecção: {error}", + + "Experimental design detected!\n\nGroups found: {groups}\nDays: {days}\n\nReview or customize the names before continuing.": "Design experimental detectado!\n\nGrupos encontrados: {groups}\nDias: {days}\n\nRevise ou personalize os nomes antes de continuar.", + "Design Detected": "Design Detectado", + "Confirmation Required": "Confirmação Necessária", + "Confirm the group names before moving on.": "Confirme os nomes dos grupos antes de avançar.", + + "📊 Videos found: {count}": "📊 Vídeos Encontrados: {count}", + "📦 Existing Parquet Files:": "📦 Arquivos Parquet Existentes:", + " • Complete (all 3): {count}": " • Completos (todos 3): {count}", + "🎯 Experimental Design Detected:": "🎯 Design Experimental Detectado:", + " • Days: {days}": " • Dias: {days}", + " • Pattern: {pattern}": " • Padrão: {pattern}", + " 📋 Subjects per Group:": " 📋 Sujeitos por Grupo:", + " - {label}: 1 subject": " - {label}: 1 sujeito", + " - {label}: {count} subjects": " - {label}: {count} sujeitos", + "⚠️ Experimental design was not detected automatically.\n\nPossible causes:\n • The folder structure does not follow a recognized pattern\n • Group/day names are not detectable (e.g. Grupo1, Day01)\n\nYou can continue without a detected design, or reorganize the files.\n": "⚠️ Design experimental não detectado automaticamente.\n\nPossíveis causas:\n • Estrutura de pastas não segue padrões reconhecidos\n • Nomes de grupos/dias não são detectáveis (ex: Grupo1, Day01)\n\nVocê pode prosseguir sem design detectado ou reorganizar os arquivos.\n", + "ℹ️ Design detection disabled (exploratory project).": "ℹ️ Detecção de design desativada (projeto exploratório).", + "🧩 Custom regex in use:": "🧩 Regex personalizada em uso:", + "⚙️ Current Detector Configuration:": "⚙️ Configuração Atual do Detector:", + + "Error!": "Erro!", + "❌ Error: {message}": "❌ Erro: {message}", + + "Custom regex applied ✓": "Regex personalizado aplicado ✓", + "The custom regex found no design; adjust the patterns or edit manually.": "Regex personalizado não encontrou design; ajuste os padrões ou edite manualmente.", + "Custom regex removed. Default detection reapplied ✓": "Regex personalizado removido. Detecção padrão reaplicada ✓", + "Default detection reapplied, but no design was found.": "Detecção padrão reaplicada, mas nenhum design foi encontrado.", + "Design edited manually ✓ (custom regex applied)": "Design editado manualmente ✓ (regex personalizado aplicado)", + "Design edited manually ✓ (default regex)": "Design editado manualmente ✓ (regex padrão)", + + "No video was found. Go back and select valid videos.": "Nenhum vídeo foi encontrado. Volte e selecione vídeos válidos.", + "Confirm the group names in the editor before moving on.": "Confirme os nomes dos grupos no editor antes de avançar.", + "Previous results (use '{button}' to refresh)": "Resultados anteriores (use '{button}' para atualizar)", + + " ⭐ Recommended": " ⭐ Recomendado", + "Models and Weights": "Modelos e Pesos", + "Adjust how ZebTrack will use each detection model.\nIf you prefer, keep the recommended defaults and move on.": "Ajuste como o ZebTrack utilizará cada modelo de detecção.\nSe preferir, mantenha os padrões recomendados e avance.", + "Methods and Weights per Role": "Métodos e Pesos por Função", + "Aquarium (arena detection)": "Aquário (detecção de arena)", + "Animals (tracking)": "Animais (rastreamento)", + + "Acceleration / OpenVINO": "Aceleração / OpenVINO", + "Use OpenVINO (requires converting the weight)": "Usar OpenVINO (requer conversão do peso)", + "Enable this once the matching OpenVINO model has been converted. It allows faster inference on compatible CPUs.": "Ative quando o modelo OpenVINO correspondente já foi convertido. Permite inferência mais rápida em CPUs compatíveis.", + "AUTO lets OpenVINO choose the target automatically.\nPick CPU/GPU/NPU to force the target, when available.": "AUTO usa seleção automática do OpenVINO.\nEscolha CPU/GPU/NPU para forçar o alvo, quando disponível.", + + "Detection Parameters (YOLO)": "Parâmetros de Detecção (YOLO)", + "Minimum confidence (0-1):": "Confiança mínima (0-1):", + "🎯 Minimum Confidence (Confidence Threshold)\n\nFilters out detections the model is unsure about.\n\n• HIGH value (0.5-0.9): fewer detections, more precise\n → Use when: large animals, clear contrast\n → Downside: may lose fast-moving animals\n\n• LOW value (0.1-0.4): more detections, less precise\n → Use when: small animals, low contrast\n → Downside: more false positives (noise)\n\n💡 Recommended default: 0.25": "🎯 Confiança Mínima (Confidence Threshold)\n\nFiltra detecções com baixa certeza do modelo.\n\n• Valor ALTO (0.5-0.9): Menos detecções, mais precisas\n → Use quando: Animais grandes, contraste claro\n → Problema: Pode perder animais em movimento rápido\n\n• Valor BAIXO (0.1-0.4): Mais detecções, menos precisas\n → Use quando: Animais pequenos, baixo contraste\n → Problema: Mais falsos positivos (ruído)\n\n💡 Padrão recomendado: 0.25", + "NMS (overlap, 0-1):": "NMS (sobreposição, 0-1):", + "🔲 NMS - Non-Maximum Suppression\n\nRemoves duplicate boxes on the same object.\n\n• HIGH value (0.6-0.9): allows more overlap\n → Use when: animals are very close together\n → Downside: several detections on the same animal\n\n• LOW value (0.1-0.4): removes overlaps aggressively\n → Use when: animals are well separated\n → Downside: may merge nearby animals\n\n💡 Recommended default: 0.45": "🔲 NMS - Non-Maximum Suppression\n\nElimina caixas duplicadas no mesmo objeto.\n\n• Valor ALTO (0.6-0.9): Permite mais sobreposição\n → Use quando: Animais muito próximos\n → Problema: Múltiplas detecções no mesmo animal\n\n• Valor BAIXO (0.1-0.4): Remove sobreposições agressivamente\n → Use quando: Animais bem separados\n → Problema: Pode unir animais próximos\n\n💡 Padrão recomendado: 0.45", + + "Tracking Parameters (ByteTrack)": "Parâmetros de Rastreamento (ByteTrack)", + "Use ByteTrack (Recommended)": "Usar ByteTrack (Recomendado)", + "Enables the ByteTrack algorithm for robust tracking with a Kalman filter.\nRecommended for most experiments.": "Ativa o algoritmo ByteTrack para rastreamento robusto com Filtro de Kalman.\nRecomendado para a maioria dos experimentos.", + "Track Threshold (0-1):": "Track Threshold (0-1):", + "🛤️ Track Threshold\n\nMinimum confidence to START or KEEP a trajectory.\nLow values help keep the trail of animals that are hard to detect.\n\n💡 Default: 0.25": "🛤️ Track Threshold\n\nConfiança mínima para INICIAR ou MANTER uma trajetória.\nValores baixos ajudam a manter o rastro de animais difíceis de detectar.\n\n💡 Padrão: 0.25", + "Match Threshold (0-1):": "Match Threshold (0-1):", + "🔗 Match Threshold\n\nTolerance when associating boxes.\nHIGH values (close to 1.0) are more permissive for fast movement.\n\n💡 Default: 0.95 (for fast zebrafish)": "🔗 Match Threshold\n\nTolerância para associação de caixas.\nValores ALTOS (perto de 1.0) são mais permissivos para movimento rápido.\n\n💡 Padrão: 0.95 (para zebrafish rápidos)", + "Track Buffer (frames):": "Track Buffer (frames):", + "🧠 Track Buffer\n\nThe tracker's memory: how many frames an animal may 'vanish' for before its ID is forgotten.\n\n💡 Default: 90 frames (~3 seconds at 30fps)": "🧠 Track Buffer\n\nMemória do rastreador: quantos frames um animal pode 'sumir' antes que seu ID seja esquecido.\n\n💡 Padrão: 90 frames (~3 segundos a 30fps)", + "Max distance (px):": "Distância Máx (px):", + "📏 Maximum Centre Distance\n\nThe furthest (in pixels) an animal may move between frames and still be considered the same one, when overlap fails.\n\n💡 Default: 200.0 px": "📏 Distância Máxima de Centro\n\nDistância máxima (em pixels) que o animal pode se mover entre frames para ser considerado o mesmo, quando a sobreposição falha.\n\n💡 Padrão: 200.0 px", + "IoU Threshold (0-1):": "IoU Threshold (0-1):", + "🔳 IoU Threshold\n\nMinimum overlap to prefer a box match over a distance match.\nFor small, fast fish, low values work better.\n\n💡 Default: 0.1": "🔳 IoU Threshold\n\nSobreposição mínima para preferir 'Match por Caixa' em vez de distância.\nPara peixes pequenos e rápidos, valores baixos são melhores.\n\n💡 Padrão: 0.1", + + "📊 Quick Guide:\n• Track Thresh: ↓ to keep a weak trail\n• Match Thresh: ↑ to accept abrupt movements\n• Buffer: ↑ to 'remember' the fish for longer\n• Distance: ↑ for very fast fish": "📊 Guia Rápido:\n• Track Thresh: ↓ para manter rastro fraco\n• Match Thresh: ↑ para aceitar movimentos bruscos\n• Buffer: ↑ para 'lembrar' do peixe por mais tempo\n• Distância: ↑ para peixes muito rápidos", + "💡 Tip: adjust ONE parameter at a time (±0.05) and test!": "💡 Dica: Ajuste UM parâmetro por vez (±0.05) e teste!", + "Current YOLO defaults: confidence {confidence}, NMS {nms}.": "Padrões atuais YOLO: confiança {confidence}, NMS {nms}.", + "🔄 Restore Recommended Defaults": "🔄 Restaurar Padrões Recomendados", + "Restores every threshold to its recommended default value.\n\nUseful if you have made adjustments and want to start over.": "Restaura todos os thresholds para os valores padrão recomendados.\n\nÚtil se você fez ajustes e quer voltar ao ponto de partida.", + "Tip: keep the defaults if you are still setting up the videos. You can review these values later in the project settings.": "Dica: mantenha os padrões se ainda estiver configurando os vídeos. Você pode revisar esses valores depois nas configurações do projeto.", + + "ℹ️ ByteTrack disabled. The system will use a simplified hybrid tracker that relies only on '{distance}' and '{iou}' to keep the ID stable. Ideal for 1 animal per aquarium.": "ℹ️ ByteTrack desativado. O sistema usará um rastreador híbrido simplificado que utiliza apenas '{distance}' e '{iou}' para manter o ID estável. Ideal para 1 animal/aquário.", + "💡 ByteTrack uses a Kalman filter to predict positions even when the fish briefly disappears. Adjust the fields below for more stability.": "💡 O ByteTrack usa Filtro de Kalman para prever posições mesmo quando o peixe some brevemente. Ajuste os campos abaixo para maior estabilidade.", + + "Segmentation supports several animals per aquarium.\nDetection is optimized for one animal per aquarium and uses ByteTrack.": "Segmentação suporta múltiplos animais por aquário.\nDetecção é otimizada para um animal por aquário e usa ByteTrack.", + "Select the weight file loaded for this role.": "Selecione o arquivo de peso carregado para esta função.", + + "❌ Confidence must be between 0 and 1": "❌ Confiança deve estar entre 0 e 1", + "❌ NMS must be between 0 and 1": "❌ NMS deve estar entre 0 e 1", + "❌ Track must be between 0 and 1": "❌ Track deve estar entre 0 e 1", + "❌ Match must be between 0 and 1": "❌ Associação deve estar entre 0 e 1", + "❌ Value must be a decimal (e.g. 0.25)": "❌ Valor deve ser decimal (ex: 0.25)", + + "⚠️ Detection (det) is recommended for only 1 animal per aquarium. Consider segmentation (seg) for several animals.": "⚠️ Detecção (det) é recomendada para apenas 1 animal por aquário. Considere segmentação (seg) para múltiplos animais.", + + "Enter decimal values between 0 and 1 for the parameters.": "Informe valores decimais entre 0 e 1 para os parâmetros.", + "The confidence parameter must be between 0 and 1.": "O parâmetro de confiança deve estar entre 0 e 1.", + "The NMS parameter must be between 0 and 1.": "O parâmetro de NMS deve estar entre 0 e 1.", + "The track parameter must be between 0 and 1.": "O parâmetro de track deve estar entre 0 e 1.", + "The match parameter must be between 0 and 1.": "O parâmetro de associação deve estar entre 0 e 1.", + "Select a valid weight for the aquarium. The file must match the chosen method.": "Selecione um peso válido para aquário. O arquivo precisa corresponder ao método escolhido.", + "Select a valid weight for the animals. The file must match the chosen method.": "Selecione um peso válido para animais. O arquivo precisa corresponder ao método escolhido." +} diff --git a/src/zebtrack/locales/_pairs/pr3-wizard-4d.json b/src/zebtrack/locales/_pairs/pr3-wizard-4d.json new file mode 100644 index 00000000..267b277c --- /dev/null +++ b/src/zebtrack/locales/_pairs/pr3-wizard-4d.json @@ -0,0 +1,181 @@ +{ + "_domain": "zebtrack", + "_comment": "Phase 3, batch 4d: the last six files in ui/wizard (calibration_step, design_editor_dialog, import_config_step, file_selection_step, custom_regex_dialog, discovery_step). ui/wizard reaches zero with this batch. custom_regex_dialog carried FIVE copies of {'group': 'Grupo', 'day': 'Dia', 'subject': 'Sujeito'} -- heading, result line, waiting placeholder, validation error and live preview -- which now collapse into _field_labels(). file_selection_step built its summary by gluing 'N arquivo(s)' + ' selecionado(s)', where the participle has to agree with a subject that may be masculine, feminine or both; the Portuguese now leads with a label ('Seleção: ...') so nothing has to agree. import_config_step's ROI radio labels keep the English strategy name plus a gloss and their Portuguese differs from confirmation_step's summary wording, so they get their own msgids instead of colliding. 'Full (do zero)' stayed distinct from confirmation_step's 'Full (processar do zero)' for the same reason. Reused and therefore absent here: Cancel, Save, Summary, Actions, 🔧 Custom Regex, 'Error validating data: {error}', 'Skip (complete data)', 'Import Zones + track', 'Partial (arena only)', 'Wizard Templates', '1 file', '{count} files', '1 folder', '{count} folders', 'empty', 'Arena', 'ROIs', 'Trajectory'.", + + "Physical Calibration": "Calibração Física", + "Set the physical dimensions of the arena to convert pixels into centimetres.": "Configure as dimensões físicas da arena para conversão de pixels para centímetros.", + "Video and Animal Configuration": "Configuração de Vídeos e Animais", + "Number of aquariums (videos):": "Número de aquários (vídeos):", + "🎬 Number of Aquariums (Videos)\n\nHow many independent videos will be analyzed in this project.\n\n• Each aquarium = 1 separate video\n• LIVE project: typically 1 (a single recording)\n• PRE-RECORDED project: may be several videos\n\nExamples:\n • 1 aquarium: a single experiment/recording\n • 6 aquariums: 6 different recordings (e.g. 3 groups x 2 days)\n • 24 aquariums: a full battery of experiments\n\n💡 Tip: if you are unsure, start with 1 and add more videos later.": "🎬 Número de Aquários (Vídeos)\n\nQuantos vídeos independentes serão analisados neste projeto.\n\n• Cada aquário = 1 vídeo separado\n• Projeto LIVE: Tipicamente 1 (gravação única)\n• Projeto PRÉ-GRAVADO: Pode ser múltiplos vídeos\n\nExemplos:\n • 1 aquário: Um único experimento/gravação\n • 6 aquários: 6 gravações diferentes (ex: 3 grupos x 2 dias)\n • 24 aquários: Bateria completa de experimentos\n\n💡 Dica: Se não tiver certeza, comece com 1 e adicione mais vídeos depois.", + "Animals per aquarium:": "Animais por aquário:", + "🐟 Animals per Aquarium\n\nHow many animals will be present in EACH video/aquarium.\n\nImpact on the analysis:\n • 1 animal: simplified individual tracking\n → Ideal for: individual behavioural studies\n → Recommended method: detection (det)\n\n • 2-5 animals: moderate multi-animal tracking\n → Ideal for: social interaction, small-group behaviour\n → Recommended method: segmentation (seg)\n\n • 6+ animals: shoal tracking\n → Ideal for: shoal dynamics, collective behaviour\n → Recommended method: segmentation (seg) with high confidence\n\n⚠️ IMPORTANT: this value must be the SAME for every video in the project.\nIf you have videos with different animal counts, create separate projects.\n\n💡 Tip: for several animals, prefer segmentation (seg) in the model selection step.": "🐟 Animais por Aquário\n\nQuantos animais estarão presentes em CADA vídeo/aquário.\n\nImpacto na Análise:\n • 1 animal: Rastreamento individual simplificado\n → Ideal para: Estudos comportamentais individuais\n → Método recomendado: Detecção (det)\n\n • 2-5 animais: Rastreamento multi-animal moderado\n → Ideal para: Interações sociais, comportamento de grupo pequeno\n → Método recomendado: Segmentação (seg)\n\n • 6+ animais: Rastreamento de cardume\n → Ideal para: Dinâmica de cardume, comportamento coletivo\n → Método recomendado: Segmentação (seg) com alta confiança\n\n⚠️ IMPORTANTE: Este valor deve ser o MESMO para todos os vídeos do projeto.\nSe você tem vídeos com números diferentes de animais, crie projetos separados.\n\n💡 Dica: Para múltiplos animais, prefira segmentação (seg) no passo de seleção de modelo.", + "Physical Aquarium Dimensions": "Dimensões Físicas do Aquário", + "Width (cm):": "Largura (cm):", + "📏 Aquarium Width (horizontal axis)\n\nThe REAL physical size of the arena visible in the video, in centimetres.\n\nHow to measure:\n 1. Identify the area visible in the video (inside the field of view)\n 2. Measure the HORIZONTAL width of that area with a ruler/tape\n 3. Measure in a straight line, from the left side to the right\n\nTypical values:\n • Larvae (Petri dish): 5-10 cm\n • Adults (small tank): 15-30 cm\n • Adults (medium tank): 30-50 cm\n • Large experimental setup: 50-100 cm\n\nUse in the analysis:\n • Converts pixel coordinates → centimetres\n • Allows real travelled distances to be computed\n • Essential for speed (cm/s) and acceleration\n • Required to compare experiments filmed with different cameras\n\n💡 Tip: if you do not know exactly, use an estimate. You can adjust it later.": "📏 Largura do Aquário (eixo horizontal)\n\nDimensão física REAL da arena visível no vídeo, medida em centímetros.\n\nComo Medir:\n 1. Identifique a área visível no vídeo (dentro do campo de visão)\n 2. Meça a largura HORIZONTAL dessa área com régua/trena\n 3. Meça em linha reta, do lado esquerdo ao direito\n\nValores Típicos:\n • Larvas (Petri dish): 5-10 cm\n • Adultos (aquário pequeno): 15-30 cm\n • Adultos (aquário médio): 30-50 cm\n • Setup experimental grande: 50-100 cm\n\nUso na Análise:\n • Converte coordenadas de pixels → centímetros\n • Permite calcular distâncias reais percorridas\n • Essencial para velocidade (cm/s) e aceleração\n • Necessário para comparar experimentos com câmeras diferentes\n\n💡 Dica: Se não souber exatamente, use uma estimativa. Você pode ajustar depois.", + "Height (cm):": "Altura (cm):", + "📏 Aquarium Height (vertical axis)\n\nThe REAL physical size of the arena visible in the video, in centimetres.\n\nHow to measure:\n 1. Identify the area visible in the video (inside the field of view)\n 2. Measure the VERTICAL height of that area with a ruler/tape\n 3. Measure in a straight line, from top to bottom\n\nTypical values:\n • Larvae (Petri dish): 5-10 cm\n • Adults (small tank): 10-20 cm\n • Adults (medium tank): 20-40 cm\n • Large experimental setup: 40-80 cm\n\nUse in the analysis:\n • Converts pixel coordinates → centimetres\n • Allows real vertical distances to be computed\n • Essential for heatmaps at real scale\n • Required for spatial metrics (time in zones, etc.)\n\n⚠️ IMPORTANT: width and height must describe the SAME arena.\nUse the dimensions of the area VISIBLE in the video, not of the whole tank.\n\n💡 Tip: for a top-down camera, width ≈ height (a square/rectangular field of view).": "📏 Altura do Aquário (eixo vertical)\n\nDimensão física REAL da arena visível no vídeo, medida em centímetros.\n\nComo Medir:\n 1. Identifique a área visível no vídeo (dentro do campo de visão)\n 2. Meça a altura VERTICAL dessa área com régua/trena\n 3. Meça em linha reta, de cima para baixo\n\nValores Típicos:\n • Larvas (Petri dish): 5-10 cm\n • Adultos (aquário pequeno): 10-20 cm\n • Adultos (aquário médio): 20-40 cm\n • Setup experimental grande: 40-80 cm\n\nUso na Análise:\n • Converte coordenadas de pixels → centímetros\n • Permite calcular distâncias verticais reais\n • Essencial para mapas de calor em escala real\n • Necessário para métricas espaciais (tempo em zonas, etc.)\n\n⚠️ IMPORTANTE: Largura e altura devem corresponder à MESMA arena.\nUse as dimensões da área VISÍVEL no vídeo, não do aquário todo.\n\n💡 Dica: Para câmera superior (top-down), largura ≈ altura (campo de visão quadrado/retangular).", + "⚙️ Advanced Settings": "⚙️ Configurações Avançadas", + "Analysis interval (frames):": "Intervalo de Análise (frames):", + "🎬 Analysis Interval\n\nProcesses 1 frame out of every N original frames.\n\n• N=1: analyzes every frame (maximum precision, slowest)\n• N=10: analyzes 1 frame and skips 9 (faster, ideal for long videos)\n\n💡 Tip: use 5 or 10 for a good balance between speed and precision.": "🎬 Intervalo de Análise\n\nProcessa 1 frame a cada N frames originais.\n\n• N=1: Analisa todos os frames (máxima precisão, mais lento)\n• N=10: Analisa 1 frame e pula 9 (mais rápido, ideal para vídeos longos)\n\n💡 Dica: Use 5 ou 10 para um bom equilíbrio entre velocidade e precisão.", + "🧠 Behavioural Analysis": "🧠 Análise Comportamental", + "About Calibration": "Sobre a Calibração", + "Physical calibration makes it possible to convert pixel coordinates into centimetres.\n\nThis is needed to:\n• Compute real travelled distances\n• Compute speeds in cm/s\n• Compare results across different camera setups\n\n💡 Tip: if you do not know the exact dimensions, you can use the default values and adjust them later in the project settings.": "A calibração física permite converter coordenadas de pixels para centímetros.\n\nIsso é necessário para:\n• Calcular distâncias percorridas reais\n• Calcular velocidades em cm/s\n• Comparar resultados entre diferentes configurações de câmera\n\n💡 Dica: Se você não souber as dimensões exatas, pode usar valores padrão e ajustar depois nas configurações do projeto.", + + "Edit Experimental Design": "Editar Design Experimental", + "Manual Editing of the Experimental Design": "Edição Manual do Design Experimental", + "Experimental Groups:": "Grupos Experimentais:", + "Set the original ID and the descriptive name for each group.": "Configure o ID original e o nome descritivo para cada grupo.", + "Original ID": "ID Original", + "Display Name": "Nome para Exibição", + "New group ID:": "Novo Grupo ID:", + "Name:": "Nome:", + "+ Add Group": "+ Adicionar Grupo", + "Days:": "Dias:", + "Add Day": "Adicionar Dia", + "Remove Selected": "Remover Selecionado", + "💡 Tip: give the groups friendly names. Subjects are derived automatically from the files.": "💡 Dica: Configure os grupos com nomes amigáveis. Os sujeitos são derivados automaticamente dos arquivos.", + "Save": "Salvar", + "Regex with no results": "Regex sem resultados", + "The custom regex patterns did not identify the design automatically. Adjust the patterns or edit it manually.": "Os padrões regex personalizados não identificaram automaticamente o design. Ajuste os padrões ou edite manualmente.", + "Empty ID": "ID Vazio", + "Enter an ID for the new group.": "Digite um ID para o novo grupo.", + "Duplicate ID": "ID Duplicado", + "The group '{group}' already exists.": "O grupo '{group}' já existe.", + "Remove group '{group}'?": "Remover grupo '{group}'?", + "Empty Day": "Dia Vazio", + "Please enter a name for the day.": "Por favor, digite um nome para o dia.", + "Duplicate Day": "Dia Duplicado", + "The day '{day}' already exists.": "O dia '{day}' já existe.", + "No Day Selected": "Nenhum Dia Selecionado", + "Select a day to remove.": "Selecione um dia para remover.", + "Are you sure you want to remove the day '{day}'?": "Tem certeza que deseja remover o dia '{day}'?", + "At least one group is required.": "É necessário ter pelo menos um grupo.", + + "Import Configuration": "Configuração de Importação", + "Choose what to import for each video.": "Configure o que importar para cada vídeo.", + "Import All Arenas": "Importar Todas Arenas", + "Import All ROIs": "Importar Todos ROIs", + "Import All Trajectories": "Importar Todas Trajetórias", + "Import Everything": "Importar Tudo", + "Videos and Strategies": "Vídeos e Estratégias", + "Video": "Vídeo", + "Action": "Ação", + "ROI Strategy": "Estratégia ROIs", + "Replace (overwrite)": "Replace (substituir)", + "Imported ROIs completely replace the existing ones.": "ROIs importados substituem completamente as existentes.", + "Merge (keep both ROIs)": "Merge (manter ambos)", + "Keep both. Conflicts will be renamed.": "Manter ambos. Conflitos serão renomeados.", + "Manual (ask)": "Manual (perguntar)", + "Ask for each conflict.": "Perguntar para cada conflito.", + "Legend": "Legenda", + "🏟 Arena | 🎯 ROIs | 🧭 Trajectory\n✓ Import | ⏸ Do not import\n✗ Unavailable": "🏟 Arena | 🎯 ROIs | 🧭 Trajetória\n✓ Importar | ⏸ Não importar\n✗ Não disponível", + "💡 Tip: adjust quickly by double-clicking the column you want. Use the bulk import buttons to apply the same pattern to every video.": "💡 Dica: ajuste rapidamente clicando 2x na coluna desejada. Use os botões de importação em lote para aplicar o mesmo padrão a todos os vídeos.", + "Full (from scratch)": "Full (do zero)", + "• 1 video: {name}": "• 1 vídeo: {name}", + "• {count} videos: {name}": "• {count} vídeos: {name}", + "No video configured": "Nenhum vídeo configurado", + "No video to configure. Go back and select videos.": "Nenhum vídeo para configurar. Volte e selecione vídeos.", + "Video {name} has no action defined.": "Vídeo {name} não possui ação definida.", + + "No video/folder selected.": "Nenhum vídeo/pasta selecionado.", + "Video Selection": "Seleção de Vídeos", + "Select the videos you want to analyze (individual files or whole folders).": "Selecione os vídeos que deseja analisar (arquivos individuais ou pastas inteiras).", + "📁 Add Files...": "📁 Adicionar Arquivos...", + "Select individual videos (.mp4, .avi, .mov). Supports multiple selection (Ctrl+Click).": "Selecionar vídeos individuais (.mp4, .avi, .mov). Suporta seleção múltipla (Ctrl+Click).", + "📂 Add Folder...": "📂 Adicionar Pasta...", + "Select a folder containing videos. The wizard scans the subfolders recursively and automatically.": "Selecionar pasta contendo vídeos. O wizard fará varredura recursiva nas subpastas automaticamente.", + "❌ Remove Selected": "❌ Remover Selecionado", + "Remove the item selected in the list (click an item to select it).": "Remover o item selecionado na lista (clique no item para selecioná-lo).", + "🗑️ Clear All": "🗑️ Limpar Tudo", + "Remove every selected video and folder.": "Remover todos os vídeos e pastas selecionados.", + "Selection Summary": "Resumo da Seleção", + "Selected Items": "Itens Selecionados", + "Structure Preview": "Pré-visualização da Estrutura", + "Folder / File": "Pasta / Arquivo", + "Select folders to preview the structure. Standalone files are listed automatically.": "Selecione pastas para visualizar a estrutura. Arquivos isolados são listados automaticamente.", + "💡 Tip: when you select folders, every video inside them (including subfolders) is included.": "💡 Dica: Ao selecionar pastas, todos os vídeos dentro delas (incluindo subpastas) serão incluídos.", + "Select the Video Files": "Selecione os Arquivos de Vídeo", + "Video files": "Arquivos de vídeo", + "Select a Folder Containing Videos": "Selecione uma Pasta Contendo Vídeos", + "Selected: {parts}": "Seleção: {parts}", + " (videos inside folders are detected in the next step)": " (a detecção de vídeos em pastas é feita na próxima etapa)", + "Please select at least one video file or folder.": "Por favor, selecione pelo menos um arquivo de vídeo ou pasta.", + "The following paths do not exist:\n{paths}": "Os seguintes caminhos não existem:\n{paths}", + "Preview truncated": "Prévia limitada", + "Individual Files": "Arquivos Individuais", + + "Configure Custom Regex Patterns": "Configurar Padrões Regex Personalizados", + "Custom Regex Patterns": "Padrões Regex Personalizados", + "Quick tips": "Dicas rápidas", + "• Empty fields leave the design unchanged.\n• \\d captures digits (0-9); \\w covers letters, digits and _.\n• The anchors ^ (start) and $ (end) pin the whole pattern.\n• The preview recalculates automatically after each edit.": "• Campos vazios permanecem inalterados no design.\n• \\d captura dígitos (0-9); \\w cobre letras, números e _.\n• Âncoras ^ (início) e $ (fim) fixam o padrão completo.\n• A pré-visualização calcula automaticamente após cada edição.", + "📚 Common Examples": "📚 Exemplos Comuns", + "Group in the file name": "Grupo no nome do arquivo", + "Day with a number": "Dia com número", + "Subject with an S prefix": "Sujeito com prefixo S", + "Group in a folder (any word)": "Grupo em pasta (qualquer palavra)", + "Use": "Usar", + "E.g. {example}": "Ex: {example}", + "Group pattern:": "Padrão de Grupos:", + "E.g. (Control|Treatment|Group\\d+) or (\\w+)_Group": "Ex: (Control|Treatment|Group\\d+) ou (\\w+)_Group", + "Day pattern:": "Padrão de Dias:", + "E.g. Day(\\d+) or D(\\d+) or (\\d{4}-\\d{2}-\\d{2})": "Ex: Day(\\d+) ou D(\\d+) ou (\\d{4}-\\d{2}-\\d{2})", + "Subject pattern:": "Padrão de Sujeitos:", + "E.g. S(\\d+) or Subject(\\d+) or Animal_(\\w+)": "Ex: S(\\d+) ou Subject(\\d+) ou Animal_(\\w+)", + "Test patterns:": "Testar Padrões:", + "Test": "Testar", + "Group": "Grupo", + "Day": "Dia", + "Subject": "Sujeito", + "○ {label}: waiting": "○ {label}: aguardando", + "Legend: ✓ matched • ✗ failed • ○ not defined": "Legenda: ✓ correspondeu • ✗ falhou • ○ não definido", + "Automatic preview (up to 15 paths)": "Pré-visualização automática (até 15 caminhos)", + "Clear All": "Limpar Tudo", + "Empty Path": "Caminho Vazio", + "Enter a path to test.": "Digite um caminho para testar.", + "Invalid Regex": "Regex Inválido", + "Invalid {label} pattern:\n{pattern}\n\nError: {error}": "Padrão de {label} inválido:\n{pattern}\n\nErro: {error}", + "✗ Enter a path to test": "✗ Informe um caminho para testar", + "○ {label}: pattern not defined": "○ {label}: Padrão não definido", + "✗ {label}: invalid regex - {error}": "✗ {label}: Regex inválido - {error}", + "✗ {label}: enter a path": "✗ {label}: Informe um caminho", + "✓ {label}: '{value}'": "✓ {label}: '{value}'", + "✗ {label}: no match": "✗ {label}: Nenhuma correspondência", + "Add videos in the previous step": "Adicione vídeos na etapa anterior", + "… 1 additional path": "… 1 caminho adicional", + "… {count} additional paths": "… {count} caminhos adicionais", + + "Welcome to the Project Creation Wizard": "Bem-vindo ao Assistente de Criação de Projeto", + "Let's start by understanding the context of your project.": "Vamos começar entendendo o contexto do seu projeto.", + "📂 Load Template...": "📂 Carregar Template...", + "1. Project Type": "1. Tipo de Projeto", + "Experimental (pre-recorded videos with groups, days, subjects)": "Experimental (vídeos pré-gravados com grupos, dias, sujeitos)", + "Projects with a formal design: treatment groups, controls, time series, etc.": "Projetos com design formal: grupos de tratamento, controles, séries temporais, etc.", + "Exploratory (pre-recorded videos, free-form analysis)": "Exploratório (vídeos pré-gravados, análise livre)", + "For quick tests, validations, or analyses with no defined experimental structure.": "Para testes rápidos, validações, ou análises sem estrutura experimental definida.", + "Live (record straight from the camera in real time)": "Ao Vivo (gravar diretamente da câmera em tempo real)", + "Record experiments in real time using a camera connected to the computer.": "Gravar experimentos em tempo real usando câmera conectada ao computador.", + "2. Folder Organization": "2. Organização de Pastas", + "Yes - folders represent the experimental structure (e.g. Group/Day/)": "Sim - pastas representam estrutura experimental (ex: Grupo/Dia/)", + "The wizard will detect groups, days and subjects automatically from the folder structure (e.g. /Control/Day01/Subject01.mp4).": "O assistente detectará automaticamente grupos, dias e sujeitos a partir da estrutura de pastas (ex: /Control/Day01/Subject01.mp4).", + "Yes - but only for organization (arbitrary names)": "Sim - mas apenas para organização (nomes arbitrários)", + "Folders are used only for organization, with no experimental meaning.": "Pastas são usadas só para organização, sem significado experimental.", + "No - every video is in a single directory": "Não - todos os vídeos estão em um único diretório", + "Every video is in one flat folder, with no subfolders.": "Todos os vídeos estão numa pasta plana, sem subpastas.", + "3. Existing Parquet Files": "3. Arquivos Parquet Existentes", + "Do you have .parquet files from previous analyses?": "Você possui arquivos .parquet de análises anteriores?", + "Yes - I want to import only the arena": "Sim - quero importar apenas arena", + "Import only the arena from *_arena.parquet files. ROIs and trajectories will be defined/generated again.": "Importar apenas a arena de arquivos *_arena.parquet. ROIs e trajetórias serão definidas/geradas novamente.", + "Yes - I want to import zones (arena and ROIs)": "Sim - quero importar zonas (arena e ROIs)", + "Import the arena and ROIs from *_arena.parquet and *_rois.parquet files. Trajectories will be generated again.": "Importar arena e ROIs de arquivos *_arena.parquet e *_rois.parquet. Trajetórias serão geradas novamente.", + "Yes - I want to import everything (zones + trajectory)": "Sim - quero importar tudo (zonas + trajetória)", + "Import arenas, ROIs and trajectories from *_arena.parquet, *_rois.parquet and *_trajectory.parquet files. Saves time by avoiding reprocessing.": "Importar arena, ROIs e trajetórias de arquivos *_arena.parquet, *_rois.parquet e *_trajectory.parquet. Economiza tempo evitando reprocessamento.", + "No - start from scratch": "Não - começar do zero", + "Process everything from the start: draw the arena, define ROIs and generate trajectories.": "Processar tudo do início: desenhar arena, definir ROIs e gerar trajetórias.", + "What do these terms mean?": "O que significam esses termos?", + "• Parquet: an efficient file format for storing data\n\n• Arena: the area of the tank where the animals move (a bounding polygon)\n\n• ROI (Region of Interest): specific regions such as 'Centre', 'Edge', 'Escape Zone'\n\n• Trajectory: frame-by-frame coordinates of the animals' movement\n\nImporting this data from previous analyses avoids reprocessing.": "• Parquet: Formato de arquivo eficiente para armazenar dados\n\n• Arena: Área do aquário onde os animais se movem (polígono delimitador)\n\n• ROI (Region of Interest): Regiões específicas como 'Centro', 'Borda', 'Zona de Escape'\n\n• Trajetória: Coordenadas frame-a-frame do movimento dos animais\n\nImportar esses dados de análises anteriores evita reprocessamento.", + "Load Wizard Template": "Carregar Template do Wizard", + "Load Template": "Carregar Template", + "Could not load the selected template. Check the file and try again.": "Não foi possível carregar o template selecionado. Verifique o arquivo e tente novamente.", + "Template Loaded": "Template Carregado", + "Settings loaded. Review each step before continuing.": "Configurações carregadas. Revise cada etapa antes de continuar." +} diff --git a/src/zebtrack/locales/_pairs/pr3-wizard-shell.json b/src/zebtrack/locales/_pairs/pr3-wizard-shell.json new file mode 100644 index 00000000..1f13baeb --- /dev/null +++ b/src/zebtrack/locales/_pairs/pr3-wizard-shell.json @@ -0,0 +1,25 @@ +{ + "_domain": "zebtrack", + "_comment": "Phase 3, batch 4a: wizard_dialog.py and experimental_design_step.py. 'Cancel' already existed and is reused. ui/wizard/models.py is NOT here: it is the Pydantic schema layer, so its validator messages and Field(description=...) values became plain English without _() — a description evaluated in a class body can never be a _() call, and the file already carried English descriptions for its newer fields.", + + "< Back": "< Voltar", + "Next >": "Próximo >", + "Create Project": "Criar Projeto", + "Validation": "Validação", + + "Experimental Design Configuration": "Configuração do Design Experimental", + "Set up the structure of your live experiment": "Configure a estrutura do seu experimento ao vivo", + "Basic Configuration": "Configuração Básica", + "Experiment Duration (days):": "Duração do Experimento (dias):", + "days": "dias", + "Experiment Duration\n\nHow many days your full experiment will last.\n\nExamples:\n• 1 day: acute test\n• 7 days: 1-week treatment\n• 21 days: chronic treatment\n\nYou can type directly or use the +/- buttons.\nThis affects how the output files are organised.": "Duração do Experimento\n\nQuantos dias durará seu experimento completo.\n\nExemplos:\n• 1 dia: Teste agudo\n• 7 dias: Tratamento de 1 semana\n• 21 dias: Tratamento crônico\n\nVocê pode digitar diretamente ou usar os botões +/-.\nIsso afeta a organização dos arquivos de saída.", + "Animals per Group:": "Animais por Grupo:", + "animals/group": "animais/grupo", + "Number of Groups:": "Número de Grupos:", + "groups": "grupos", + "Group Names": "Nomes dos Grupos", + "Give each group a descriptive name:": "Defina nomes descritivos para cada grupo:", + "ℹ️ How will this be used?": "ℹ️ Como isso será usado?", + "The structure you configure will be used to:\n\n• Organise recordings by Day → Group → Animal\n• Build a visual grid of experiment progress\n• Make comparative analysis between groups easier\n\nExample: 2 groups x 5 days x 3 animals = 30 organised recordings": "A estrutura configurada será usada para:\n\n• Organizar gravações por Dia → Grupo → Animal\n• Criar grid visual de progresso do experimento\n• Facilitar análise comparativa entre grupos\n\nExemplo: 2 grupos x 5 dias x 3 animais = 30 gravações organizadas", + "📊 Total: {sessions} recordings ({animals} animals x {days} days)": "📊 Total: {sessions} gravações ({animals} animais x {days} dias)" +} diff --git a/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.mo b/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.mo index c817875a..40ef2442 100644 Binary files a/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.mo and b/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.mo differ diff --git a/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.po b/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.po index 92992a6a..d82e1005 100644 --- a/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.po +++ b/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: DRerio LogAI VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-08-12 16:15-0300\n" +"POT-Creation-Date: 2026-08-14 21:39-0300\n" "PO-Revision-Date: 2026-08-08 21:41-0300\n" "Last-Translator: FULL NAME \n" "Language-Team: pt_BR \n" @@ -19,6 +19,162 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "Generated-By: Babel 2.18.0\n" +#: src/zebtrack/analysis/analysis_service.py:243 +#, python-brace-format +msgid "" +"Trajectory with {count} animals (track_ids). The ROI metrics at the top of " +"the report describe region OCCUPANCY (any_track semantics: the ROI counts as" +" occupied while any animal is inside). The per-animal metrics are in the " +"'por_animal' sheet of the summary spreadsheet (_summary.xlsx). The general " +"behaviour metrics that depend on frame ORDER (curvas_acentuadas) pool every " +"animal together and must be read with caution." +msgstr "" +"Trajetória com {count} animais (track_ids). As métricas de ROI no topo do " +"relatório são de OCUPAÇÃO da região (semântica any_track: a ROI conta como " +"ocupada enquanto qualquer animal estiver dentro). As métricas por animal " +"estão na aba 'por_animal' da planilha de resumo (_summary.xlsx). As métricas" +" de comportamento geral que dependem da ORDEM dos frames (curvas_acentuadas)" +" agrupam todos os animais e devem ser lidas com cautela." + +# | msgid "Batch processing started: {count} video(s)." +#: src/zebtrack/analysis/analysis_service.py:1194 +#: src/zebtrack/coordinators/ui_state_coordinator.py:735 +#, python-brace-format +msgid "Starting processing for {count} videos..." +msgstr "Iniciando processamento para {count} vídeos..." + +# | msgid "Analysis in Progress" +#: src/zebtrack/analysis/analysis_service.py:1288 +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:1899 +msgid "Analysis Error" +msgstr "Erro na Análise" + +#: src/zebtrack/analysis/analysis_service.py:1289 +#, python-brace-format +msgid "An unexpected error occurred: {error}" +msgstr "Ocorreu um erro inesperado: {error}" + +# | msgid "Cancel" +#: src/zebtrack/analysis/analysis_service.py:1333 +#: src/zebtrack/coordinators/ui_state_coordinator.py:756 +msgid "Cancelled" +msgstr "Cancelado" + +# | msgid "The video analysis was cancelled." +#: src/zebtrack/analysis/analysis_service.py:1333 +msgid "Video analysis was cancelled." +msgstr "A análise de vídeo foi cancelada." + +#: src/zebtrack/analysis/analysis_service.py:1337 +#, python-brace-format +msgid "" +"Analysis finished. Results saved to:\n" +"{path}" +msgstr "" +"Análise concluída. Resultados salvos em:\n" +"{path}" + +#: src/zebtrack/analysis/analysis_service.py:1338 +#: src/zebtrack/coordinators/model_diagnostics_coordinator.py:729 +#: src/zebtrack/coordinators/video_processing_coordinator.py:444 +#: src/zebtrack/ui/components/canvas/multi_aquarium_overlay.py:235 +#: src/zebtrack/ui/components/canvas/zone_editor.py:902 +#: src/zebtrack/ui/components/dialog_manager.py:1454 +#: src/zebtrack/ui/components/dialog_manager.py:1515 +#: src/zebtrack/ui/components/project_views/report_generator_actions.py:307 +#: src/zebtrack/ui/components/validation_manager.py:1613 +msgid "Success" +msgstr "Sucesso" + +# | msgid "Ready!" +#: src/zebtrack/analysis/analysis_service.py:1340 +#: src/zebtrack/coordinators/_unified_report_mixin.py:194 +#: src/zebtrack/ui/components/canvas/zone_editor.py:273 +msgid "Ready." +msgstr "Pronto." + +#: src/zebtrack/analysis/roi.py:819 +#, python-brace-format +msgid "" +"The bbox_intersects rule requires the bbox columns: {columns}. They are not " +"available in this dataset. Consider using 'centroid_in' or " +"'centroid_in_on_buffered_roi'." +msgstr "" +"A regra bbox_intersects requer colunas de bbox: {columns}. Essas colunas não" +" estão disponíveis no dataset. Considere usar 'centroid_in' ou " +"'centroid_in_on_buffered_roi'." + +#: src/zebtrack/analysis/roi.py:948 +msgid "" +"The pre-aligned masks do not have the same row count as the trajectory." +msgstr "" +"As máscaras pré-alinhadas não têm o mesmo número de linhas da trajetória." + +#: src/zebtrack/analysis/roi.py:961 +#, python-brace-format +msgid "The mask sidecar is missing the columns {columns}." +msgstr "O sidecar de máscaras não tem as colunas {columns}." + +#: src/zebtrack/analysis/roi.py:971 +msgid "" +"The analysed trajectory has no 'frame' column, which is half of the join key" +" with the masks." +msgstr "" +"A trajetória analisada não tem a coluna 'frame', que é metade da chave de " +"junção com as máscaras." + +#: src/zebtrack/analysis/roi.py:1009 +msgid "The mask sidecar contains no valid geometry." +msgstr "O sidecar de máscaras não contém nenhuma geometria válida." + +#: src/zebtrack/analysis/roi.py:1049 +msgid "" +"No mask in the sidecar matches a (frame, track_id) of the analysed " +"trajectory." +msgstr "" +"Nenhuma máscara do sidecar corresponde a (frame, track_id) da trajetória " +"analisada." + +#: src/zebtrack/analysis/roi.py:1068 +msgid "" +"The 'seg_overlap' rule was selected but no mask sidecar " +"(3b_Mascaras_*.parquet) was provided. Record with recorder.persist_masks " +"enabled and a segmentation model (model_selection.animal_method='seg')." +msgstr "" +"A regra 'seg_overlap' foi selecionada mas nenhum sidecar de máscaras " +"(3b_Mascaras_*.parquet) foi informado. Grave com recorder.persist_masks " +"ligado e um modelo de segmentação (model_selection.animal_method='seg')." + +#: src/zebtrack/analysis/roi.py:1078 +msgid "The mask sidecar is empty." +msgstr "O sidecar de máscaras está vazio." + +#: src/zebtrack/analysis/roi.py:1087 +#, python-brace-format +msgid "" +"The mask sidecar does not exist at '{path}'. Data recorded before this " +"feature, or with recorder.persist_masks disabled, does not have one." +msgstr "" +"O sidecar de máscaras não existe em '{path}'. Dados gravados antes desta " +"funcionalidade, ou com recorder.persist_masks desligado, não o têm." + +#: src/zebtrack/analysis/roi.py:1099 +#, python-brace-format +msgid "The sidecar '{path}' could not be read: {error}" +msgstr "O sidecar '{path}' não pôde ser lido: {error}" + +# | msgid "The file linked to the template was not found." +# | msgid "The report path was not found." +#: src/zebtrack/analysis/roi.py:1104 +#, python-brace-format +msgid "The sidecar '{path}' has no rows." +msgstr "O sidecar '{path}' não tem linhas." + +#: src/zebtrack/analysis/roi.py:1112 +#, python-brace-format +msgid "ROI rule 'seg_overlap' degraded to 'bbox_intersects': {detail}" +msgstr "Regra de ROI 'seg_overlap' degradada para 'bbox_intersects': {detail}" + # | msgid "Invalid Configuration" #: src/zebtrack/coordinators/_single_video_mixin.py:107 #: src/zebtrack/coordinators/video_processing_coordinator.py:921 @@ -53,6 +209,8 @@ msgstr "Vídeo Único" #: src/zebtrack/coordinators/model_diagnostics_coordinator.py:474 #: src/zebtrack/coordinators/model_diagnostics_coordinator.py:619 #: src/zebtrack/coordinators/project_lifecycle_coordinator.py:416 +#: src/zebtrack/core/recording/recording_service.py:154 +#: src/zebtrack/core/recording/recording_service.py:207 #: src/zebtrack/ui/builders/analysis_widgets.py:240 #: src/zebtrack/ui/builders/analysis_widgets.py:354 #: src/zebtrack/ui/builders/analysis_widgets.py:471 @@ -92,14 +250,25 @@ msgstr "Vídeo Único" #: src/zebtrack/ui/components/project_views/video_selector_tree_manager.py:824 #: src/zebtrack/ui/components/single_video_workflow.py:74 #: src/zebtrack/ui/components/single_video_workflow.py:258 -#: src/zebtrack/ui/components/single_video_workflow.py:277 -#: src/zebtrack/ui/components/single_video_workflow.py:354 -#: src/zebtrack/ui/components/single_video_workflow.py:385 -#: src/zebtrack/ui/components/single_video_workflow.py:389 +#: src/zebtrack/ui/components/single_video_workflow.py:274 +#: src/zebtrack/ui/components/single_video_workflow.py:351 +#: src/zebtrack/ui/components/single_video_workflow.py:382 +#: src/zebtrack/ui/components/single_video_workflow.py:386 #: src/zebtrack/ui/components/validation_manager.py:154 #: src/zebtrack/ui/components/validation_manager.py:183 #: src/zebtrack/ui/components/validation_manager.py:491 #: src/zebtrack/ui/components/validation_manager.py:1634 +#: src/zebtrack/ui/dialogs/block_detail_dialog.py:992 +#: src/zebtrack/ui/dialogs/block_detail_dialog.py:1001 +#: src/zebtrack/ui/dialogs/block_detail_dialog.py:1473 +#: src/zebtrack/ui/dialogs/block_detail_dialog.py:1529 +#: src/zebtrack/ui/dialogs/live_config_dialog.py:236 +#: src/zebtrack/ui/dialogs/live_config_dialog.py:244 +#: src/zebtrack/ui/dialogs/single_video_config_dialog.py:509 +#: src/zebtrack/ui/dialogs/single_video_config_dialog.py:548 +#: src/zebtrack/ui/dialogs/start_recording_dialog.py:233 +#: src/zebtrack/ui/gui.py:144 src/zebtrack/ui/gui.py:808 +#: src/zebtrack/ui/wizard/live_config_step.py:860 msgid "Error" msgstr "Erro" @@ -146,12 +315,6 @@ msgstr "Dados insuficientes" msgid "Report Error" msgstr "Erro no Relatório" -# | msgid "Ready!" -#: src/zebtrack/coordinators/_unified_report_mixin.py:194 -#: src/zebtrack/ui/components/canvas/zone_editor.py:273 -msgid "Ready." -msgstr "Pronto." - #: src/zebtrack/coordinators/_unified_report_mixin.py:606 msgid "Could not generate any file of the unified report." msgstr "Não foi possível gerar nenhum arquivo do relatório unificado." @@ -316,6 +479,8 @@ msgstr "Validação Falhou" # | msgid "No video selected" #: src/zebtrack/coordinators/_video_selection_mixin.py:224 #: src/zebtrack/ui/components/model_diagnostics_panel.py:56 +#: src/zebtrack/ui/dialogs/create_project_dialog.py:62 +#: src/zebtrack/ui/wizard/detection_step.py:221 msgid "No video selected." msgstr "Nenhum vídeo selecionado." @@ -596,6 +761,10 @@ msgstr "Peso Não Selecionado" #: src/zebtrack/ui/components/validation_manager.py:147 #: src/zebtrack/ui/components/validation_manager.py:162 #: src/zebtrack/ui/components/validation_manager.py:1631 +#: src/zebtrack/ui/dialogs/aquarium_assignment_dialog.py:579 +#: src/zebtrack/ui/dialogs/missing_metadata_dialog.py:81 +#: src/zebtrack/ui/dialogs/missing_metadata_dialog.py:88 +#: src/zebtrack/ui/wizard/design_editor_dialog.py:509 msgid "Validation Error" msgstr "Erro de Validação" @@ -723,6 +892,7 @@ msgstr "" # | msgid "Invalid selection" # | msgid "Detection" #: src/zebtrack/coordinators/live_calibration_coordinator.py:1018 +#: src/zebtrack/ui/dialogs/live_analysis_dialog.py:548 msgid "Detection Error" msgstr "Erro na Detecção" @@ -820,11 +990,6 @@ msgid "Analysing camera {camera} (analysis: {analysis}f, display: {display}f)" msgstr "" "Analisando câmera {camera} (análise: {analysis}f, exibição: {display}f)" -# | msgid "Analysis in Progress" -#: src/zebtrack/coordinators/live_camera_session_coordinator.py:1899 -msgid "Analysis Error" -msgstr "Erro na Análise" - #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1900 #, python-brace-format msgid "Failed to start the analysis of camera {camera}." @@ -867,10 +1032,16 @@ msgstr "" msgid "External Trigger Unavailable" msgstr "Trigger Externo Indisponível" +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2024 +#: src/zebtrack/coordinators/recording_session_coordinator.py:550 +#, python-brace-format +msgid "Waiting for external signal... (port {port})" +msgstr "Aguardando sinal externo... (porta {port})" + # | msgid "Video not found" # | msgid "ROI not found" #: src/zebtrack/coordinators/live_camera_session_coordinator.py:2168 -#: src/zebtrack/ui/components/project_initializer.py:348 +#: src/zebtrack/ui/components/project_initializer.py:347 msgid "Camera not found" msgstr "Câmera não encontrada" @@ -990,17 +1161,6 @@ msgstr "Salvar Relatório de Diagnóstico" msgid "Text files" msgstr "Arquivos de Texto" -#: src/zebtrack/coordinators/model_diagnostics_coordinator.py:729 -#: src/zebtrack/coordinators/video_processing_coordinator.py:444 -#: src/zebtrack/ui/components/canvas/multi_aquarium_overlay.py:235 -#: src/zebtrack/ui/components/canvas/zone_editor.py:902 -#: src/zebtrack/ui/components/dialog_manager.py:1454 -#: src/zebtrack/ui/components/dialog_manager.py:1515 -#: src/zebtrack/ui/components/project_views/report_generator_actions.py:307 -#: src/zebtrack/ui/components/validation_manager.py:1613 -msgid "Success" -msgstr "Sucesso" - #: src/zebtrack/coordinators/model_diagnostics_coordinator.py:730 #, python-brace-format msgid "" @@ -1012,6 +1172,7 @@ msgstr "" # | msgid "Error Saving to Project" #: src/zebtrack/coordinators/model_diagnostics_coordinator.py:742 +#: src/zebtrack/ui/wizard/confirmation_step.py:794 msgid "Error Saving" msgstr "Erro ao Salvar" @@ -1094,6 +1255,8 @@ msgstr " - Nenhuma detecção encontrada." # | msgid "Validation finished" # | msgid "Validation Failed" #: src/zebtrack/coordinators/multi_aquarium_coordinator.py:315 +#: src/zebtrack/ui/dialogs/block_detail_dialog.py:650 +#: src/zebtrack/ui/dialogs/start_recording_dialog.py:152 msgid "Detection failed" msgstr "Detecção falhou" @@ -1111,6 +1274,10 @@ msgstr "Erro desconhecido" # | msgid "Video Processing" # | msgid "Processing" #: src/zebtrack/coordinators/progress_tracking_coordinator.py:484 +#: src/zebtrack/core/video/analysis_pipeline_runner.py:696 +#: src/zebtrack/core/video/analysis_pipeline_runner.py:730 +#: src/zebtrack/core/video/video_context_factory.py:297 +#: src/zebtrack/core/video/video_context_factory.py:317 msgid "Processing Error" msgstr "Erro no Processamento" @@ -1213,6 +1380,7 @@ msgstr "" "Processando {name}: {done}/{total} ({pct}%) - {detected} detecções{eta}" #: src/zebtrack/coordinators/project_lifecycle_coordinator.py:382 +#: src/zebtrack/core/viewmodels/analysis_control_view_model.py:90 #: src/zebtrack/ui/project_workflow_adapter.py:178 msgid "Invalid Configuration" msgstr "Configuração Inválida" @@ -1263,6 +1431,7 @@ msgstr "Falhas em:\n" # | msgid "Reports removed" #: src/zebtrack/coordinators/report_generation_coordinator.py:433 +#: src/zebtrack/ui/dialogs/block_detail_dialog.py:1334 msgid "Reports Generated" msgstr "Relatórios Gerados" @@ -1456,17 +1625,6 @@ msgstr "Análise cancelada" msgid "The video analysis was cancelled. No report will be generated." msgstr "A análise de vídeo foi cancelada. Nenhum relatório será gerado." -# | msgid "Batch processing started: {count} video(s)." -#: src/zebtrack/coordinators/ui_state_coordinator.py:735 -#, python-brace-format -msgid "Starting processing for {count} videos..." -msgstr "Iniciando processamento para {count} vídeos..." - -# | msgid "Cancel" -#: src/zebtrack/coordinators/ui_state_coordinator.py:756 -msgid "Cancelled" -msgstr "Cancelado" - #: src/zebtrack/coordinators/ui_state_coordinator.py:756 msgid "The video analysis was cancelled." msgstr "A análise de vídeo foi cancelada." @@ -1504,6 +1662,8 @@ msgstr "Selecione Vídeos ou Pastas para Adicionar ao Projeto" #: src/zebtrack/ui/components/dialog_manager.py:541 #: src/zebtrack/ui/components/global_model_configuration_panel.py:515 #: src/zebtrack/ui/components/roi_template_manager.py:425 +#: src/zebtrack/ui/dialogs/save_roi_template_dialog.py:227 +#: src/zebtrack/ui/dialogs/single_video_config_dialog.py:493 msgid "All files" msgstr "Todos os arquivos" @@ -1511,6 +1671,9 @@ msgstr "Todos os arquivos" #: src/zebtrack/coordinators/video_processing_coordinator.py:384 #: src/zebtrack/coordinators/video_processing_coordinator.py:489 #: src/zebtrack/ui/components/model_diagnostics_panel.py:792 +#: src/zebtrack/ui/dialogs/create_project_dialog.py:285 +#: src/zebtrack/ui/dialogs/single_video_config_dialog.py:492 +#: src/zebtrack/ui/wizard/file_selection_step.py:240 msgid "Video files" msgstr "Arquivos de vídeo" @@ -1602,6 +1765,7 @@ msgstr "Nenhum vídeo elegível foi encontrado com dados para análise." #: src/zebtrack/coordinators/video_processing_coordinator.py:913 #: src/zebtrack/ui/components/canvas/renderer.py:261 #: src/zebtrack/ui/components/project_views/report_tree_builder.py:634 +#: src/zebtrack/ui/dialogs/multi_aquarium_live_preview_window.py:184 #, python-brace-format msgid "Aquarium {number}" msgstr "Aquário {number}" @@ -1691,5802 +1855,12280 @@ msgid "" msgstr "" "Recomendado mas modelo não convertido. Use 'Diagnóstico' para converter." -#: src/zebtrack/io/arduino_manager.py:140 -#, python-brace-format -msgid "Could not connect to the Arduino on port {port}." -msgstr "Não foi possível conectar ao Arduino na porta {port}." - -#: src/zebtrack/io/arduino_manager.py:220 -#, python-brace-format -msgid "Invalid command: {command}" -msgstr "Comando inválido: {command}" +# | msgid "Select the Video for Diagnostics" +# | msgid "📹 Select Video for Drawing" +# | msgid "Select a video before applying the template." +# | msgid "Select a video in the list before applying the template." +#: src/zebtrack/core/project/asset_manager.py:107 +msgid "Project not initialised for saving ROI templates." +msgstr "Projeto não inicializado para salvar templates de ROI." -#: src/zebtrack/io/arduino_manager.py:229 -msgid "Could not send command: Arduino disconnected." -msgstr "Não foi possível enviar comando: Arduino desconectado." +# | msgid "The mask sidecar is empty." +#: src/zebtrack/core/project/asset_manager.py:231 +#: src/zebtrack/core/project/roi_template_manager.py:103 +msgid "The template name cannot be empty." +msgstr "O nome do template não pode ficar vazio." -#: src/zebtrack/io/arduino_manager.py:556 -msgid "Serial connection to the Arduino was lost." -msgstr "Conexão serial com Arduino perdida." +# | msgid "Select the Video for Diagnostics" +# | msgid "📹 Select Video for Drawing" +# | msgid "Select a video before applying the template." +# | msgid "Error applying the template" +#: src/zebtrack/core/project/asset_manager.py:234 +msgid "Invalid zone data for saving the template." +msgstr "Dados de zona inválidos para salvar o template." -#: src/zebtrack/ui/project_workflow_adapter.py:307 -msgid "Project Loaded" -msgstr "Projeto Carregado" +#: src/zebtrack/core/project/asset_manager.py:237 +#: src/zebtrack/core/project/roi_template_manager.py:106 +msgid "Select at least the arena or the ROIs to save." +msgstr "Selecione ao menos arena ou ROIs para salvar." -#: src/zebtrack/ui/project_workflow_adapter.py:309 -#, python-brace-format -msgid "" -"Project '{name}' loaded successfully!\n" -"\n" -"• Videos: {videos}\n" -"• Main Arena: {arena}\n" -"• ROIs: {rois}\n" -"• Weights: {weights}\n" -"• OpenVINO: {openvino}" +#: src/zebtrack/core/project/asset_manager.py:245 +#: src/zebtrack/core/project/project_manager.py:138 +msgid "Cannot save the template into the current project: no project loaded." msgstr "" -"Projeto '{name}' carregado com sucesso!\n" -"\n" -"• Vídeos: {videos}\n" -"• Arena Principal: {arena}\n" -"• ROIs: {rois}\n" -"• Peso: {weights}\n" -"• OpenVINO: {openvino}" +"Não é possível salvar o template no projeto atual: projeto não carregado." -#: src/zebtrack/ui/sentinels.py:33 -msgid "All" -msgstr "Todos" +# | msgid "Template '{name}' applied to the video being edited." +#: src/zebtrack/core/project/asset_manager.py:259 +#, python-brace-format +msgid "Template '{name}' already exists." +msgstr "Template '{name}' já existe." -#: src/zebtrack/ui/sentinels.py:38 -msgid "No Group" -msgstr "Sem Grupo" +#: src/zebtrack/core/project/asset_manager.py:378 +msgid "Invalid template file: the 'data' block is missing." +msgstr "Arquivo de template inválido: bloco 'data' ausente." -#: src/zebtrack/ui/sentinels.py:43 -msgid "No Day" -msgstr "Sem Dia" +# | msgid "The selected video could not be located in the project." +# | msgid "Every selected video is already registered in the project." +# | msgid "No video is currently registered in the project" +#: src/zebtrack/core/project/asset_manager.py:434 +msgid "The template file is not registered in the project." +msgstr "Arquivo do template não registrado no projeto." -# | msgid "Remove reports" -#: src/zebtrack/ui/sentinels.py:48 -msgid "Not reported" -msgstr "Não informado" +# | msgid "Invalid selection" +# | msgid "Invalid target" +# | msgid "Invalid template" +#: src/zebtrack/core/project/asset_manager.py:447 +#: src/zebtrack/core/project/asset_manager.py:479 +msgid "Invalid template content." +msgstr "Conteúdo do template inválido." -#: src/zebtrack/ui/components/dialog_manager.py:284 -#: src/zebtrack/ui/components/zone_controls.py:26 -#: src/zebtrack/ui/sentinels.py:57 -msgid "Day" -msgstr "Dia" +# | msgid "Video not found" +# | msgid "ROI '{name}' not found." +#: src/zebtrack/core/project/asset_manager.py:453 +#, python-brace-format +msgid "ROI template '{name}' not found in the project." +msgstr "Template de ROI '{name}' não encontrado no projeto." -# | msgid "Pending arenas" -#: src/zebtrack/ui/sentinels.py:65 -msgid "Main Arena" -msgstr "Arena Principal" +#: src/zebtrack/core/project/asset_manager.py:468 +#, python-brace-format +msgid "ROI template '{name}' not found for the requested context." +msgstr "Template de ROI '{name}' não encontrado para o contexto solicitado." -#: src/zebtrack/ui/sentinels.py:81 -msgid "Both" -msgstr "Ambos" +#: src/zebtrack/core/project/asset_manager.py:718 +msgid "" +"Remove the reports and summaries before deleting the arena, the ROIs or the " +"trajectory." +msgstr "" +"Remova os relatórios e sumários antes de apagar arena, ROIs ou trajetórias." -# | msgid "Optimizing for your hardware (first run)..." -#: src/zebtrack/ui/splash_screen.py:218 -msgid "First run — optimizing for your hardware..." -msgstr "Primeira execução — otimizando para seu hardware..." +# | msgid "Remove the trajectory generated for this video?" +#: src/zebtrack/core/project/asset_manager.py:728 +msgid "There is no arena recorded for this video." +msgstr "Não há arena registrada para este vídeo." -#: src/zebtrack/ui/builders/analysis_widgets.py:62 -#: src/zebtrack/ui/components/analysis_controls.py:167 -msgid "Video Analysis" -msgstr "Análise de Vídeo" +# | msgid "Remove every ROI saved for this video?" +#: src/zebtrack/core/project/asset_manager.py:729 +msgid "There are no ROIs recorded for this video." +msgstr "Não há ROIs registradas para este vídeo." -#: src/zebtrack/ui/builders/analysis_widgets.py:163 -msgid "Processing and Reports" -msgstr "Processamento e Relatórios" +# | msgid "Remove the trajectory generated for this video?" +#: src/zebtrack/core/project/asset_manager.py:730 +msgid "There is no trajectory recorded for this video." +msgstr "Não há trajetória registrada para este vídeo." -# | msgid "Loading settings..." -#: src/zebtrack/ui/builders/analysis_widgets.py:205 -msgid "Advanced Settings" -msgstr "Config. Avançadas" +#: src/zebtrack/core/project/asset_manager.py:733 +#, python-brace-format +msgid "There is no {asset} recorded for this video." +msgstr "Não há {asset} registrado para este vídeo." -#: src/zebtrack/ui/builders/analysis_widgets.py:240 -msgid "Settings unavailable. Could not load." -msgstr "Settings não disponível. Não foi possível carregar." +# | msgid "There were no unified reports to delete." +#: src/zebtrack/core/project/asset_manager.py:737 +msgid "There are no reports or summaries to remove." +msgstr "Não há relatórios ou sumários para remover." -#: src/zebtrack/ui/builders/analysis_widgets.py:339 -#: src/zebtrack/ui/components/state_synchronizer.py:430 -msgid "Form reloaded" -msgstr "Formulário recarregado" +# | msgid "Remove the reports and summaries associated with this video?" +#: src/zebtrack/core/project/asset_manager.py:741 +msgid "Remove the reports and summaries before deleting the video." +msgstr "Remova relatórios e sumários antes de excluir o vídeo." -#: src/zebtrack/ui/builders/analysis_widgets.py:340 -#: src/zebtrack/ui/components/state_synchronizer.py:431 -msgid "Values restored to reflect the current settings." -msgstr "Valores restaurados para refletir as configurações atuais." +#: src/zebtrack/core/project/asset_manager.py:749 +msgid "" +"Remove the arena, the ROIs and the trajectory before deleting the video from" +" the project." +msgstr "Remova arena, ROIs e trajetórias antes de excluir o vídeo do projeto." -#: src/zebtrack/ui/builders/analysis_widgets.py:354 -msgid "Settings unavailable. Could not save." -msgstr "Settings não disponível. Não foi possível salvar." +#: src/zebtrack/core/project/project_lifecycle_manager.py:73 +#, python-brace-format +msgid "" +"Project configuration file '{filename}' not found in the selected directory: {path}\n" +"\n" +"Please make sure you selected a valid project folder." +msgstr "" +"Arquivo de configuração do projeto '{filename}' não encontrado no diretório selecionado: {path}\n" +"\n" +"Por favor, garanta que você selecionou uma pasta de projeto válida." -#: src/zebtrack/ui/builders/analysis_widgets.py:383 -#: src/zebtrack/ui/components/validation_manager.py:130 -msgid "FPS must be greater than 0." -msgstr "FPS deve ser maior que 0." +#: src/zebtrack/core/project/project_lifecycle_manager.py:94 +#, python-brace-format +msgid "" +"Failed to load or parse the project configuration file: {path}\n" +"\n" +"The file may be corrupted or unreadable.\n" +"\n" +"Error: {error}" +msgstr "" +"Falha ao carregar ou analisar o arquivo de configuração do projeto: {path}\n" +"\n" +"O arquivo pode estar corrompido ou ilegível.\n" +"\n" +"Erro: {error}" -#: src/zebtrack/ui/builders/analysis_widgets.py:385 -#: src/zebtrack/ui/components/validation_manager.py:132 -#: src/zebtrack/ui/components/validators/config_validator.py:41 -msgid "The processing interval must be greater than 0." -msgstr "O intervalo de processamento deve ser maior que 0." +#: src/zebtrack/core/project/project_lifecycle_manager.py:121 +msgid "" +"Cannot save the project: the project path is not set.\n" +"\n" +"The project must be created before it can be saved." +msgstr "" +"Não é possível salvar o projeto: caminho do projeto não definido.\n" +"\n" +"O projeto deve ser criado antes de ser salvo." -#: src/zebtrack/ui/builders/analysis_widgets.py:387 -#: src/zebtrack/ui/components/validation_manager.py:136 -#: src/zebtrack/ui/components/validators/config_validator.py:43 -msgid "The offset must be greater than or equal to 0." -msgstr "O offset deve ser maior ou igual a 0." - -#: src/zebtrack/ui/builders/analysis_widgets.py:389 -#: src/zebtrack/ui/components/validation_manager.py:138 -#: src/zebtrack/ui/components/validators/config_validator.py:45 -msgid "The flush interval must be >= 0." -msgstr "O intervalo de flush deve ser >= 0." - -#: src/zebtrack/ui/builders/analysis_widgets.py:391 -msgid "The flush row threshold must be >= 1." -msgstr "O limite de linhas para flush deve ser >= 1." - -#: src/zebtrack/ui/builders/analysis_widgets.py:393 -#: src/zebtrack/ui/components/validation_manager.py:142 -#: src/zebtrack/ui/components/validators/config_validator.py:49 -msgid "Window length must be odd and at least 3." -msgstr "Window length deve ser ímpar e pelo menos 3." - -#: src/zebtrack/ui/builders/analysis_widgets.py:395 -#: src/zebtrack/ui/components/validation_manager.py:144 -#: src/zebtrack/ui/components/validators/config_validator.py:51 -msgid "Polyorder must be at least 1." -msgstr "Polyorder deve ser pelo menos 1." - -#: src/zebtrack/ui/builders/analysis_widgets.py:434 -msgid "Project Settings Updated" -msgstr "Configurações do Projeto Atualizadas" - -#: src/zebtrack/ui/builders/analysis_widgets.py:436 +#: src/zebtrack/core/project/project_lifecycle_manager.py:133 +#, python-brace-format msgid "" -"The changes were saved to the CURRENT PROJECT ONLY.\n" -"The global file (config.local.yaml) was NOT modified." +"Permission denied while saving the project: {path}\n" +"\n" +"Check that you have write permission on the folder.\n" +"\n" +"Error: {error}" msgstr "" -"As alterações foram salvas APENAS no projeto atual.\n" -"O arquivo global (config.local.yaml) NÃO foi alterado." - -#: src/zebtrack/ui/builders/analysis_widgets.py:445 -msgid "Error Saving to Project" -msgstr "Erro ao Salvar no Projeto" +"Permissão negada ao salvar o projeto: {path}\n" +"\n" +"Verifique se você tem permissão de escrita na pasta.\n" +"\n" +"Erro: {error}" -#: src/zebtrack/ui/builders/analysis_widgets.py:446 +#: src/zebtrack/core/project/project_lifecycle_manager.py:143 #, python-brace-format -msgid "Failed to update the project settings: {error}" -msgstr "Falha ao atualizar configurações do projeto: {error}" +msgid "" +"I/O error while saving the project: {path}\n" +"\n" +"Check the disk space and the permissions.\n" +"\n" +"Error: {error}" +msgstr "" +"Erro de I/O ao salvar o projeto: {path}\n" +"\n" +"Verifique o espaço em disco e permissões.\n" +"\n" +"Erro: {error}" -#: src/zebtrack/ui/builders/analysis_widgets.py:472 -#: src/zebtrack/ui/components/validation_manager.py:184 +#: src/zebtrack/core/project/project_lifecycle_manager.py:153 #, python-brace-format -msgid "Could not save config.local.yaml: {error}" -msgstr "Não foi possível salvar config.local.yaml: {error}" - -#: src/zebtrack/ui/builders/analysis_widgets.py:489 -msgid "Changes recorded in config.local.yaml." -msgstr "Alterações registradas em config.local.yaml." +msgid "" +"Error serialising the project data: {path}\n" +"\n" +"The project data may be corrupted.\n" +"\n" +"Error: {error}" +msgstr "" +"Erro ao serializar dados do projeto: {path}\n" +"\n" +"Dados do projeto podem estar corrompidos.\n" +"\n" +"Erro: {error}" -#: src/zebtrack/ui/builders/analysis_widgets.py:491 +#: src/zebtrack/core/project/project_lifecycle_manager.py:164 +#, python-brace-format msgid "" +"Unexpected error while saving the project: {path}\n" "\n" +"Please check the folder permissions.\n" "\n" -"The current project was also updated with these values." +"Error: {error}" msgstr "" +"Erro inesperado ao salvar o projeto: {path}\n" "\n" +"Por favor, verifique as permissões da pasta.\n" "\n" -"O projeto atual também foi atualizado com estes valores." +"Erro: {error}" -#: src/zebtrack/ui/builders/analysis_widgets.py:493 -msgid "Settings Saved" -msgstr "Configurações Salvas" +#: src/zebtrack/core/project/project_lifecycle_manager.py:398 +#, python-brace-format +msgid "" +"Could not create the project directory: {error}\n" +"\n" +"Please check the folder permissions and that the path is valid." +msgstr "" +"Não foi possível criar o diretório do projeto: {error}\n" +"\n" +"Por favor, verifique as permissões da pasta e se o caminho é válido." -#: src/zebtrack/ui/builders/analysis_widgets.py:557 -#: src/zebtrack/ui/components/project_views/report_generator_actions.py:331 -#: src/zebtrack/ui/components/validation_manager.py:1627 -msgid "Warning" -msgstr "Aviso" +# | msgid "Remove video from project" +# | msgid "No video found in the project" +#: src/zebtrack/core/project/project_manager.py:829 +msgid "Video not found in the project." +msgstr "Vídeo não encontrado no projeto." -#: src/zebtrack/ui/builders/analysis_widgets.py:559 +#: src/zebtrack/core/project/project_workflow_service.py:129 +#: src/zebtrack/core/viewmodels/analysis_control_view_model.py:92 #, python-brace-format msgid "" -"Global settings were saved, but the current project could not be updated: " -"{error}" +"The detection mode (det) for animals is only compatible with 1 animal per aquarium.\n" +"Current configuration: {count} animals per aquarium.\n" +"\n" +"To use multiple animals per aquarium, change the animal detection method to 'seg' (segmentation) in the settings." msgstr "" -"Configuração global salva, mas erro ao atualizar projeto atual: {error}" +"O modo de detecção (det) para animais só é compatível com 1 animal por aquário.\n" +"Configuração atual: {count} animais por aquário.\n" +"\n" +"Para usar múltiplos animais por aquário, altere o método de detecção de animais para 'seg' (segmentação) nas configurações." -#: src/zebtrack/ui/builders/button_factory.py:36 -msgid "Project Actions" -msgstr "Ações do Projeto" +# | msgid "Default?" +#: src/zebtrack/core/project/project_workflow_service.py:1184 +msgid "Default" +msgstr "Padrão" -#: src/zebtrack/ui/builders/button_factory.py:41 -msgid "Global Model Configuration..." -msgstr "Configuração Global de Modelos..." +# | msgid "Zone saved successfully." +#: src/zebtrack/core/project/project_workflow_service.py:1328 +msgid "🎉 Project created successfully!" +msgstr "🎉 Projeto criado com sucesso!" -# | msgid "Loading settings..." -#: src/zebtrack/ui/builders/button_factory.py:47 -msgid "Global Diagnostics..." -msgstr "Diagnóstico Global..." +# | msgid "OpenVINO: {status}" +#: src/zebtrack/core/project/project_workflow_service.py:1330 +msgid "📊 Video status:" +msgstr "📊 Status dos vídeos:" -#: src/zebtrack/ui/builders/button_factory.py:53 -msgid "Analyze Single Video" -msgstr "Analisar Vídeo Único" +# | msgid "Failure: {error}" +# | msgid "❌ Failures: {count}" +#: src/zebtrack/core/project/project_workflow_service.py:1331 +#: src/zebtrack/ui/wizard/confirmation_step.py:554 +#, python-brace-format +msgid " • Total videos: {count}" +msgstr " • Total de vídeos: {count}" -#: src/zebtrack/ui/builders/button_factory.py:59 -msgid "Analyze Live Camera" -msgstr "Analisar Câmera ao Vivo" +# | msgid "- Frames analysed: {count}" +# | msgid "" +# | "\" \"\n" +# | "\" \"✅ Finished: {count}" +#: src/zebtrack/core/project/project_workflow_service.py:1332 +#, python-brace-format +msgid " • With arena defined: {count}" +msgstr " • Com arena definida: {count}" -#: src/zebtrack/ui/builders/button_factory.py:65 -msgid "Create New Project" -msgstr "Criar Novo Projeto" +# | msgid "- Frames analysed: {count}" +# | msgid "" +# | "\" \"\n" +# | "\" \"✅ Finished: {count}" +#: src/zebtrack/core/project/project_workflow_service.py:1333 +#, python-brace-format +msgid " • With ROIs defined: {count}" +msgstr " • Com ROIs definidas: {count}" -#: src/zebtrack/ui/builders/button_factory.py:71 -msgid "Open Existing Project" -msgstr "Abrir Projeto Existente" +#: src/zebtrack/core/project/project_workflow_service.py:1334 +#, python-brace-format +msgid " • With trajectory ready: {count}" +msgstr " • Com trajetória pronta: {count}" -#: src/zebtrack/ui/builders/button_factory.py:97 -msgid "↶ Undo (Ctrl+Z)" -msgstr "↶ Desfazer (Ctrl+Z)" +# | msgid "Preparing video processing..." +# | msgid "Recording and Processing Controls" +#: src/zebtrack/core/project/project_workflow_service.py:1335 +#, python-brace-format +msgid " • Pending processing: {count}" +msgstr " • Pendentes de processamento: {count}" -#: src/zebtrack/ui/builders/button_factory.py:106 -msgid "↷ Redo (Ctrl+Y)" -msgstr "↷ Refazer (Ctrl+Y)" +#: src/zebtrack/core/project/project_workflow_service.py:1337 +msgid "🚀 Recommended next steps:" +msgstr "🚀 Próximos passos recomendados:" -#: src/zebtrack/ui/builders/common_widgets.py:49 -#: src/zebtrack/ui/components/validation_manager.py:1007 -msgid "Trajectory" -msgstr "Trajetória" +#: src/zebtrack/core/project/project_workflow_service.py:1343 +#, python-brace-format +msgid "{step}. Review and adjust the imported zones" +msgstr "{step}. Visualizar e ajustar zonas importadas" -#: src/zebtrack/ui/builders/common_widgets.py:52 -#: src/zebtrack/ui/components/validation_manager.py:1009 -msgid "Summary" -msgstr "Sumário" +#: src/zebtrack/core/project/project_workflow_service.py:1344 +#: src/zebtrack/core/project/project_workflow_service.py:1359 +#: src/zebtrack/core/project/project_workflow_service.py:1367 +#, python-brace-format +msgid " - Open the '{tab}' tab" +msgstr " - Abra a aba '{tab}'" -#: src/zebtrack/ui/builders/common_widgets.py:53 -msgid "Missing" -msgstr "Ausente" +# | msgid "Global Model Configuration..." +# | msgid "Model Configuration" +#: src/zebtrack/core/project/project_workflow_service.py:1344 +#: src/zebtrack/ui/components/tab_builder.py:134 +#: src/zebtrack/ui/dialogs/zone_calibration_dialog.py:40 +msgid "Zone Configuration" +msgstr "Configuração de Zonas" -#: src/zebtrack/ui/builders/common_widgets.py:54 -msgid "Legend: " -msgstr "Legenda: " +#: src/zebtrack/core/project/project_workflow_service.py:1346 +#, python-brace-format +msgid " - Use the '{panel}' panel" +msgstr " - Use o painel '{panel}'" -#: src/zebtrack/ui/builders/common_widgets.py:59 -msgid "" -"marks videos ready to generate trajectories (arena and ROIs saved). The " -"number shows how many are still awaiting processing." -msgstr "" -"indica vídeos prontos para gerar trajetórias (arena e ROIs salvos). O valor " -"mostra quantos ainda aguardam processamento." +# | msgid "Select the Video for Diagnostics" +#: src/zebtrack/core/project/project_workflow_service.py:1346 +#: src/zebtrack/ui/components/zone_controls.py:693 +msgid "📹 Select Video for Drawing" +msgstr "📹 Selecionar Vídeo para Desenho" -#: src/zebtrack/ui/builders/common_widgets.py:180 -msgid "Weight Type" -msgstr "Tipo de Peso" +#: src/zebtrack/core/project/project_workflow_service.py:1349 +#, python-brace-format +msgid " - Double-click, or use '{button}', to review" +msgstr " - Clique duas vezes, ou use '{button}', para revisar" -#: src/zebtrack/ui/builders/common_widgets.py:191 -msgid "Select the model type:" -msgstr "Selecione o tipo de modelo:" +# | msgid "Confirm removal of the selected item?" +#: src/zebtrack/core/project/project_workflow_service.py:1350 +#: src/zebtrack/ui/components/zone_controls.py:763 +msgid "📹 Load Frame from the Selected Video" +msgstr "📹 Carregar Frame do Vídeo Selecionado" -#: src/zebtrack/ui/builders/common_widgets.py:197 -msgid "Segmentation (for masks and precise edges)" -msgstr "Segmentação (para máscaras e bordas precisas)" +# | msgid "Start drawing the arena and the ROIs for this video." +#: src/zebtrack/core/project/project_workflow_service.py:1353 +msgid " - Adjust the arena and the ROIs as needed" +msgstr " - Ajuste arena e ROIs conforme necessário" -#: src/zebtrack/ui/builders/common_widgets.py:204 -msgid "Detection (for fast bounding boxes)" -msgstr "Detecção (para caixas delimitadoras rápidas)" +# | msgid "Processing and Reports" +# | msgid "Processing Mode:" +# | msgid "Processing Videos" +# | msgid "Process Pending Videos..." +#: src/zebtrack/core/project/project_workflow_service.py:1358 +#, python-brace-format +msgid "{step}. Process the pending videos" +msgstr "{step}. Processar vídeos pendentes" -#: src/zebtrack/ui/builders/common_widgets.py:223 -msgid "Cancel" -msgstr "Cancelar" +#: src/zebtrack/core/project/project_workflow_service.py:1359 +#: src/zebtrack/ui/components/tab_builder.py:61 +msgid "Main Control" +msgstr "Controle Principal" -#: src/zebtrack/ui/builders/panel_builder.py:41 -#: src/zebtrack/ui/components/tab_builder.py:337 -msgid "Detection Model Status" -msgstr "Estado do Modelo de Detecção" +# | msgid "Processing Interval (N):" +#: src/zebtrack/core/project/project_workflow_service.py:1360 +msgid " - Confirm the processing intervals" +msgstr " - Confirme os intervalos de processamento" -#: src/zebtrack/ui/builders/panel_builder.py:83 -msgid "Readiness Indicators" -msgstr "Indicadores de Preparação" +#: src/zebtrack/core/project/project_workflow_service.py:1361 +#, python-brace-format +msgid " - Click '{button}'" +msgstr " - Clique em '{button}'" -#: src/zebtrack/ui/builders/panel_builder.py:92 -msgid "Pending arenas" -msgstr "Arenas pendentes" +# | msgid "Processing and Reports" +# | msgid "Processing Mode:" +# | msgid "Processing Videos" +#: src/zebtrack/core/project/project_workflow_service.py:1361 +#: src/zebtrack/ui/components/tab_builder.py:326 +msgid "Process Pending Videos..." +msgstr "Processar Vídeos Pendentes..." -#: src/zebtrack/ui/builders/panel_builder.py:93 -msgid "Pending ROIs" -msgstr "ROIs pendentes" +# | msgid "Loading model system..." +# | msgid "Error Loading Model" +# | msgid "Error opening folder" +# | msgid "Error Generating Reports" +#: src/zebtrack/core/project/project_workflow_service.py:1366 +#, python-brace-format +msgid "{step}. Generate reports" +msgstr "{step}. Gerar relatórios" -#: src/zebtrack/ui/builders/panel_builder.py:96 -msgid "Ready for trajectories" -msgstr "Prontos para trajetórias" +#: src/zebtrack/core/project/project_workflow_service.py:1367 +#: src/zebtrack/ui/builders/analysis_widgets.py:163 +msgid "Processing and Reports" +msgstr "Processamento e Relatórios" -#: src/zebtrack/ui/builders/panel_builder.py:108 -msgid "No videos listed" -msgstr "Nenhum vídeo listado" +#: src/zebtrack/core/project/project_workflow_service.py:1368 +msgid " - Browse the hierarchy of groups, days and subjects" +msgstr " - Navegue pela hierarquia de grupos, dias e sujeitos" -#: src/zebtrack/ui/builders/project_widgets.py:127 -msgid "The experimental design is not fully configured." -msgstr "O design experimental não está totalmente configurado." +# | msgid "Generating the unified report..." +#: src/zebtrack/core/project/project_workflow_service.py:1369 +msgid " - Generate individual or unified reports as needed" +msgstr " - Gere relatórios individuais ou unificados conforme necessário" -#: src/zebtrack/ui/builders/project_widgets.py:279 -msgid "Edit Selected Video Metadata..." -msgstr "Editar Metadata do Vídeo Selecionado..." +#: src/zebtrack/core/project/project_workflow_service.py:1372 +msgid "💡 Tips:" +msgstr "💡 Dicas:" -#: src/zebtrack/ui/builders/project_widgets.py:285 -msgid "Go to Processing and Reports →" -msgstr "Ir para Processamento e Relatórios →" +#: src/zebtrack/core/project/project_workflow_service.py:1373 +msgid " • Use the search box to locate videos quickly" +msgstr " • Use a busca para localizar vídeos rapidamente" -#: src/zebtrack/ui/builders/project_widgets.py:338 -#: src/zebtrack/ui/components/project_initializer.py:84 -msgid "Live" -msgstr "Ao Vivo" +#: src/zebtrack/core/project/project_workflow_service.py:1374 +msgid " • The status symbols show which arenas, ROIs and trajectories exist" +msgstr "" +" • Os símbolos de status indicam arenas, ROIs e trajetórias disponíveis" -#: src/zebtrack/ui/builders/project_widgets.py:340 -#: src/zebtrack/ui/components/project_initializer.py:86 -msgid "Pre-recorded" -msgstr "Pré-gravado" +#: src/zebtrack/core/project/project_workflow_service.py:1375 +msgid " • Adjust the zones before processing if necessary" +msgstr " • Ajuste zonas antes de processar se necessário" -#: src/zebtrack/ui/builders/project_widgets.py:344 -#: src/zebtrack/ui/components/project_initializer.py:90 -#, python-brace-format -msgid "Project: {name} ({type})" -msgstr "Projeto: {name} ({type})" +#: src/zebtrack/core/project/roi_template_manager.py:109 +msgid "Zone data cannot be empty." +msgstr "Dados de zona não podem ser vazios." -#: src/zebtrack/ui/builders/zone_control_builder.py:85 -msgid "" -"Counts as inside when the animal's centroid is inside the ROI polygon. " -"Simple and fast; may miss partial entries (e.g. the head enters first)." -msgstr "" -"Considera dentro quando o centróide do animal está dentro do polígono da " -"ROI. Simples e rápido; pode perder entradas parciais (ex.: cabeça entra " -"primeiro)." +#: src/zebtrack/core/project/roi_template_manager.py:112 +msgid "Invalid arena: at least 3 points are required." +msgstr "Arena inválida: é necessário ao menos 3 pontos." -#: src/zebtrack/ui/builders/zone_control_builder.py:93 -msgid "" -"Same as centroid, but with the ROI dilated by r to capture partial entries " -"(e.g. the head). r is in cm when calibration exists, otherwise in px." -msgstr "" -"Igual ao centróide, porém com ROI dilatada por r para capturar entradas " -"parciais (ex.: cabeça). r em cm se houver calibração; senão em px." +# | msgid "Action unavailable" +# | msgid "{count} ROI(s) available." +#: src/zebtrack/core/project/roi_template_manager.py:115 +msgid "No ROI available to save." +msgstr "Nenhuma ROI disponível para salvar." -#: src/zebtrack/ui/builders/zone_control_builder.py:103 -msgid "" -"A detection counts as inside the ROI when the fraction of the bbox area " -"contained in the ROI reaches this value." -msgstr "" -"A detecção é considerada dentro da ROI quando a fração de área do bbox " -"contida na ROI atinge este valor." +#: src/zebtrack/core/project/roi_template_manager.py:123 +msgid "A project path is required to save a template into the project." +msgstr "Caminho do projeto é necessário para salvar template no projeto." -#: src/zebtrack/ui/builders/zone_control_builder.py:108 -msgid "" -"Counts as inside when the animal's rectangle (bbox) overlaps the ROI by at " -"least the given fraction. Captures partial entries; may overestimate at the " -"edges." -msgstr "" -"Considera dentro quando o retângulo do animal (bbox) sobrepõe a ROI ao menos" -" pela fração definida. Captura entradas parciais; pode superestimar em " -"bordas." +#: src/zebtrack/core/project/roi_template_manager.py:128 +msgid "A custom path is required for save_location='custom'." +msgstr "Caminho personalizado é necessário para save_location='custom'." -#: src/zebtrack/ui/builders/zone_control_builder.py:118 -msgid "" -"Fraction of the animal's MASK inside the ROI (not the bbox). Without " -"recorded masks the analysis falls back to 'bbox_intersects'." -msgstr "" -"Fração da MÁSCARA do animal dentro da ROI (não do bbox). Sem máscaras " -"gravadas a análise cai para 'bbox_intersects'." +#: src/zebtrack/core/project/roi_template_manager.py:153 +#, python-brace-format +msgid "Template '{name}' already exists in {directory}." +msgstr "Template '{name}' já existe em {directory}." -#: src/zebtrack/ui/builders/zone_control_builder.py:124 +# | msgid "Could not connect to the Arduino on port {port}." +# | msgid "Could not open the video: {path}" +# | msgid "Could not open: {path}" +#: src/zebtrack/core/project/roi_template_manager.py:230 +#, python-brace-format +msgid "Template not found: {path}" +msgstr "Template não encontrado: {path}" + +# | msgid "Inference failed on frame {frame}: {error}" +#: src/zebtrack/core/project/roi_template_manager.py:249 +#, python-brace-format +msgid "Invalid JSON in {path}: {error}" +msgstr "JSON inválido em {path}: {error}" + +# | msgid "Invalid selection" +# | msgid "Invalid target" +# | msgid "Invalid template" +#: src/zebtrack/core/project/roi_template_manager.py:256 +#, python-brace-format +msgid "Invalid template in {path}: {error}" +msgstr "Template inválido em {path}: {error}" + +# | msgid "Validation Error" +# | msgid "Validation finished" +# | msgid "✓ Drawing Finished" +# | msgid "Processing Finished" +# | msgid "Zones not finalised" +#: src/zebtrack/core/project/zone_manager.py:587 +msgid "Project data not initialised" +msgstr "Dados do projeto não inicializados" + +# | msgid "Analysing frame {current}/{total}..." +# | msgid "Processing aquarium {current}/{total}..." +#: src/zebtrack/core/recording/frame_processing_pipeline.py:684 +#, python-brace-format +msgid "🔍 Detecting aquarium... ({current}/{total})" +msgstr "🔍 Detectando aquário... ({current}/{total})" + +#: src/zebtrack/core/recording/live_analysis_post_processor.py:628 +#, python-brace-format msgid "" -"Counts as inside based on the overlap between the animal's mask and the ROI;" -" more precise and more expensive. BEYOND this rule it requires: " -"recorder.persist_masks enabled (writes the 3b_Mascaras_