From 79ce8eb4740dfe6ca8fd7790d8820f3b3379e907 Mon Sep 17 00:00:00 2001 From: MarkSant Date: Thu, 13 Aug 2026 13:15:32 -0300 Subject: [PATCH 01/21] feat(i18n): traduzir analysis/** e travar o pacote no ratchet (fase 3, lote 1) Primeiro lote da fase 3. `src/zebtrack/analysis` vai a zero literais e entra em MIGRATED_PATHS -- `analysis/reporters/**` ja estava limpo desde a fase 1, entao o pacote inteiro fecha de uma vez. Dois defeitos achados de quebra, nenhum deles "so traduzir": 1. `data_transformer.rename_geotaxis_columns` gravava "Fundo"/"Meio"/"Superficie" como CABECALHO DE COLUNA do `_summary.xlsx`, no meio de um arquivo em que todo o resto (COLUMN_MAPPING, DISPLAY_COLUMN_MAPPING) ja e ingles. Eram as unicas colunas do sumario cujo nome dependia do idioma: a mesma analise saia com esquema diferente conforme a maquina, quebrando script de terceiro. Viram "Bottom"/"Middle"/"Surface" em ingles FIXO, sem `_()` -- rotulo de coluna de arquivo de dados nao pode variar com `ui.language`. Comentario no local registra o porque. 2. `visualization_generator` tinha uma unica legenda em portugues ("Area de processamento...") entre ~20 textos de grafico em ingles fixo (set_title, set_xlabel, ax.text). Mesmo tratamento: ingles fixo, sem `_()`. O que e prosa de interface foi para o catalogo `zebtrack` com `_()`: `roi.py` (12 avisos de degradacao do `seg_overlap` + o ValueError de `bbox_intersects`) e `analysis_service.py` (aviso multi-animal, barra de status, dialogos de erro/cancelamento/sucesso). Os avisos de `roi.py` sao conteudo de relatorio: chegam ao leitor por degradation_warnings -> validation_warnings -> apendice de validacao do `.docx`. Dominio `zebtrack`, nao `reporter`: so `analysis/reporters/**` importa o `_` do reporter_context. Cinco msgids que este lote precisava JA EXISTIAM com o portugues IDENTICO ("Starting processing for {count} videos...", "Analysis Error", "Cancelled", "Success", "Ready.") -- reusados, omitidos de pr3-analysis.json. Escrever de novo faria `update_translations.py` avisar "conflicting translations" e ignorar o par em silencio. Tres strings sem acento que o scanner NAO ve foram junto ("Cancelado", "Sucesso", "Pronto." em analysis_service): confirmacao de que a contagem de 942 literais e piso, nao teto. Asserts migrados no mesmo commit: test_seg_overlap_fallback (5), test_roi_analyzer, test_visualization_generator, test_data_transformer_roi_metrics e test_geotaxis_features (4 casos de nome de coluna). Verde: ruff limpo, mypy limpo em 698 arquivos, 4209 fast (+1 = o novo caso parametrizado do ratchet), 1079 gui. Co-Authored-By: Claude Opus 5 --- docs/tasks/active/ROLLING_TASK_LOG.md | 37 +++ src/zebtrack/analysis/analysis_service.py | 33 +- src/zebtrack/analysis/data_transformer.py | 12 +- src/zebtrack/analysis/roi.py | 57 ++-- .../analysis/visualization_generator.py | 2 +- src/zebtrack/locales/_pairs/pr3-analysis.json | 22 ++ .../locales/pt_BR/LC_MESSAGES/zebtrack.mo | Bin 133512 -> 136997 bytes .../locales/pt_BR/LC_MESSAGES/zebtrack.po | 283 +++++++++++++----- src/zebtrack/locales/zebtrack.pot | 251 +++++++++++----- .../test_data_transformer_roi_metrics.py | 2 +- tests/analysis/test_roi_analyzer.py | 2 +- tests/analysis/test_seg_overlap_fallback.py | 10 +- .../analysis/test_visualization_generator.py | 2 +- tests/i18n/test_no_untranslated_literals.py | 1 + tests/test_geotaxis_features.py | 22 +- 15 files changed, 521 insertions(+), 215 deletions(-) create mode 100644 src/zebtrack/locales/_pairs/pr3-analysis.json diff --git a/docs/tasks/active/ROLLING_TASK_LOG.md b/docs/tasks/active/ROLLING_TASK_LOG.md index 9970832e..6c79aa9b 100644 --- a/docs/tasks/active/ROLLING_TASK_LOG.md +++ b/docs/tasks/active/ROLLING_TASK_LOG.md @@ -6,6 +6,43 @@ 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:__ In Progress 🔄 +__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. +- [ ] Lote 2: `ui/gui.py` + `ui/ui_coordinator.py`. +- [ ] Lote 3: `core/**` (29 arquivos), incluindo simplificar o consumidor de + `progress_notifier` para só "step:" depois de migrar o produtor. +- [ ] Lote 4: `ui/wizard/**` (13 arquivos) — valores de enum em models.py/enums.py + são persistidos, traduzir só os rótulos exibidos. +- [ ] Lote 5: `ui/dialogs/**` (26 arquivos). +- [ ] Fechamento: colapsar MIGRATED_PATHS, atualizar guia e CHANGELOG. + ### [2026-06-09] Sexteto de bugs em projetos live (zonas, lote, contadores, OpenVINO, settings globais) __ID:__ TASK-067 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/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/pt_BR/LC_MESSAGES/zebtrack.mo b/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.mo index c817875a0d00b83f34a3c1a8310ae836bb155790..4e90c2a5c8bf85792697b8151bcc3c86f3c1b8eb 100644 GIT binary patch delta 23080 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
r5q&k5NlVTm6LqbTzAA(|DrfUIuDI~*AJ4dI M;g|0EGfu5Ki<1iUcL(Oa!YPA+)e>{j< zis&|uQw!H)J$!&Au~b{f$&bCTE>1&V0SdoRP$*xbB9Q!Jn@L{O07{}lR>!UP#7xxV zFbrp6MqKS)Pr~HXPog4n4%Pl`_x>|f1cKTTe}ZI#EabeI?PUecYET$h{Aa;l)%0n_zy0}p%~QB z)_gQ-M#oVjKZBb2byV^_LxnnTCtJ!st|L&-O~NEx=GI$wwgL3i${O`){!VW*{-V*a(%Ju~-x*V?Eq~dhfqM#9wO>KG<>QdUyfV^Vjk)ra6aZ4VFyqm22u}>X;LkqGp(c!FU#{;T6n)**K#xs@u6OrjcQ<(TTjA_)PKU9cpY_qyul!J###0U zquwimdafpFz+EtyzMcLQG{O<6kWR$EaVcu8CXKfjR$*@H30NObqXw9Tt-xwHg^>Su zR`Xv|RiDTwAa2DRcndkBod0ke_MF7z6v`}FF*6UE&zXOBt6qdt_m;ys)*jb+m{R2crI2yGiwPz52HPC?zYH+Cg;CQ#b z2-9$V4eEf|hS~5qdhs?Yw1G2isl2FqC5*wgm57zCyDY zwH-HL1Rh0&{vWJ}wPy1Pho7U8@&sxE*Dw%Ypg!H+U@H88>NwRLi)0~G#~m>QeQ^{N zs>zraXJRo-z~Xol6|!`5ZD8T3dTms0v`0M`k2*2bkxV8I-ZBA zwf|R9(8xF8PTYo}_{n^GVK}P8DOeL%U@+doqIe%KV-^k(<-k9v_g|wr3R-A}Asfoc zk6Mzom{$9LFNHc>IDy(;=@!}A7Q^(^n_(&JhGlROmcWy!$i2t3m^|KgQ5c3%FN|8s zs;DLEh|O^rdhr-0r+??33iue+(F@cJ3NN+@l*4H1^-+-;fhln`Cg5aLXv;3Kfz-nS z)Z3y4FdCI~OEEL9M-BJ@`m$0uO(6r`#f#4T0rgxR^kQdxjz0JLkY%>- zXP_ds2^E0@s3o|78o(cz9iO0bE!A@3uNR`0+l;HCX3`k7ww+M}n1QK0><(0et5;Zq z+fdIRKs|Q`b^jV_0{2llk?BjznVhI4D2N)+h%bqMTM84o5QEoIA+EC0Mp_fqa3{=$ zeNZ8s;MSL-w%HEMi3c$YUPn#r1uCK`R@p$Zqn^))ifBbXs*wL5~E>3K{~WPV3Q;`vv0Kk#b?!u3p8nCnriIgYV4YWJ+c;^^BWo7zS%Hmqy#(sH>KKkKP}x2bwblzz6Wxqz z_h$^Ef9F>UTH_m77~f$j4FATKrad|=$#7ICM|^8v!zZ8`-i2DiGnfw_pavGY$&xY$ zYH7-&2KX_S#lGlcnVqjFbiultnH{b}9g*3#SOd|hjvAt7&;yI%c+_*-Q4Jr%f_McB z;RjSC3MJV2(gZcYuBhiGCJ=vx`Wr4NY4)MA{tT+YJE*mMh1!0hTdkqOsNAUJ)?1=- zpchueepnpWVQRdD%7xpQ2A`r1y!Ts)e@_bOw^2IEEKJtp6Db1G%tvA6ZG3 zXa3G+*m%DsTNkX&{V}M49me8#8nu=mP@zwAz_Q$n*{D~?T-YA#W1M^a5T>Ml4O40V z-|-g+FQ((h|IpvyK|Ti=rvz!Nb>tj$$HXSv9#m9($YYN-k2ZDaG?H1;m9rZ#URD)5d8P~-)Y>S!jEGpD@ zFa`eSn(VkmA{8oGv!T|$2Pz`{j}w1|>~k*U#W|P^x1&b53l;h^uD8*@|6M~)Scmyh z$r*_na9Iq;TJH6}s3jSW4R8`x#S&#fO3t0Anf`>D$Yo5853vyXUQQ!7DU?lansF{z)w73YPaUF(g|6im~f(w6QCCq%@ z{vK$7QPdY>dHfNJ;(OE*L|w3)DTf+JJM?02tchP>VLXWn`M+2dJs0h{>X=#kzbl2( z7>5~fGpeKSu`E8uB3STO`_O5Q>R>u5a;H%~hN^Rf-FA!^OrV{sgh zK8-w)f|BO2>pj%L5qgC!g+)&gEhoSlzeS`Qb)T_Cmwf+Sa%J-uIUsje%eGf`_e4_o6h)Qq2^)-wDL z8&DJ1E~t(Nq9!mNHL$tZ1D9cDbZ*

B~YvA&x?2bqDN@y|5dez~)%=Ps?5(YQ_gp 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 diff --git a/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.po b/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.po index 92992a6a..91fe744d 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-13 13:03-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:1050 +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:1071 +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:1081 +msgid "The mask sidecar is empty." +msgstr "O sidecar de máscaras está vazio." + +#: src/zebtrack/analysis/roi.py:1090 +#, 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:1102 +#, 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:1108 +#, python-brace-format +msgid "The sidecar '{path}' has no rows." +msgstr "O sidecar '{path}' não tem linhas." + +#: src/zebtrack/analysis/roi.py:1117 +#, 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 @@ -92,10 +248,10 @@ 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 @@ -146,12 +302,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." @@ -820,11 +970,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}." @@ -870,7 +1015,7 @@ msgstr "Trigger Externo Indisponível" # | 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 +1135,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 "" @@ -1456,17 +1590,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." @@ -3350,7 +3473,7 @@ msgstr "" # | msgid "No Group" #: src/zebtrack/ui/components/dialog_manager.py:283 -#: src/zebtrack/ui/components/project_overview.py:401 +#: src/zebtrack/ui/components/project_overview.py:400 #: src/zebtrack/ui/components/zone_controls.py:26 msgid "Group" msgstr "Grupo" @@ -3480,7 +3603,7 @@ msgstr "" # | msgid "Open Existing Project" #: src/zebtrack/ui/components/dialog_manager.py:848 -#: src/zebtrack/ui/components/project_initializer.py:536 +#: src/zebtrack/ui/components/project_initializer.py:533 msgid "Select an Existing Project Folder" msgstr "Selecione uma Pasta de Projeto Existente" @@ -3736,22 +3859,22 @@ msgstr "✓ Reconectado na porta {port}." msgid "✗ Could not connect on port {port}." msgstr "✗ Não foi possível conectar na porta {port}." -#: src/zebtrack/ui/components/event_dispatcher.py:1026 +#: src/zebtrack/ui/components/event_dispatcher.py:1025 msgid "Position adjusted. Click 'Save Edit' to confirm." msgstr "Posição ajustada. Clique em 'Salvar Edição' para confirmar." -#: src/zebtrack/ui/components/event_dispatcher.py:1032 +#: src/zebtrack/ui/components/event_dispatcher.py:1029 msgid "✓ Drawing finished. Click 'Save Edit' to confirm." msgstr "✓ Desenho finalizado. Clique em 'Salvar Edição' para confirmar." # | msgid "Template:" # | msgid "Template applied" -#: src/zebtrack/ui/components/event_dispatcher.py:1175 -#: src/zebtrack/ui/components/event_dispatcher.py:1191 +#: src/zebtrack/ui/components/event_dispatcher.py:1171 +#: src/zebtrack/ui/components/event_dispatcher.py:1187 msgid "ROI Rule Applied" msgstr "Regra de ROI Aplicada" -#: src/zebtrack/ui/components/event_dispatcher.py:1177 +#: src/zebtrack/ui/components/event_dispatcher.py:1173 #, python-brace-format msgid "" "{description}\n" @@ -3762,7 +3885,7 @@ msgstr "" "\n" "Salva no projeto: vale para novas análises, para a regeneração de relatórios e para o gatilho Arduino ao vivo." -#: src/zebtrack/ui/components/event_dispatcher.py:1193 +#: src/zebtrack/ui/components/event_dispatcher.py:1189 #, python-brace-format msgid "" "{description}\n" @@ -3774,27 +3897,27 @@ msgstr "" "Aplicada a esta sessão. Sem projeto aberto, ela não é salva em disco." # | msgid "Buffer radius (r):" -#: src/zebtrack/ui/components/event_dispatcher.py:1206 +#: src/zebtrack/ui/components/event_dispatcher.py:1202 #, python-brace-format msgid "buffer radius {value:g} cm" msgstr "raio de buffer {value:g} cm" # | msgid "0 = any real overlap." -#: src/zebtrack/ui/components/event_dispatcher.py:1210 +#: src/zebtrack/ui/components/event_dispatcher.py:1204 msgid "any non-zero area overlap" msgstr "qualquer sobreposição de área não-nula" -#: src/zebtrack/ui/components/event_dispatcher.py:1212 +#: src/zebtrack/ui/components/event_dispatcher.py:1206 #, python-brace-format msgid "minimum overlap {value:g} (basis: {basis})" msgstr "sobreposição mínima {value:g} (base: {basis})" # | msgid "Detector Parameters" -#: src/zebtrack/ui/components/event_dispatcher.py:1217 +#: src/zebtrack/ui/components/event_dispatcher.py:1211 msgid "no additional parameters" msgstr "sem parâmetros adicionais" -#: src/zebtrack/ui/components/event_dispatcher.py:1218 +#: src/zebtrack/ui/components/event_dispatcher.py:1212 #, python-brace-format msgid "Rule '{rule}' ({detail})." msgstr "Regra '{rule}' ({detail})." @@ -4031,7 +4154,7 @@ msgid "YOLO models" msgstr "Modelos YOLO" #: src/zebtrack/ui/components/global_model_configuration_panel.py:531 -#: src/zebtrack/ui/components/processing_reports.py:471 +#: src/zebtrack/ui/components/processing_reports.py:469 #: src/zebtrack/ui/components/roi_template_manager.py:274 msgid "Confirm Deletion" msgstr "Confirmar Exclusão" @@ -4440,7 +4563,7 @@ msgstr "🔄 Editar Grupo / Dia / Sujeitos" #: src/zebtrack/ui/components/menu_manager.py:289 #: src/zebtrack/ui/components/processing_reports.py:220 -#: src/zebtrack/ui/components/processing_reports.py:677 +#: src/zebtrack/ui/components/processing_reports.py:673 msgid "No video selected" msgstr "Nenhum vídeo selecionado" @@ -5191,8 +5314,8 @@ msgstr "🔄 Atualizar" # | msgid "Expand all" #: src/zebtrack/ui/components/processing_reports.py:160 -#: src/zebtrack/ui/components/processing_reports.py:323 -#: src/zebtrack/ui/components/processing_reports.py:703 +#: src/zebtrack/ui/components/processing_reports.py:321 +#: src/zebtrack/ui/components/processing_reports.py:699 msgid "⊞ Expand All" msgstr "⊞ Expandir Tudo" @@ -5231,58 +5354,58 @@ msgid "Status" msgstr "Status" # | msgid "Processing and Reports" -#: src/zebtrack/ui/components/processing_reports.py:229 +#: src/zebtrack/ui/components/processing_reports.py:228 msgid "Processing and Report Actions" msgstr "Ações de Processamento e Relatórios" # | msgid "📈 Delete Trajectory" -#: src/zebtrack/ui/components/processing_reports.py:236 +#: src/zebtrack/ui/components/processing_reports.py:234 msgid "▶️ Generate Trajectories" msgstr "▶️ Gerar Trajetórias" -#: src/zebtrack/ui/components/processing_reports.py:244 +#: src/zebtrack/ui/components/processing_reports.py:242 msgid "Σ Export Summaries (Parquet)" msgstr "Σ Exportar Sumários (Parquet)" # | msgid "No video selected" # | msgid "Video not selected" -#: src/zebtrack/ui/components/processing_reports.py:255 +#: src/zebtrack/ui/components/processing_reports.py:253 msgid "📄 Report for Selected" msgstr "📄 Relatório para Selecionados" # | msgid "Remove reports" # | msgid "Not reported" # | msgid "Unified Report" -#: src/zebtrack/ui/components/processing_reports.py:263 +#: src/zebtrack/ui/components/processing_reports.py:261 msgid "📚 Unified Report (All)" msgstr "📚 Relatório Unificado (Todos)" # | msgid "Open Cache Folder" # | msgid "📁 Open File Folder" -#: src/zebtrack/ui/components/processing_reports.py:274 +#: src/zebtrack/ui/components/processing_reports.py:272 msgid "📄 Open Unified Word" msgstr "📄 Abrir Word Unificado" -#: src/zebtrack/ui/components/processing_reports.py:282 +#: src/zebtrack/ui/components/processing_reports.py:280 msgid "📊 Open Unified Excel" msgstr "📊 Abrir Excel Unificado" # | msgid "Remove reports" # | msgid "Not reported" # | msgid "Unified Report" -#: src/zebtrack/ui/components/processing_reports.py:290 +#: src/zebtrack/ui/components/processing_reports.py:288 msgid "Σ Open Unified Parquet" msgstr "Σ Abrir Parquet Unificado" # | msgid "Delete {asset}" # | msgid "Delete {kind}" # | msgid "🗑️ Delete {kind}…" -#: src/zebtrack/ui/components/processing_reports.py:298 +#: src/zebtrack/ui/components/processing_reports.py:296 msgid "🗑️ Delete Everything" msgstr "🗑️ Apagar Tudo" # | msgid "Collapse all" -#: src/zebtrack/ui/components/processing_reports.py:330 +#: src/zebtrack/ui/components/processing_reports.py:328 msgid "⊟ Collapse All" msgstr "⊟ Colapsar Tudo" @@ -5291,7 +5414,7 @@ msgstr "⊟ Colapsar Tudo" # | msgid "" "Are you sure you want to remove ROI '{name}'?\n" "\n" "This # action # | cannot be undone." -#: src/zebtrack/ui/components/processing_reports.py:473 +#: src/zebtrack/ui/components/processing_reports.py:471 msgid "" "Are you sure you want to delete ALL unified reports (Parquet, Excel, Word)?\n" "\n" @@ -5303,7 +5426,7 @@ msgstr "" # | msgid "{count} vertex/vertices selected." # | msgid "{count} vertex(es) selected." -#: src/zebtrack/ui/components/processing_reports.py:670 +#: src/zebtrack/ui/components/processing_reports.py:668 #, python-brace-format msgid "{count} video(s) selected" msgstr "{count} vídeo(s) selecionado(s)" @@ -5325,7 +5448,7 @@ msgstr "" # | msgid "" "The camera saved in the project ('{name}') was not detected.\n" # | "\n" "Connect the right device or use 'Change camera' in the recording # | dialog " "to pick another one." -#: src/zebtrack/ui/components/project_initializer.py:350 +#: src/zebtrack/ui/components/project_initializer.py:349 #, python-brace-format msgid "" "The camera saved in the project ('{name}') was not detected.\n" @@ -5338,18 +5461,18 @@ msgstr "" # | msgid "Error" # | msgid "Model Error" -#: src/zebtrack/ui/components/project_initializer.py:397 +#: src/zebtrack/ui/components/project_initializer.py:396 msgid "Camera Error" msgstr "Erro na Câmera" # | msgid "Error" # | msgid "Model Error" # | msgid "Import error" -#: src/zebtrack/ui/components/project_initializer.py:508 +#: src/zebtrack/ui/components/project_initializer.py:507 msgid "Wizard Error" msgstr "Erro no Wizard" -#: src/zebtrack/ui/components/project_initializer.py:509 +#: src/zebtrack/ui/components/project_initializer.py:508 #, python-brace-format msgid "Missing required fields: {fields}" msgstr "Campos obrigatórios ausentes: {fields}" @@ -5543,7 +5666,7 @@ msgstr "" # | msgid "Delete {asset}" # | msgid "🗑️ Delete {label}…" -#: src/zebtrack/ui/components/project_overview.py:432 +#: src/zebtrack/ui/components/project_overview.py:431 #: src/zebtrack/ui/components/project_views/report_tree_builder.py:353 #: src/zebtrack/ui/components/project_views/video_selector_tree_manager.py:509 #: src/zebtrack/ui/components/validation_manager.py:354 @@ -5553,14 +5676,14 @@ msgid "🐟 Subject {label}" msgstr "🐟 Sujeito {label}" # | msgid "Partial Unified Report" -#: src/zebtrack/ui/components/project_overview.py:462 +#: src/zebtrack/ui/components/project_overview.py:461 #: src/zebtrack/ui/components/project_views/report_tree_builder.py:285 msgid "🧾 Partial Reports" msgstr "🧾 Relatórios Parciais" # | msgid "Partial Unified Report" # | msgid "🧾 Partial Reports" -#: src/zebtrack/ui/components/project_overview.py:475 +#: src/zebtrack/ui/components/project_overview.py:474 msgid "Partial Report" msgstr "Relatório Parcial" @@ -5657,7 +5780,7 @@ msgstr "Template '{name}' disponível para uso." # | msgid "Failed to update the project settings: {error}" # | msgid "Failed to start the recording: {error}" #: src/zebtrack/ui/components/single_video_workflow.py:74 -#: src/zebtrack/ui/components/single_video_workflow.py:354 +#: src/zebtrack/ui/components/single_video_workflow.py:351 #, python-brace-format msgid "Failed to start the analysis: {error}" msgstr "Falha ao iniciar análise: {error}" @@ -5692,7 +5815,7 @@ msgstr "Entrada Inválida" msgid "The number of frames to analyse must be a positive whole number." msgstr "O número de frames para análise deve ser um número inteiro positivo." -#: src/zebtrack/ui/components/single_video_workflow.py:260 +#: src/zebtrack/ui/components/single_video_workflow.py:259 msgid "" "Could not start camera detection (calibration coordinator unavailable)." msgstr "" @@ -5703,19 +5826,19 @@ msgstr "" # | msgid "Validation finished" # | msgid "Validation Failed" # | msgid "Automatic Detection Failed" -#: src/zebtrack/ui/components/single_video_workflow.py:278 +#: src/zebtrack/ui/components/single_video_workflow.py:275 #, python-brace-format msgid "Automatic camera detection failed: {error}" msgstr "Falha na detecção automática pela câmera: {error}" # | msgid "Every arena has been defined." -#: src/zebtrack/ui/components/single_video_workflow.py:385 +#: src/zebtrack/ui/components/single_video_workflow.py:382 msgid "No aquarium has been defined." msgstr "Nenhum aquário foi definido." # | msgid "The aquarium polygon data could not be retrieved." # | msgid "The main aquarium polygon has not been defined" -#: src/zebtrack/ui/components/single_video_workflow.py:390 +#: src/zebtrack/ui/components/single_video_workflow.py:387 msgid "The aquarium's main area (polygon) has not been defined." msgstr "A área principal do aquário (polígono) não foi definida." diff --git a/src/zebtrack/locales/zebtrack.pot b/src/zebtrack/locales/zebtrack.pot index 4ac5eb8c..ce20231e 100644 --- a/src/zebtrack/locales/zebtrack.pot +++ b/src/zebtrack/locales/zebtrack.pot @@ -9,7 +9,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-13 13:03-0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,6 +18,134 @@ msgstr "" "Content-Transfer-Encoding: 8bit\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 "" + +#: 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 "" + +#: src/zebtrack/analysis/analysis_service.py:1288 +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:1899 +msgid "Analysis Error" +msgstr "" + +#: src/zebtrack/analysis/analysis_service.py:1289 +#, python-brace-format +msgid "An unexpected error occurred: {error}" +msgstr "" + +#: src/zebtrack/analysis/analysis_service.py:1333 +#: src/zebtrack/coordinators/ui_state_coordinator.py:756 +msgid "Cancelled" +msgstr "" + +#: src/zebtrack/analysis/analysis_service.py:1333 +msgid "Video analysis was cancelled." +msgstr "" + +#: src/zebtrack/analysis/analysis_service.py:1337 +#, python-brace-format +msgid "" +"Analysis finished. Results saved to:\n" +"{path}" +msgstr "" + +#: 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 "" + +#: 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 "" + +#: 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 "" + +#: src/zebtrack/analysis/roi.py:948 +msgid "The pre-aligned masks do not have the same row count as the trajectory." +msgstr "" + +#: src/zebtrack/analysis/roi.py:961 +#, python-brace-format +msgid "The mask sidecar is missing the columns {columns}." +msgstr "" + +#: 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 "" + +#: src/zebtrack/analysis/roi.py:1009 +msgid "The mask sidecar contains no valid geometry." +msgstr "" + +#: src/zebtrack/analysis/roi.py:1050 +msgid "" +"No mask in the sidecar matches a (frame, track_id) of the analysed " +"trajectory." +msgstr "" + +#: src/zebtrack/analysis/roi.py:1071 +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 "" + +#: src/zebtrack/analysis/roi.py:1081 +msgid "The mask sidecar is empty." +msgstr "" + +#: src/zebtrack/analysis/roi.py:1090 +#, 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 "" + +#: src/zebtrack/analysis/roi.py:1102 +#, python-brace-format +msgid "The sidecar '{path}' could not be read: {error}" +msgstr "" + +#: src/zebtrack/analysis/roi.py:1108 +#, python-brace-format +msgid "The sidecar '{path}' has no rows." +msgstr "" + +#: src/zebtrack/analysis/roi.py:1117 +#, python-brace-format +msgid "ROI rule 'seg_overlap' degraded to 'bbox_intersects': {detail}" +msgstr "" + #: src/zebtrack/coordinators/_single_video_mixin.py:107 #: src/zebtrack/coordinators/video_processing_coordinator.py:921 msgid "Incomplete Configuration" @@ -85,10 +213,10 @@ msgstr "" #: 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 @@ -129,11 +257,6 @@ msgstr "" msgid "Report Error" msgstr "" -#: src/zebtrack/coordinators/_unified_report_mixin.py:194 -#: src/zebtrack/ui/components/canvas/zone_editor.py:273 -msgid "Ready." -msgstr "" - #: src/zebtrack/coordinators/_unified_report_mixin.py:606 msgid "Could not generate any file of the unified report." msgstr "" @@ -624,10 +747,6 @@ msgstr "" msgid "Analysing camera {camera} (analysis: {analysis}f, display: {display}f)" msgstr "" -#: src/zebtrack/coordinators/live_camera_session_coordinator.py:1899 -msgid "Analysis Error" -msgstr "" - #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1900 #, python-brace-format msgid "Failed to start the analysis of camera {camera}." @@ -666,7 +785,7 @@ msgid "External Trigger Unavailable" msgstr "" #: 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 "" @@ -772,17 +891,6 @@ msgstr "" msgid "Text files" msgstr "" -#: 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 "" - #: src/zebtrack/coordinators/model_diagnostics_coordinator.py:730 #, python-brace-format msgid "" @@ -1153,15 +1261,6 @@ msgstr "" msgid "The video analysis was cancelled. No report will be generated." msgstr "" -#: src/zebtrack/coordinators/ui_state_coordinator.py:735 -#, python-brace-format -msgid "Starting processing for {count} videos..." -msgstr "" - -#: src/zebtrack/coordinators/ui_state_coordinator.py:756 -msgid "Cancelled" -msgstr "" - #: src/zebtrack/coordinators/ui_state_coordinator.py:756 msgid "The video analysis was cancelled." msgstr "" @@ -2734,7 +2833,7 @@ msgid "" msgstr "" #: src/zebtrack/ui/components/dialog_manager.py:283 -#: src/zebtrack/ui/components/project_overview.py:401 +#: src/zebtrack/ui/components/project_overview.py:400 #: src/zebtrack/ui/components/zone_controls.py:26 msgid "Group" msgstr "" @@ -2834,7 +2933,7 @@ msgid "" msgstr "" #: src/zebtrack/ui/components/dialog_manager.py:848 -#: src/zebtrack/ui/components/project_initializer.py:536 +#: src/zebtrack/ui/components/project_initializer.py:533 msgid "Select an Existing Project Folder" msgstr "" @@ -3028,20 +3127,20 @@ msgstr "" msgid "✗ Could not connect on port {port}." msgstr "" -#: src/zebtrack/ui/components/event_dispatcher.py:1026 +#: src/zebtrack/ui/components/event_dispatcher.py:1025 msgid "Position adjusted. Click 'Save Edit' to confirm." msgstr "" -#: src/zebtrack/ui/components/event_dispatcher.py:1032 +#: src/zebtrack/ui/components/event_dispatcher.py:1029 msgid "✓ Drawing finished. Click 'Save Edit' to confirm." msgstr "" -#: src/zebtrack/ui/components/event_dispatcher.py:1175 -#: src/zebtrack/ui/components/event_dispatcher.py:1191 +#: src/zebtrack/ui/components/event_dispatcher.py:1171 +#: src/zebtrack/ui/components/event_dispatcher.py:1187 msgid "ROI Rule Applied" msgstr "" -#: src/zebtrack/ui/components/event_dispatcher.py:1177 +#: src/zebtrack/ui/components/event_dispatcher.py:1173 #, python-brace-format msgid "" "{description}\n" @@ -3050,7 +3149,7 @@ msgid "" "and to the live Arduino trigger." msgstr "" -#: src/zebtrack/ui/components/event_dispatcher.py:1193 +#: src/zebtrack/ui/components/event_dispatcher.py:1189 #, python-brace-format msgid "" "{description}\n" @@ -3058,25 +3157,25 @@ msgid "" "Applied to this session. With no project open it is not saved to disk." msgstr "" -#: src/zebtrack/ui/components/event_dispatcher.py:1206 +#: src/zebtrack/ui/components/event_dispatcher.py:1202 #, python-brace-format msgid "buffer radius {value:g} cm" msgstr "" -#: src/zebtrack/ui/components/event_dispatcher.py:1210 +#: src/zebtrack/ui/components/event_dispatcher.py:1204 msgid "any non-zero area overlap" msgstr "" -#: src/zebtrack/ui/components/event_dispatcher.py:1212 +#: src/zebtrack/ui/components/event_dispatcher.py:1206 #, python-brace-format msgid "minimum overlap {value:g} (basis: {basis})" msgstr "" -#: src/zebtrack/ui/components/event_dispatcher.py:1217 +#: src/zebtrack/ui/components/event_dispatcher.py:1211 msgid "no additional parameters" msgstr "" -#: src/zebtrack/ui/components/event_dispatcher.py:1218 +#: src/zebtrack/ui/components/event_dispatcher.py:1212 #, python-brace-format msgid "Rule '{rule}' ({detail})." msgstr "" @@ -3292,7 +3391,7 @@ msgid "YOLO models" msgstr "" #: src/zebtrack/ui/components/global_model_configuration_panel.py:531 -#: src/zebtrack/ui/components/processing_reports.py:471 +#: src/zebtrack/ui/components/processing_reports.py:469 #: src/zebtrack/ui/components/roi_template_manager.py:274 msgid "Confirm Deletion" msgstr "" @@ -3640,7 +3739,7 @@ msgstr "" #: src/zebtrack/ui/components/menu_manager.py:289 #: src/zebtrack/ui/components/processing_reports.py:220 -#: src/zebtrack/ui/components/processing_reports.py:677 +#: src/zebtrack/ui/components/processing_reports.py:673 msgid "No video selected" msgstr "" @@ -4290,8 +4389,8 @@ msgid "🔄 Refresh" msgstr "" #: src/zebtrack/ui/components/processing_reports.py:160 -#: src/zebtrack/ui/components/processing_reports.py:323 -#: src/zebtrack/ui/components/processing_reports.py:703 +#: src/zebtrack/ui/components/processing_reports.py:321 +#: src/zebtrack/ui/components/processing_reports.py:699 msgid "⊞ Expand All" msgstr "" @@ -4321,47 +4420,47 @@ msgstr "" msgid "Status" msgstr "" -#: src/zebtrack/ui/components/processing_reports.py:229 +#: src/zebtrack/ui/components/processing_reports.py:228 msgid "Processing and Report Actions" msgstr "" -#: src/zebtrack/ui/components/processing_reports.py:236 +#: src/zebtrack/ui/components/processing_reports.py:234 msgid "▶️ Generate Trajectories" msgstr "" -#: src/zebtrack/ui/components/processing_reports.py:244 +#: src/zebtrack/ui/components/processing_reports.py:242 msgid "Σ Export Summaries (Parquet)" msgstr "" -#: src/zebtrack/ui/components/processing_reports.py:255 +#: src/zebtrack/ui/components/processing_reports.py:253 msgid "📄 Report for Selected" msgstr "" -#: src/zebtrack/ui/components/processing_reports.py:263 +#: src/zebtrack/ui/components/processing_reports.py:261 msgid "📚 Unified Report (All)" msgstr "" -#: src/zebtrack/ui/components/processing_reports.py:274 +#: src/zebtrack/ui/components/processing_reports.py:272 msgid "📄 Open Unified Word" msgstr "" -#: src/zebtrack/ui/components/processing_reports.py:282 +#: src/zebtrack/ui/components/processing_reports.py:280 msgid "📊 Open Unified Excel" msgstr "" -#: src/zebtrack/ui/components/processing_reports.py:290 +#: src/zebtrack/ui/components/processing_reports.py:288 msgid "Σ Open Unified Parquet" msgstr "" -#: src/zebtrack/ui/components/processing_reports.py:298 +#: src/zebtrack/ui/components/processing_reports.py:296 msgid "🗑️ Delete Everything" msgstr "" -#: src/zebtrack/ui/components/processing_reports.py:330 +#: src/zebtrack/ui/components/processing_reports.py:328 msgid "⊟ Collapse All" msgstr "" -#: src/zebtrack/ui/components/processing_reports.py:473 +#: src/zebtrack/ui/components/processing_reports.py:471 msgid "" "Are you sure you want to delete ALL unified reports (Parquet, Excel, " "Word)?\n" @@ -4369,7 +4468,7 @@ msgid "" "This action cannot be undone." msgstr "" -#: src/zebtrack/ui/components/processing_reports.py:670 +#: src/zebtrack/ui/components/processing_reports.py:668 #, python-brace-format msgid "{count} video(s) selected" msgstr "" @@ -4383,7 +4482,7 @@ msgstr "" msgid "Could not connect to the Arduino on port {port}. Running in offline mode." msgstr "" -#: src/zebtrack/ui/components/project_initializer.py:350 +#: src/zebtrack/ui/components/project_initializer.py:349 #, python-brace-format msgid "" "The camera saved in the project ('{name}') was not detected.\n" @@ -4393,15 +4492,15 @@ msgid "" "connected device." msgstr "" -#: src/zebtrack/ui/components/project_initializer.py:397 +#: src/zebtrack/ui/components/project_initializer.py:396 msgid "Camera Error" msgstr "" -#: src/zebtrack/ui/components/project_initializer.py:508 +#: src/zebtrack/ui/components/project_initializer.py:507 msgid "Wizard Error" msgstr "" -#: src/zebtrack/ui/components/project_initializer.py:509 +#: src/zebtrack/ui/components/project_initializer.py:508 #, python-brace-format msgid "Missing required fields: {fields}" msgstr "" @@ -4542,7 +4641,7 @@ msgid "" " Summary" msgstr "" -#: src/zebtrack/ui/components/project_overview.py:432 +#: src/zebtrack/ui/components/project_overview.py:431 #: src/zebtrack/ui/components/project_views/report_tree_builder.py:353 #: src/zebtrack/ui/components/project_views/video_selector_tree_manager.py:509 #: src/zebtrack/ui/components/validation_manager.py:354 @@ -4551,12 +4650,12 @@ msgstr "" msgid "🐟 Subject {label}" msgstr "" -#: src/zebtrack/ui/components/project_overview.py:462 +#: src/zebtrack/ui/components/project_overview.py:461 #: src/zebtrack/ui/components/project_views/report_tree_builder.py:285 msgid "🧾 Partial Reports" msgstr "" -#: src/zebtrack/ui/components/project_overview.py:475 +#: src/zebtrack/ui/components/project_overview.py:474 msgid "Partial Report" msgstr "" @@ -4631,7 +4730,7 @@ msgid "Template '{name}' is available for use." msgstr "" #: src/zebtrack/ui/components/single_video_workflow.py:74 -#: src/zebtrack/ui/components/single_video_workflow.py:354 +#: src/zebtrack/ui/components/single_video_workflow.py:351 #, python-brace-format msgid "Failed to start the analysis: {error}" msgstr "" @@ -4658,20 +4757,20 @@ msgstr "" msgid "The number of frames to analyse must be a positive whole number." msgstr "" -#: src/zebtrack/ui/components/single_video_workflow.py:260 +#: src/zebtrack/ui/components/single_video_workflow.py:259 msgid "Could not start camera detection (calibration coordinator unavailable)." msgstr "" -#: src/zebtrack/ui/components/single_video_workflow.py:278 +#: src/zebtrack/ui/components/single_video_workflow.py:275 #, python-brace-format msgid "Automatic camera detection failed: {error}" msgstr "" -#: src/zebtrack/ui/components/single_video_workflow.py:385 +#: src/zebtrack/ui/components/single_video_workflow.py:382 msgid "No aquarium has been defined." msgstr "" -#: src/zebtrack/ui/components/single_video_workflow.py:390 +#: src/zebtrack/ui/components/single_video_workflow.py:387 msgid "The aquarium's main area (polygon) has not been defined." msgstr "" diff --git a/tests/analysis/test_data_transformer_roi_metrics.py b/tests/analysis/test_data_transformer_roi_metrics.py index a406119f..3a70003a 100644 --- a/tests/analysis/test_data_transformer_roi_metrics.py +++ b/tests/analysis/test_data_transformer_roi_metrics.py @@ -339,7 +339,7 @@ def test_multi_animal_warning_points_to_the_summary_sheet(): freezing_min_duration=2.0, ) - multi_animal = [w for w in warnings if "animais (track_ids)" in w] + multi_animal = [w for w in warnings if "animals (track_ids)" in w] assert multi_animal, "aviso multi-animal não emitido" assert PER_ANIMAL_SHEET_NAME in multi_animal[0] assert "_summary.xlsx" in multi_animal[0] diff --git a/tests/analysis/test_roi_analyzer.py b/tests/analysis/test_roi_analyzer.py index a68afb6a..3c19d998 100644 --- a/tests/analysis/test_roi_analyzer.py +++ b/tests/analysis/test_roi_analyzer.py @@ -122,7 +122,7 @@ def test_bbox_intersects_missing_columns_error(self): inclusion_rule="bbox_intersects", ) - self.assertIn("bbox_intersects requer colunas de bbox", str(context.exception)) + self.assertIn("bbox_intersects rule requires the bbox columns", str(context.exception)) def test_seg_overlap_without_masks_degrades_instead_of_raising(self): """Sem sidecar, ``seg_overlap`` cai para ``bbox_intersects`` COM aviso. diff --git a/tests/analysis/test_seg_overlap_fallback.py b/tests/analysis/test_seg_overlap_fallback.py index 32c055be..b570ca38 100644 --- a/tests/analysis/test_seg_overlap_fallback.py +++ b/tests/analysis/test_seg_overlap_fallback.py @@ -84,7 +84,7 @@ def test_missing_sidecar_matches_bbox_intersects_exactly() -> None: ) assert len(degraded.degradation_warnings) == 1 assert degraded.degradation_warnings[0].startswith( - "Regra de ROI 'seg_overlap' degradada para 'bbox_intersects'" + "ROI rule 'seg_overlap' degraded to 'bbox_intersects'" ) @@ -106,7 +106,7 @@ def test_nonexistent_path_degrades_without_raising(tmp_path: Path) -> None: analyzer = _build("seg_overlap", mask_source=tmp_path / "3b_Mascaras_inexistente.parquet") assert len(analyzer.degradation_warnings) == 1 - assert "não existe" in analyzer.degradation_warnings[0] + assert "does not exist" in analyzer.degradation_warnings[0] def test_empty_sidecar_degrades(tmp_path: Path) -> None: @@ -116,7 +116,7 @@ def test_empty_sidecar_degrades(tmp_path: Path) -> None: analyzer = _build("seg_overlap", mask_source=path) assert len(analyzer.degradation_warnings) == 1 - assert "não tem linhas" in analyzer.degradation_warnings[0] + assert "has no rows" in analyzer.degradation_warnings[0] def test_sidecar_with_wrong_columns_degrades() -> None: @@ -142,7 +142,7 @@ def test_sidecar_that_matches_nothing_degrades() -> None: analyzer = _build("seg_overlap", mask_source=foreign) assert len(analyzer.degradation_warnings) == 1 - assert "Nenhuma máscara" in analyzer.degradation_warnings[0] + assert "No mask in the sidecar" in analyzer.degradation_warnings[0] def test_unreadable_file_degrades(tmp_path: Path) -> None: @@ -152,7 +152,7 @@ def test_unreadable_file_degrades(tmp_path: Path) -> None: analyzer = _build("seg_overlap", mask_source=path) assert len(analyzer.degradation_warnings) == 1 - assert "não pôde ser lido" in analyzer.degradation_warnings[0] + assert "could not be read" in analyzer.degradation_warnings[0] def test_warning_reaches_validation_warnings_of_the_report() -> None: diff --git a/tests/analysis/test_visualization_generator.py b/tests/analysis/test_visualization_generator.py index bdc641d8..eb02b765 100644 --- a/tests/analysis/test_visualization_generator.py +++ b/tests/analysis/test_visualization_generator.py @@ -455,7 +455,7 @@ def test_generate_roi_reference_plot_no_rois(behavior_analyzer, mock_settings): assert fig is not None ax = fig.get_axes()[0] # Should show message about no ROIs (in Portuguese) - assert any("sem ROIs" in text.get_text() for text in ax.texts) + assert any("no ROIs defined" in text.get_text() for text in ax.texts) # Clean up plt.close(fig) diff --git a/tests/i18n/test_no_untranslated_literals.py b/tests/i18n/test_no_untranslated_literals.py index 612a6210..6a3f6098 100644 --- a/tests/i18n/test_no_untranslated_literals.py +++ b/tests/i18n/test_no_untranslated_literals.py @@ -28,6 +28,7 @@ # Migrated in PR1. Extend as later PRs land. MIGRATED_PATHS: tuple[str, ...] = ( + "src/zebtrack/analysis", "src/zebtrack/coordinators", "src/zebtrack/core/app_runner.py", "src/zebtrack/core/application_bootstrapper.py", diff --git a/tests/test_geotaxis_features.py b/tests/test_geotaxis_features.py index 7504719b..f3013121 100644 --- a/tests/test_geotaxis_features.py +++ b/tests/test_geotaxis_features.py @@ -12,9 +12,9 @@ def test_rename_geotaxis_columns_3_zones(self): # Mapping input column -> expected output test_cases = { - "geotaxis_zone_0_pct": "Fundo (0.0-5.0cm) [%]", - "geotaxis_zone_1_pct": "Meio (5.0-10.0cm) [%]", - "geotaxis_zone_2_pct": "Superfície (10.0-15.0cm) [%]", + "geotaxis_zone_0_pct": "Bottom (0.0-5.0cm) [%]", + "geotaxis_zone_1_pct": "Middle (5.0-10.0cm) [%]", + "geotaxis_zone_2_pct": "Surface (10.0-15.0cm) [%]", } # Create DataFrame with all columns @@ -27,16 +27,16 @@ def test_rename_geotaxis_columns_3_zones(self): assert input_col not in result_df.columns def test_rename_geotaxis_columns_4_zones(self): - """Test renaming for 4 zones (Bottom, Meio 1, Meio 2, Surface).""" + """Test renaming for 4 zones (Bottom, Middle 1, Middle 2, Surface).""" dt = DataTransformer() height_cm = 20.0 num_zones = 4 test_cases = { - "geotaxis_zone_0_pct": "Fundo (0.0-5.0cm) [%]", - "geotaxis_zone_1_pct": "Meio 1 (5.0-10.0cm) [%]", - "geotaxis_zone_2_pct": "Meio 2 (10.0-15.0cm) [%]", - "geotaxis_zone_3_pct": "Superfície (15.0-20.0cm) [%]", + "geotaxis_zone_0_pct": "Bottom (0.0-5.0cm) [%]", + "geotaxis_zone_1_pct": "Middle 1 (5.0-10.0cm) [%]", + "geotaxis_zone_2_pct": "Middle 2 (10.0-15.0cm) [%]", + "geotaxis_zone_3_pct": "Surface (15.0-20.0cm) [%]", } df = pd.DataFrame(columns=list(test_cases.keys())) @@ -52,8 +52,8 @@ def test_rename_geotaxis_columns_2_zones(self): num_zones = 2 test_cases = { - "geotaxis_zone_0_pct": "Fundo (0.0-5.0cm) [%]", - "geotaxis_zone_1_pct": "Superfície (5.0-10.0cm) [%]", + "geotaxis_zone_0_pct": "Bottom (0.0-5.0cm) [%]", + "geotaxis_zone_1_pct": "Surface (5.0-10.0cm) [%]", } df = pd.DataFrame(columns=list(test_cases.keys())) @@ -66,7 +66,7 @@ def test_ignore_non_geotaxis_columns(self): """Ensure non-geotaxis columns are left untouched.""" dt = DataTransformer() cols = ["Total Distance", "Average Speed", "geotaxis_zone_0_pct"] - expected_cols = ["Total Distance", "Average Speed", "Fundo (0.0-5.0cm) [%]"] + expected_cols = ["Total Distance", "Average Speed", "Bottom (0.0-5.0cm) [%]"] df = pd.DataFrame(columns=cols) result_df = dt.rename_geotaxis_columns(df, height_cm=15.0, num_zones=3) From 5ddb35ddec21ed006cc29d3b91057c499effa4cd Mon Sep 17 00:00:00 2001 From: MarkSant Date: Thu, 13 Aug 2026 13:30:34 -0300 Subject: [PATCH 02/21] feat(i18n): traduzir a casca da UI -- gui.py e ui_coordinator.py (fase 3, lote 2) Os dois arquivos soltos em `src/zebtrack/ui/` vao a zero e entram no ratchet. O achado do lote: DEZ dos dezesseis msgids necessarios JA EXISTIAM com o portugues identico. `gui.py` mantem copias proprias de dialogos e de defaults de estado que os componentes migrados na fase 2 ja possuem -- "No Project", "Open a project before adjusting the specific calibration.", "Failed to save camera", "Could not save the camera as the project default:", "Form reloaded", "Values restored to reflect the current settings." (dialog_manager), "No analysis in progress.", "No task in progress.", "Analysis configuration: {value}" (analysis_controls) e "Error" (pr1). Todos reusados; `pr3-ui-shell.json` so carrega os 6 genuinamente novos. A duplicacao em si nao foi mexida -- e refactor, nao traducao -- mas fica registrada aqui. Outros pontos: - `gui.py:805` ja estava em INGLES e sem `_()` ("This project is not configured for live experimental tracking."): aparecia em ingles para o usuario pt_BR sem ninguem notar, porque o scanner so procura acento. Agora traduzido de fato. - `gui.py:865` passava `reason="Atualizacao manual"` ao debouncer do overview. `reason=` e tag de proveniencia, nunca renderizada -- virou "Manual refresh" em ingles FIXO, sem `_()`, como todo outro `reason=` da camada de UI ("zones_concluded", "Template Applied", "TabBuilder initialization"). - `ui_coordinator.py:873` montava "sessao(oes)" para fugir do plural. Virou dois msgids simples escolhidos por `session_count == 1`, o padrao do projeto (format_status_label): os arquivos de pares nao carregam formas plurais e ngettext nao e usado em lugar nenhum. - Tres literais sem acento que o scanner nao ve entraram junto ("Ocorreu um erro desconhecido.", "Nenhuma tarefa em andamento.", "Nenhum Projeto"), achados por varredura lexica dos dois arquivos. Verde: ruff limpo, mypy limpo em 698 arquivos, 4211 fast (+2 = os dois novos casos parametrizados do ratchet), 1079 gui. Co-Authored-By: Claude Opus 5 --- docs/tasks/active/ROLLING_TASK_LOG.md | 4 +- src/zebtrack/locales/_pairs/pr3-ui-shell.json | 11 + .../locales/pt_BR/LC_MESSAGES/zebtrack.mo | Bin 136997 -> 137669 bytes .../locales/pt_BR/LC_MESSAGES/zebtrack.po | 189 +++++++++++------- src/zebtrack/locales/zebtrack.pot | 162 +++++++++------ src/zebtrack/ui/gui.py | 35 ++-- src/zebtrack/ui/ui_coordinator.py | 15 +- tests/i18n/test_no_untranslated_literals.py | 2 + 8 files changed, 262 insertions(+), 156 deletions(-) create mode 100644 src/zebtrack/locales/_pairs/pr3-ui-shell.json diff --git a/docs/tasks/active/ROLLING_TASK_LOG.md b/docs/tasks/active/ROLLING_TASK_LOG.md index 6c79aa9b..51053025 100644 --- a/docs/tasks/active/ROLLING_TASK_LOG.md +++ b/docs/tasks/active/ROLLING_TASK_LOG.md @@ -35,7 +35,9 @@ no topo do ratchet; depois, atualizar a seção "A migração" em 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. -- [ ] Lote 2: `ui/gui.py` + `ui/ui_coordinator.py`. +- [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. - [ ] Lote 3: `core/**` (29 arquivos), incluindo simplificar o consumidor de `progress_notifier` para só "step:" depois de migrar o produtor. - [ ] Lote 4: `ui/wizard/**` (13 arquivos) — valores de enum em models.py/enums.py 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/pt_BR/LC_MESSAGES/zebtrack.mo b/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.mo index 4e90c2a5c8bf85792697b8151bcc3c86f3c1b8eb..ca713b04999996c5d24551f37fe3caaa4ceb2dc5 100644 GIT binary patch delta 20679 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 diff --git a/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.po b/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.po index 91fe744d..05d760d5 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-13 13:03-0300\n" +"POT-Creation-Date: 2026-08-13 13:24-0300\n" "PO-Revision-Date: 2026-08-08 21:41-0300\n" "Last-Translator: FULL NAME \n" "Language-Team: pt_BR \n" @@ -127,7 +127,7 @@ msgstr "" 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:1050 +#: src/zebtrack/analysis/roi.py:1049 msgid "" "No mask in the sidecar matches a (frame, track_id) of the analysed " "trajectory." @@ -135,7 +135,7 @@ msgstr "" "Nenhuma máscara do sidecar corresponde a (frame, track_id) da trajetória " "analisada." -#: src/zebtrack/analysis/roi.py:1071 +#: 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 " @@ -145,11 +145,11 @@ msgstr "" "(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:1081 +#: 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:1090 +#: src/zebtrack/analysis/roi.py:1087 #, python-brace-format msgid "" "The mask sidecar does not exist at '{path}'. Data recorded before this " @@ -158,19 +158,19 @@ 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:1102 +#: 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:1108 +#: 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:1117 +#: 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}" @@ -256,6 +256,7 @@ msgstr "Vídeo Único" #: 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/gui.py:144 src/zebtrack/ui/gui.py:808 msgid "Error" msgstr "Erro" @@ -1832,6 +1833,88 @@ msgstr "Não foi possível enviar comando: Arduino desconectado." msgid "Serial connection to the Arduino was lost." msgstr "Conexão serial com Arduino perdida." +# | msgid "unknown" +#: src/zebtrack/ui/gui.py:145 +#| msgid "Unknown error" +msgid "An unknown error occurred." +msgstr "Ocorreu um erro desconhecido." + +# | msgid "Analysis in Progress" +#: src/zebtrack/ui/components/analysis_controls.py:48 +#: src/zebtrack/ui/components/analysis_display.py:63 +#: src/zebtrack/ui/components/analysis_display.py:437 +#: src/zebtrack/ui/components/analysis_view_controller.py:221 +#: src/zebtrack/ui/components/project_initializer.py:126 +#: src/zebtrack/ui/components/single_video_workflow.py:92 +#: src/zebtrack/ui/components/state_synchronizer.py:269 +#: src/zebtrack/ui/gui.py:358 +msgid "No analysis in progress." +msgstr "Nenhuma análise em andamento." + +# | msgid "Analysis in Progress" +#: src/zebtrack/ui/components/analysis_display.py:64 +#: src/zebtrack/ui/components/analysis_display.py:438 +#: src/zebtrack/ui/components/analysis_view_controller.py:223 +#: src/zebtrack/ui/components/state_synchronizer.py:491 +#: src/zebtrack/ui/gui.py:359 +msgid "No task in progress." +msgstr "Nenhuma tarefa em andamento." + +# | msgid "Invalid Configuration" +#: src/zebtrack/ui/components/analysis_controls.py:69 +#: src/zebtrack/ui/components/analysis_controls.py:214 +#: src/zebtrack/ui/components/analysis_display.py:76 +#: src/zebtrack/ui/components/analysis_display.py:298 +#: src/zebtrack/ui/components/analysis_display.py:306 +#: src/zebtrack/ui/components/analysis_view_controller.py:308 +#: src/zebtrack/ui/gui.py:361 +#, python-brace-format +msgid "Analysis configuration: {value}" +msgstr "Configuração de análise: {value}" + +# | msgid "Error Saving to Project" +# | msgid "Save to Project" +#: src/zebtrack/ui/components/dialog_manager.py:444 src/zebtrack/ui/gui.py:576 +msgid "No Project" +msgstr "Nenhum Projeto" + +#: src/zebtrack/ui/components/dialog_manager.py:445 src/zebtrack/ui/gui.py:577 +msgid "Open a project before adjusting the specific calibration." +msgstr "Abra um projeto antes de ajustar a calibração específica." + +#: src/zebtrack/ui/builders/analysis_widgets.py:339 +#: src/zebtrack/ui/components/state_synchronizer.py:430 +#: src/zebtrack/ui/gui.py:600 +msgid "Form reloaded" +msgstr "Formulário recarregado" + +#: src/zebtrack/ui/builders/analysis_widgets.py:340 +#: src/zebtrack/ui/components/state_synchronizer.py:431 +#: src/zebtrack/ui/gui.py:601 +msgid "Values restored to reflect the current settings." +msgstr "Valores restaurados para refletir as configurações atuais." + +#: src/zebtrack/ui/gui.py:809 +msgid "This project is not configured for live experimental tracking." +msgstr "" +"Este projeto não está configurado para rastreamento experimental ao vivo." + +# | msgid "Failed to open folder" +#: src/zebtrack/ui/components/dialog_manager.py:824 src/zebtrack/ui/gui.py:840 +msgid "Failed to save camera" +msgstr "Falha ao salvar câmera" + +# | msgid "Could not save config.local.yaml: {error}" +# | msgid "Could not save the file: {error}" +#: src/zebtrack/ui/components/dialog_manager.py:825 src/zebtrack/ui/gui.py:841 +#, python-brace-format +msgid "" +"Could not save the camera as the project default:\n" +"{error}" +msgstr "" +"Não foi possível salvar a câmera como padrão do projeto:\n" +"{error}" + #: src/zebtrack/ui/project_workflow_adapter.py:307 msgid "Project Loaded" msgstr "Projeto Carregado" @@ -1892,6 +1975,30 @@ msgstr "Ambos" msgid "First run — optimizing for your hardware..." msgstr "Primeira execução — otimizando para seu hardware..." +#: src/zebtrack/ui/ui_coordinator.py:737 +#, python-brace-format +msgid "Camera reconnected (gap: {seconds:.1f}s)" +msgstr "Câmera reconectada (gap: {seconds:.1f}s)" + +#: src/zebtrack/ui/ui_coordinator.py:825 +#, python-brace-format +msgid "Detecting aquarium: frame {frame}/100" +msgstr "Detectando aquário: frame {frame}/100" + +# | msgid "Remove the trajectory generated for this video?" +#: src/zebtrack/ui/ui_coordinator.py:879 +#, python-brace-format +#| msgid "Reports generated for {count} videos." +msgid "Batch report generated ({count} session): {batch_id}" +msgstr "Relatório de lote gerado ({count} sessão): {batch_id}" + +# | msgid "Remove the trajectory generated for this video?" +#: src/zebtrack/ui/ui_coordinator.py:881 +#, python-brace-format +#| msgid "Reports generated for {count} videos." +msgid "Batch report generated ({count} sessions): {batch_id}" +msgstr "Relatório de lote gerado ({count} sessões): {batch_id}" + #: src/zebtrack/ui/builders/analysis_widgets.py:62 #: src/zebtrack/ui/components/analysis_controls.py:167 msgid "Video Analysis" @@ -1910,16 +2017,6 @@ msgstr "Config. Avançadas" msgid "Settings unavailable. Could not load." msgstr "Settings não disponível. Não foi possível carregar." -#: src/zebtrack/ui/builders/analysis_widgets.py:339 -#: src/zebtrack/ui/components/state_synchronizer.py:430 -msgid "Form reloaded" -msgstr "Formulário recarregado" - -#: 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/ui/builders/analysis_widgets.py:354 msgid "Settings unavailable. Could not save." msgstr "Settings não disponível. Não foi possível salvar." @@ -2277,17 +2374,6 @@ msgstr "Selecione um aquário ativo primeiro." msgid "The aquarium polygon data could not be retrieved." msgstr "Não foi possível obter os dados do polígono do aquário." -# | msgid "Analysis in Progress" -#: src/zebtrack/ui/components/analysis_controls.py:48 -#: src/zebtrack/ui/components/analysis_display.py:63 -#: src/zebtrack/ui/components/analysis_display.py:437 -#: src/zebtrack/ui/components/analysis_view_controller.py:221 -#: src/zebtrack/ui/components/project_initializer.py:126 -#: src/zebtrack/ui/components/single_video_workflow.py:92 -#: src/zebtrack/ui/components/state_synchronizer.py:269 -msgid "No analysis in progress." -msgstr "Nenhuma análise em andamento." - #: src/zebtrack/ui/components/analysis_controls.py:52 #: src/zebtrack/ui/components/analysis_controls.py:202 #: src/zebtrack/ui/components/analysis_display.py:66 @@ -2339,31 +2425,12 @@ msgstr "Modo de rastreamento: {value}" msgid "Multi-individual" msgstr "Multi-indivíduos" -# | msgid "Invalid Configuration" -#: src/zebtrack/ui/components/analysis_controls.py:69 -#: src/zebtrack/ui/components/analysis_controls.py:214 -#: src/zebtrack/ui/components/analysis_display.py:76 -#: src/zebtrack/ui/components/analysis_display.py:298 -#: src/zebtrack/ui/components/analysis_display.py:306 -#: src/zebtrack/ui/components/analysis_view_controller.py:308 -#, python-brace-format -msgid "Analysis configuration: {value}" -msgstr "Configuração de análise: {value}" - #: src/zebtrack/ui/components/analysis_controls.py:74 #: src/zebtrack/ui/components/analysis_display.py:442 #: src/zebtrack/ui/components/state_synchronizer.py:589 msgid "Social interactions: waiting for data." msgstr "Interações sociais: aguardando dados." -# | msgid "Analysis in Progress" -#: src/zebtrack/ui/components/analysis_display.py:64 -#: src/zebtrack/ui/components/analysis_display.py:438 -#: src/zebtrack/ui/components/analysis_view_controller.py:223 -#: src/zebtrack/ui/components/state_synchronizer.py:491 -msgid "No task in progress." -msgstr "Nenhuma tarefa em andamento." - #: src/zebtrack/ui/components/analysis_display.py:84 msgid "Social interactions: not applicable right now." msgstr "Interações sociais: não aplicável no momento." @@ -3516,16 +3583,6 @@ msgstr "" "\n" "Se escolher 'Não', apenas a referência no projeto será removida." -# | msgid "Error Saving to Project" -# | msgid "Save to Project" -#: src/zebtrack/ui/components/dialog_manager.py:444 -msgid "No Project" -msgstr "Nenhum Projeto" - -#: src/zebtrack/ui/components/dialog_manager.py:445 -msgid "Open a project before adjusting the specific calibration." -msgstr "Abra um projeto antes de ajustar a calibração específica." - #: src/zebtrack/ui/components/dialog_manager.py:511 #: src/zebtrack/ui/components/roi_template_manager.py:422 msgid "Import ROI Template into the Library" @@ -3585,22 +3642,6 @@ msgstr "Erro ao importar e aplicar" msgid "Template applied" msgstr "Template aplicado" -# | msgid "Failed to open folder" -#: src/zebtrack/ui/components/dialog_manager.py:824 -msgid "Failed to save camera" -msgstr "Falha ao salvar câmera" - -# | msgid "Could not save config.local.yaml: {error}" -# | msgid "Could not save the file: {error}" -#: src/zebtrack/ui/components/dialog_manager.py:825 -#, python-brace-format -msgid "" -"Could not save the camera as the project default:\n" -"{error}" -msgstr "" -"Não foi possível salvar a câmera como padrão do projeto:\n" -"{error}" - # | msgid "Open Existing Project" #: src/zebtrack/ui/components/dialog_manager.py:848 #: src/zebtrack/ui/components/project_initializer.py:533 diff --git a/src/zebtrack/locales/zebtrack.pot b/src/zebtrack/locales/zebtrack.pot index ce20231e..4f52ee5a 100644 --- a/src/zebtrack/locales/zebtrack.pot +++ b/src/zebtrack/locales/zebtrack.pot @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: DRerio LogAI VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-08-13 13:03-0300\n" +"POT-Creation-Date: 2026-08-13 13:24-0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -107,41 +107,41 @@ msgstr "" msgid "The mask sidecar contains no valid geometry." msgstr "" -#: src/zebtrack/analysis/roi.py:1050 +#: src/zebtrack/analysis/roi.py:1049 msgid "" "No mask in the sidecar matches a (frame, track_id) of the analysed " "trajectory." msgstr "" -#: src/zebtrack/analysis/roi.py:1071 +#: 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 "" -#: src/zebtrack/analysis/roi.py:1081 +#: src/zebtrack/analysis/roi.py:1078 msgid "The mask sidecar is empty." msgstr "" -#: src/zebtrack/analysis/roi.py:1090 +#: 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 "" -#: src/zebtrack/analysis/roi.py:1102 +#: src/zebtrack/analysis/roi.py:1099 #, python-brace-format msgid "The sidecar '{path}' could not be read: {error}" msgstr "" -#: src/zebtrack/analysis/roi.py:1108 +#: src/zebtrack/analysis/roi.py:1104 #, python-brace-format msgid "The sidecar '{path}' has no rows." msgstr "" -#: src/zebtrack/analysis/roi.py:1117 +#: src/zebtrack/analysis/roi.py:1112 #, python-brace-format msgid "ROI rule 'seg_overlap' degraded to 'bbox_intersects': {detail}" msgstr "" @@ -221,6 +221,7 @@ msgstr "" #: 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/gui.py:144 src/zebtrack/ui/gui.py:808 msgid "Error" msgstr "" @@ -1467,6 +1468,75 @@ msgstr "" msgid "Serial connection to the Arduino was lost." msgstr "" +#: src/zebtrack/ui/gui.py:145 +msgid "An unknown error occurred." +msgstr "" + +#: src/zebtrack/ui/components/analysis_controls.py:48 +#: src/zebtrack/ui/components/analysis_display.py:63 +#: src/zebtrack/ui/components/analysis_display.py:437 +#: src/zebtrack/ui/components/analysis_view_controller.py:221 +#: src/zebtrack/ui/components/project_initializer.py:126 +#: src/zebtrack/ui/components/single_video_workflow.py:92 +#: src/zebtrack/ui/components/state_synchronizer.py:269 +#: src/zebtrack/ui/gui.py:358 +msgid "No analysis in progress." +msgstr "" + +#: src/zebtrack/ui/components/analysis_display.py:64 +#: src/zebtrack/ui/components/analysis_display.py:438 +#: src/zebtrack/ui/components/analysis_view_controller.py:223 +#: src/zebtrack/ui/components/state_synchronizer.py:491 +#: src/zebtrack/ui/gui.py:359 +msgid "No task in progress." +msgstr "" + +#: src/zebtrack/ui/components/analysis_controls.py:69 +#: src/zebtrack/ui/components/analysis_controls.py:214 +#: src/zebtrack/ui/components/analysis_display.py:76 +#: src/zebtrack/ui/components/analysis_display.py:298 +#: src/zebtrack/ui/components/analysis_display.py:306 +#: src/zebtrack/ui/components/analysis_view_controller.py:308 +#: src/zebtrack/ui/gui.py:361 +#, python-brace-format +msgid "Analysis configuration: {value}" +msgstr "" + +#: src/zebtrack/ui/components/dialog_manager.py:444 src/zebtrack/ui/gui.py:576 +msgid "No Project" +msgstr "" + +#: src/zebtrack/ui/components/dialog_manager.py:445 src/zebtrack/ui/gui.py:577 +msgid "Open a project before adjusting the specific calibration." +msgstr "" + +#: src/zebtrack/ui/builders/analysis_widgets.py:339 +#: src/zebtrack/ui/components/state_synchronizer.py:430 +#: src/zebtrack/ui/gui.py:600 +msgid "Form reloaded" +msgstr "" + +#: src/zebtrack/ui/builders/analysis_widgets.py:340 +#: src/zebtrack/ui/components/state_synchronizer.py:431 +#: src/zebtrack/ui/gui.py:601 +msgid "Values restored to reflect the current settings." +msgstr "" + +#: src/zebtrack/ui/gui.py:809 +msgid "This project is not configured for live experimental tracking." +msgstr "" + +#: src/zebtrack/ui/components/dialog_manager.py:824 src/zebtrack/ui/gui.py:840 +msgid "Failed to save camera" +msgstr "" + +#: src/zebtrack/ui/components/dialog_manager.py:825 src/zebtrack/ui/gui.py:841 +#, python-brace-format +msgid "" +"Could not save the camera as the project default:\n" +"{error}" +msgstr "" + #: src/zebtrack/ui/project_workflow_adapter.py:307 msgid "Project Loaded" msgstr "" @@ -1517,6 +1587,26 @@ msgstr "" msgid "First run — optimizing for your hardware..." msgstr "" +#: src/zebtrack/ui/ui_coordinator.py:737 +#, python-brace-format +msgid "Camera reconnected (gap: {seconds:.1f}s)" +msgstr "" + +#: src/zebtrack/ui/ui_coordinator.py:825 +#, python-brace-format +msgid "Detecting aquarium: frame {frame}/100" +msgstr "" + +#: src/zebtrack/ui/ui_coordinator.py:879 +#, python-brace-format +msgid "Batch report generated ({count} session): {batch_id}" +msgstr "" + +#: src/zebtrack/ui/ui_coordinator.py:881 +#, python-brace-format +msgid "Batch report generated ({count} sessions): {batch_id}" +msgstr "" + #: src/zebtrack/ui/builders/analysis_widgets.py:62 #: src/zebtrack/ui/components/analysis_controls.py:167 msgid "Video Analysis" @@ -1534,16 +1624,6 @@ msgstr "" msgid "Settings unavailable. Could not load." msgstr "" -#: src/zebtrack/ui/builders/analysis_widgets.py:339 -#: src/zebtrack/ui/components/state_synchronizer.py:430 -msgid "Form reloaded" -msgstr "" - -#: 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 "" - #: src/zebtrack/ui/builders/analysis_widgets.py:354 msgid "Settings unavailable. Could not save." msgstr "" @@ -1869,16 +1949,6 @@ msgstr "" msgid "The aquarium polygon data could not be retrieved." msgstr "" -#: src/zebtrack/ui/components/analysis_controls.py:48 -#: src/zebtrack/ui/components/analysis_display.py:63 -#: src/zebtrack/ui/components/analysis_display.py:437 -#: src/zebtrack/ui/components/analysis_view_controller.py:221 -#: src/zebtrack/ui/components/project_initializer.py:126 -#: src/zebtrack/ui/components/single_video_workflow.py:92 -#: src/zebtrack/ui/components/state_synchronizer.py:269 -msgid "No analysis in progress." -msgstr "" - #: src/zebtrack/ui/components/analysis_controls.py:52 #: src/zebtrack/ui/components/analysis_controls.py:202 #: src/zebtrack/ui/components/analysis_display.py:66 @@ -1929,29 +1999,12 @@ msgstr "" msgid "Multi-individual" msgstr "" -#: src/zebtrack/ui/components/analysis_controls.py:69 -#: src/zebtrack/ui/components/analysis_controls.py:214 -#: src/zebtrack/ui/components/analysis_display.py:76 -#: src/zebtrack/ui/components/analysis_display.py:298 -#: src/zebtrack/ui/components/analysis_display.py:306 -#: src/zebtrack/ui/components/analysis_view_controller.py:308 -#, python-brace-format -msgid "Analysis configuration: {value}" -msgstr "" - #: src/zebtrack/ui/components/analysis_controls.py:74 #: src/zebtrack/ui/components/analysis_display.py:442 #: src/zebtrack/ui/components/state_synchronizer.py:589 msgid "Social interactions: waiting for data." msgstr "" -#: src/zebtrack/ui/components/analysis_display.py:64 -#: src/zebtrack/ui/components/analysis_display.py:438 -#: src/zebtrack/ui/components/analysis_view_controller.py:223 -#: src/zebtrack/ui/components/state_synchronizer.py:491 -msgid "No task in progress." -msgstr "" - #: src/zebtrack/ui/components/analysis_display.py:84 msgid "Social interactions: not applicable right now." msgstr "" @@ -2866,14 +2919,6 @@ msgid "" "Choosing 'No' removes only the reference in the project." msgstr "" -#: src/zebtrack/ui/components/dialog_manager.py:444 -msgid "No Project" -msgstr "" - -#: src/zebtrack/ui/components/dialog_manager.py:445 -msgid "Open a project before adjusting the specific calibration." -msgstr "" - #: src/zebtrack/ui/components/dialog_manager.py:511 #: src/zebtrack/ui/components/roi_template_manager.py:422 msgid "Import ROI Template into the Library" @@ -2921,17 +2966,6 @@ msgstr "" msgid "Template applied" msgstr "" -#: src/zebtrack/ui/components/dialog_manager.py:824 -msgid "Failed to save camera" -msgstr "" - -#: src/zebtrack/ui/components/dialog_manager.py:825 -#, python-brace-format -msgid "" -"Could not save the camera as the project default:\n" -"{error}" -msgstr "" - #: src/zebtrack/ui/components/dialog_manager.py:848 #: src/zebtrack/ui/components/project_initializer.py:533 msgid "Select an Existing Project Folder" diff --git a/src/zebtrack/ui/gui.py b/src/zebtrack/ui/gui.py index 7d887c0b..715b9872 100644 --- a/src/zebtrack/ui/gui.py +++ b/src/zebtrack/ui/gui.py @@ -32,6 +32,7 @@ ) from zebtrack.core.services.zone_context_service import ZoneContextService from zebtrack.core.video.processing_mode import ProcessingMode +from zebtrack.i18n import _ from zebtrack.ui.builders import ButtonFactory, PanelBuilder, ZoneControlBuilder from zebtrack.ui.components import ( AnalysisDisplayWidget, @@ -140,8 +141,8 @@ def _on_error_occurred(data: Any) -> None: self.root.after( 0, lambda: self.dialog_manager.show_error( - _payload_get(data, "title", "Erro"), - _payload_get(data, "message", "Ocorreu um erro desconhecido."), + _payload_get(data, "title", _("Error")), + _payload_get(data, "message", _("An unknown error occurred.")), ), ) @@ -354,9 +355,11 @@ def _on_error_occurred(data: Any) -> None: self._analysis_overlay_image = None # Analysis status widgets and variables - self.analysis_status_var = StringVar(value="Nenhuma análise em andamento.") - self.analysis_task_var = StringVar(value="Nenhuma tarefa em andamento.") - self.analysis_profile_var = StringVar(value="Perfil de análise: default") + self.analysis_status_var = StringVar(value=_("No analysis in progress.")) + self.analysis_task_var = StringVar(value=_("No task in progress.")) + self.analysis_profile_var = StringVar( + value=_("Analysis configuration: {value}").format(value="default") + ) self.analysis_video_label: Label | None = None # User options @@ -570,8 +573,8 @@ def show_project_model_configuration_tab(self) -> bool: def _open_project_calibration_window(self): if not getattr(self.controller.project_manager, "project_path", None): self.dialog_manager.show_warning( - "Nenhum Projeto", - "Abra um projeto antes de ajustar a calibração específica.", + _("No Project"), + _("Open a project before adjusting the specific calibration."), ) return @@ -594,8 +597,8 @@ def _on_reset_global_config_form_widget(self) -> None: """Reset ConfigEditorWidget form fields to reflect current settings object.""" self._reload_config_editor_values_widget() self.dialog_manager.show_info( - "Formulário recarregado", - "Valores restaurados para refletir as configurações atuais.", + _("Form reloaded"), + _("Values restored to reflect the current settings."), ) def _reload_config_editor_values_widget(self) -> None: @@ -802,8 +805,8 @@ def ask_recording_details_unified(self): pm = self.controller.project_manager if not pm.project_data.get("experiment_days"): self.dialog_manager.show_error( - "Error", - "This project is not configured for live experimental tracking.", + _("Error"), + _("This project is not configured for live experimental tracking."), ) return None @@ -834,8 +837,10 @@ def _camera_provider(): pm.save_project() except (OSError, AttributeError, ValueError) as exc: self.dialog_manager.show_warning( - "Falha ao salvar câmera", - f"Não foi possível salvar a câmera como padrão do projeto:\n{exc}", + _("Failed to save camera"), + _("Could not save the camera as the project default:\n{error}").format( + error=exc + ), ) return result @@ -862,7 +867,9 @@ def _on_mousewheel(self, event): def _handle_project_refresh_requested(self, data: dict) -> None: """Handle project refresh request from overview widget.""" - self.video_selector_manager.request_overview_refresh(reason="Atualização manual") + # Provenance tag for the refresh debouncer, never rendered — English + # like every other `reason=` in the UI layer, and deliberately not _(). + self.video_selector_manager.request_overview_refresh(reason="Manual refresh") def _handle_project_video_double_click(self, payload: payloads.ItemIdPayload) -> None: """Handle video double-click from overview widget.""" diff --git a/src/zebtrack/ui/ui_coordinator.py b/src/zebtrack/ui/ui_coordinator.py index 55a99cf3..8dcc0aff 100644 --- a/src/zebtrack/ui/ui_coordinator.py +++ b/src/zebtrack/ui/ui_coordinator.py @@ -15,6 +15,7 @@ import structlog +from zebtrack.i18n import _ from zebtrack.ui import payloads as payloads from zebtrack.ui.event_bus_v2 import EventBusV2, UIEvents @@ -733,7 +734,7 @@ def _on_camera_reconnected(self, event_data: payloads.EventPayload) -> None: def update_status(): state_synchronizer.update_status( - f"Câmera reconectada (gap: {gap_duration:.1f}s)" + _("Camera reconnected (gap: {seconds:.1f}s)").format(seconds=gap_duration) ) self._safe_ui_call(update_status) @@ -819,7 +820,7 @@ def update_dialog(): def update_status(): state_synchronizer.update_status( - f"Detectando aquário: frame {frame_number}/100" + _("Detecting aquarium: frame {frame}/100").format(frame=frame_number) ) self._safe_ui_call(update_status) @@ -869,8 +870,16 @@ def refresh_views(): state_synchronizer = self.state_synchronizer def update_status(): + # Two plain msgids instead of ngettext: the pair files carry + # no plural forms, and the Portuguese original papered over + # the problem with "sessão(ões)". + template = ( + _("Batch report generated ({count} session): {batch_id}") + if session_count == 1 + else _("Batch report generated ({count} sessions): {batch_id}") + ) state_synchronizer.update_status( - f"Relatório de lote gerado ({session_count} sessão(ões)): {batch_id}" + template.format(count=session_count, batch_id=batch_id) ) self._safe_ui_call(update_status) diff --git a/tests/i18n/test_no_untranslated_literals.py b/tests/i18n/test_no_untranslated_literals.py index 6a3f6098..be953036 100644 --- a/tests/i18n/test_no_untranslated_literals.py +++ b/tests/i18n/test_no_untranslated_literals.py @@ -40,9 +40,11 @@ # here collapse into one path — a new file under ui/components/ is now # covered by the ratchet the moment it is added. "src/zebtrack/ui/components", + "src/zebtrack/ui/gui.py", "src/zebtrack/ui/language_dialog.py", "src/zebtrack/ui/project_workflow_adapter.py", "src/zebtrack/ui/splash_screen.py", + "src/zebtrack/ui/ui_coordinator.py", "src/zebtrack/utils", ) From f7440653e478ecb26189efe8716cf8ee26eec080 Mon Sep 17 00:00:00 2001 From: MarkSant Date: Thu, 13 Aug 2026 13:48:41 -0300 Subject: [PATCH 03/21] feat(i18n): traduzir core/recording/** (fase 3, lote 3a) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `core/recording` a zero e no ratchet: 53 literais em 5 arquivos, o fluxo ao vivo inteiro (selecao de modo por hardware, abertura de camera, status de atraso da analise, mensagens de conclusao). DOIS `_` COMO DESCARTE sombreavam o gettext -- o gotcha nº 1 do guia, de novo: live_session_manager.py:459 success, _ = initialize_detector(...) frame_processing_pipeline.py:693 detections, _ = detector.detect(...) Ambos ligam `_` como local, o que sombreia o alias do gettext na FUNCAO INTEIRA, inclusive nas chamadas ACIMA do ponto de atribuicao. Pegos por ruff F823 e por mypy (`Name "_" is used before definition`), NENHUM por leitura do diff. Renomeados para `_detector` / `_annotated` com comentario no local. Defeitos de texto corrigidos junto: - `live_camera_mode.recommend_mode` montava a recomendacao com `reason +=` sobre meia oracao ("...mas N foram solicitados. " + "Recomendado: ..."). Um tradutor nao consegue reordenar meia frase. Virou DUAS oracoes completas, cada uma seu msgid. - "aquario(s)" e "frame(s) descartado(s)" eram fuga do plural. Viraram pares singular/plural escolhidos por comparacao (`max_supported == 1`, `_dropped_frames_video == 1`), o padrao do projeto -- sem ngettext, que os arquivos de pares nao suportam. - `get_mode_description` monta o dicionario DENTRO da funcao, entao pode levar `_()` direto; comentario registra por que nao pode subir para o corpo do modulo. O docstring dizia "Portuguese description" -- agora "Description in the active interface language". - `LiveCameraModeRecommendation.__str__` NAO foi traduzido: nenhum call site o renderiza, e a primeira linha imprime `recommended_mode.value`, valor de enum persistido. Passou a ingles fixo, com o motivo no docstring. `reason == "no_detections"` (live_analysis_post_processor) e sentinela comparada, nao texto exibido -- intocada. Asserts migrados: test_live_camera_phase5_hygiene ("verifique" -> "check the disk"), test_live_camera_workflow_e2e ("HARDWARE INSUFICIENTE" -> "INSUFFICIENT HARDWARE"). Verde: ruff limpo, mypy limpo em 698 arquivos, 4212 fast, 1079 gui. Co-Authored-By: Claude Opus 5 --- docs/tasks/active/ROLLING_TASK_LOG.md | 10 +- .../recording/frame_processing_pipeline.py | 13 +- .../recording/live_analysis_post_processor.py | 60 ++-- .../core/recording/live_camera_mode.py | 102 ++++-- .../core/recording/live_session_manager.py | 60 ++-- .../core/recording/recording_service.py | 8 +- .../locales/_pairs/pr3-core-recording.json | 50 +++ .../locales/pt_BR/LC_MESSAGES/zebtrack.mo | Bin 137669 -> 144059 bytes .../locales/pt_BR/LC_MESSAGES/zebtrack.po | 336 +++++++++++++++++- src/zebtrack/locales/zebtrack.pot | 251 ++++++++++++- tests/core/test_live_camera_phase5_hygiene.py | 2 +- tests/i18n/test_no_untranslated_literals.py | 1 + tests/test_live_camera_workflow_e2e.py | 2 +- 13 files changed, 775 insertions(+), 120 deletions(-) create mode 100644 src/zebtrack/locales/_pairs/pr3-core-recording.json diff --git a/docs/tasks/active/ROLLING_TASK_LOG.md b/docs/tasks/active/ROLLING_TASK_LOG.md index 51053025..139fc48d 100644 --- a/docs/tasks/active/ROLLING_TASK_LOG.md +++ b/docs/tasks/active/ROLLING_TASK_LOG.md @@ -38,8 +38,14 @@ no topo do ratchet; depois, atualizar a seção "A migração" em - [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. -- [ ] Lote 3: `core/**` (29 arquivos), incluindo simplificar o consumidor de - `progress_notifier` para só "step:" depois de migrar o produtor. +- [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. +- [ ] Lote 3b: `core/project/**` (72 literais em 7 arquivos). +- [ ] Lote 3c: `core/services/**` (61 em 7 arquivos). +- [ ] Lote 3d: `core/video/**`, `core/detection/**`, `core/viewmodels/**` (30 em + 10 arquivos), incluindo simplificar o consumidor de `progress_notifier` + para só "step:" depois de migrar o produtor. - [ ] Lote 4: `ui/wizard/**` (13 arquivos) — valores de enum em models.py/enums.py são persistidos, traduzir só os rótulos exibidos. - [ ] Lote 5: `ui/dialogs/**` (26 arquivos). 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..5617d5f3 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) @@ -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, @@ -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 @@ -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/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/pt_BR/LC_MESSAGES/zebtrack.mo b/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.mo index ca713b04999996c5d24551f37fe3caaa4ceb2dc5..98cf8a0af1cab1300ea9fb75a36c1a6f5a995776 100644 GIT binary patch delta 26284 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 b-si$Bm;bT_)@saeOaF3z?&V4mZ~OiS=Cfu& delta 20325 zcmYM*1$0$MyTRxWZo##O(r0Rlx@T#I~o zX^RFgR$3hH|2ebQx_8~RelxT8nLYE)JF`zt`SzuFA4~Ff{S}yMzQzCjp31Va6yDwO+aU}JDsE%G@GoEW?qYpkn4d^B6 zIe|?rD;Gv#HlAwI9!9dF>^CBz_Q3#t%|I^)#`TRSWB3ah!{hcpS6gGjxShNYl!)YGMJ5zyYYxCZHm+1~s$8r~zF z_iD{&sb@vayas9_tuYAWoc8gUn)*Uif6H1Ee?4fMbHgvF&|F3(%N*Z0`8(<&&78S|=IoF>ct7Uz1>W$kHf31C&wx)xDn2q`bT!7257{;`- ztX|j?2jOK@ves#DX0!k`@I=(i*Q1j17t|6wM=f!w4#w)J`DZWeSM6^;aC#4U~asRIWSEZGr>q?r&}?OuJRQ0!74Zr zhoD0G#WA3(*}oB}$kf0J*b#kkDJt|EP)oHRwR9(tC|eht>jl{&S*TY;-QN<)4VTrE zfyJc?SPGpMY;i3(+k<3Fg++1)H_90p?GZ={}7=yljEUO?^Lp?AKwPf+A2TeyU z)hg7MmFhu=AlM&-g(Y=tW@JAOn>Fv}>jOiQ>4k7-An0c1lhNnXc_7)rf4s^1|_`!w{` z{$E8w9c)E^`~~%(b65>;qh_X8r8+K#^RPB5w0~k->@&v9>=bI>-$f;@&scL1HAVHi z74@7$m`?lug1f+soD0uT5B!LFp!fG?TjoNoZBZOw&W$rwz76EQc=M161vYVA&;M*aZ3@n6(Hzo0_vJDxWshM|_s zZ-Ti$FXp6P3U#10M-6Z(x<*jgLV-T4{6CnJZwnTo9y8IL1KqI)^>MfzpP|n7rISp~ zd_aXZ-DJyZin*{n4#Xz74fR0JDV9|lgHXx-%@pFVP^_RK7w*E|c*Ut#naVDp-U-z~ z+G*ysnhkY-9n6nCaS_hLxmaYnIZuwDmf#(RqYnv_4hx}nLAe>kKMjRuG{j(A)KbjD z)VLVk14M;*7ivk)pgOpV>hL4#KA)MUo&)`;7r@L|8nta3VJP-Oov>3~6tq@rorc5M zk@{a4jFo1YwQq*%@H^B8MV&GW~Y7zl^b`E z`&?E|HghBm4N(^cV;C!j|Cdrw4h&5&9k@_4oZ*;=90k?}490>#nvhq)8q^!3wpRj%;%*GUYghsw zV@b@h(7Y9!q9Qi|)AM|58ihbiM2&bWDrCnn3*N)V_z6R?0jaK~>hCxh^`H@`2y8_~ z;2_%c!+YK@A{i8Odz3J5c@QUvBy?aa^kNWThO2zI-_O~hsue?sH9niT7pfe z0e!)i=)1yf*Dk0KA4LuH6sq6*m=#~5BA9lisfVF=bHZX6%BR*$7lfr=te4 z0`-CQsF3bNb@&@ItX5bEEed+pJK&&@~m_v2dAz8*D!0~m(KP}d(K6LnedC@2Tg zt}`FVkBUS^bT18R=AE7EeNhn_i%QbDmQ%5PjznMFjLM0LxreIy||Faa7e3|!|ziLaM z2GkMz;AC8g|Dhf@Yrh%561+louf{mqM;s(6sXs%_F#K1OY$dTe^`@wS{fJuHpU_YH z{|bd58t$U9{6EZ!nGTr_iehc*4N&a~sMqLr)OOtGcpNiOzliSsu!2p0sHKcPV*0C( zy013|@_cKg3OExr@|CD5jauVLSPOSzRs4kWu+lLT`pc*R z-A66uI}E`z$BDm25>CNALev22qi*bgIk6vV4X2|bv;=kk0n`Alq9=NsFiGl#I!AJ1 zC|1D+9vtDQiETe+CVc)B@%N_TE)5#UQ`9#5gbH=q)8-pY1ggC$DhE2D2G9>H;zVqY z$5HoXKV!CCe#auH&qbs9t%7=9n={0J7==DG1Y-&+tbQ_!QrUL zj6+3kE^3KZqatzwHPAEYUINE=n2~lr*Kekf8}-1_=!=!m59?tbjCI;|zXmcDHIPME z9#c?9YVLF92g)X>0i8h2_y+ppKiCO9&YR?Qb)}#O^+%0pBax*a28HLEmfxfnGOqKI_kAC1Gd0XBwcsZQl(rr6L{hH6+^WD(_b+oEr5*3DvOFp z6UPpipL!qE%o9+N*ocL3A8Mvguo%9_a#-N1`TL+d7NouzOXKfY2!pP%1ls>qC@5*Z zL3Pv*Lva*pKQBe?|2wFVr@d~nJ`;vgZ;qL82u9;{48+5z2VKEZ=y}6z*RoiKdN*|G z0m~^UboWp*eT6eI`|teKgPTy>wB=1Rz>b)gdVeg62~PV-RL)#;{ERw3!v7%rSQWJt z`TjKLK=D6`ze3%}X=sP3sEnvDwe z9@JVtKt(d>u8CMhOhdgs>cP!XN&2nJxiAEK(l8noqDM~qYt+m?U?=puXMT$5ht;TG zLnU3veKVl?Sc!Tw)RNA_RJanggln-mZbMDj<;B8iEz6-s)WvZS>cJCGNtu8e*c$AL z+fYfD@qzPKF={td!L%5MCvgOJ!Rimq&+zN8F?HWZ?j&|u?I>vGiP#ps9-A3;!P3;{ z<7zyIJ#f$y^JVi-TtvOXQ(i!L9ed)|XJ&U~{o5pGEGiC9? zW%C5oOm<@!reG+hd131LFp_#*Y=Ci&zhG7B{x8kv8{&P6}^Ux698zQxOS@19>=6trEgqSp2f z2BJU5-fBJ&hN-BZ@V4DQBArD)>K^=;CA|yMqXrg&idcTs62_qhx&ni6D~90_%z`PH z30+?(45N^d-$^Rt3{;2bQ6v2e1Mmf^Biq+JAS0^1ChA0t#RWJ3`=bxjRX@W~{Y*mL zzYuk@ZbT;FvQAOROv4pSkAI^g@Y!k4kj{30VaSJ?SvA!CgPith=tX@kDl%J858UTm zKkL*VVhP&cVHzyRdi!esM^n(uD!UsvI#C^TL_O#`r#=}qz(p8}8!!aVptjvZ^ud7i zW*{M`WX*?Ks&=S>_C(zmhwktH@f5Ts(;e5K9`p;A!Sfh{J{fHHQC$V~;GS3*C!i*< z)46^U6^WNv06(M7nJ9kn(O0kam>rLzOKW$>x$qH#sb>nb-QQjdpw_SmD%&fflCio| zujAAkqV8*kBe4r=0=H2|?n~4JLxRk}qftxHD9CoXZ;YiuYu>|g1nPq`Py^hH`oJYr zC~u-3@X%?0iyGip)Id@Pn|d(n^Vv|_G8%_s8&m`?1+)K^6fbGe_K3)6woOgc2b!Tm zI0DttTC9wRP!ICRWV=to0BlZuJZd-nj+#(YeoD)Y-B1VHWK=&}P!T!lqM$XogF5Lx zIX6URvEAQ_E1^Ev+%XtX7m}A{eB^)gB+;#lBl(qA4KA;|$J zLT$ek)Pr85CXhO}8F-LmKFq9rS%HEc)Epx*&beU$YK=FdLVXPD;9XQG3x?ZP3v7p) z=}y$Z4x%1>6*a)es0n;U4b(TnENK+Fw05N_C>tB1>OD}~%Z2J_CaU9=PW#WO96618 z@J-Y}Q&0!dS5zc|BF%k~n3sAf)BxIJD2|L||7&JTXi!H#qaJt^6~b$%ZFt{l_swJ4 zbE57mfQnp2)Bszdw(EDO0WCqTeKKm-Ttnr~f2axj=5?7HBl4P%R77=H9~HVDs0WQk zbu=5*!FF6omLJD5)H_C*`xl{dXdBkWeb^Vh^4sn|R0g58^EOofS6md-@l(_Z=Uc$G z=F@QyYG1D}Xa;x;>r=mlI#3D~GTX5cYRP`XXuOY6n6P2B;oL9~m8A)&17jD~!`rCsS+JCeNE1|~x}XL;4mFU4 zsD3t~CUyafVG8QL%rPbcg)obYhU!j3J5&w~LT#gwsF}_}&3qkdR~$iaynsr!8>o)& zqq6#g(;igX3?L_JU?otyq9*FO9i@`&HU)+F6ShSEGN#@Qwf4iD`ef8V7UOE%jXH?h zmbI;ToQ~tryPRz`#c8M|xs4k515{-H!9o~Np8c_Tjp;**Xc1JDM2*)3=BlX!X3Yy7lR0lrQ zOz88VB2XH26gNfgKzn>oSF2 zGEkt!3^zjQXB%3U#ntM^Ai!I+CBDUP75_ z8*?K&kiY*ZC_B5h(u!rY>(P*iKvL2L-l_h z)z4GZwoFw|Il?~3NO1Q9fd7vUyDl4 zuc!%>ZEUW$Lmkl*F%;Khemsrk@L#9BSQFx}eb|J;d>n(CX{M&8!+KbqdOs|PNvMu* zqXy{F%(gmVIn*{=g8KXgoM`inr@3u?p+3EZ?f#`?T1)d1yNL~HkBVjgFQ+g(*0z>n zwpM1X_M$?378CIaHo%Fk&9=OX2YA3APJK~Zvt-LrFR2}<HSs~M4eSne2VI~LN7DpmiR68URV_`U_0&qpx)+&@30E>@z@uSqjDm; zkJ*lOu{-s?m=CX@4wx^fY~Iz^?C;a4$a(d%-M{-6z^&9f<2nrNZ~o%ihc2CT?s0dg4 zj{UD2o6w*E^g+#R3M#phP!Blk)bF7}_zATuIuCH(8K@3tqjF+7&cr>aoTxp}?K-v=S<$O;8i;^|(~U3(R1Q=myP+a63AO*%VJUt8-$$Vg4X?2z7943F*cp{PJyB~NkIID^ zs1tKFDo0LX9lVQqvEV4PRLvY)qt?75DpK=M{jS2o+W$K#Xl?JIBJdb@;v3Wh*N-+c z{S}p**HKHN-;UK$1ZKmMsDU;@C3RQS8jp6`=c1N$E$U#pitfMv?@`cqzmJ&H=6C-w zW@a&C%>x^uIv$Ri@pM#@twAlt9@OVgqE5yX)cpb9n;A!@APFuL-K-I8=wjP{}sQxxN84^OLBE+{93P zfm*^06A8JpvIJjObVF0r47y`JA~P5@^QDuFTd@G`zoK&C*<{=OKQ75Q#YAEllGWDt z7>avQx$!%u<8%L_c8%vW^Slrj1Cqi7WaC&(EkrLz$Z@Kb0Pa*$rM6C z*<2Zwtvylg<55Yq9`z15fMxM0#^6^}Bw`ksZ_71MYuf;|6zxzG?1x&C=cpWchkemn zO#F59_FHU5ItVq=DOes?;&8l$8c3@|b58U{op_URIj%v?tkx3S{cl4$qOQL}MZkZl z`2!^am5eP>k?XaT{a=#80vf#84rfuxcx9Ox$P-j3J(k<<|MD4)8sKcqh3ioroMw8Yyu%-Wa=^{jX4UqCv?Kk4m;#jvG+dkD?yE2A zWew`SC-^;v?lJeR+Qa_W1Cwdc7l@143-6#F^vzx~gI1{f`k)^011jkjp(1q@HGvzb z6Z8dYfN6g=M|BodglnSuX^!frzl%Z`g>k4*twz1w_MkethFZ(ls1K&!XNeW#9 z?L-~X=W!NZMfKZ#zi~2Zi8i7Jd;ph_bFTkU(9Gk0F*l4sJ!lRpTQ{MupGGak@2JT9 zgPNiBt4TtC)b&cJ2gIU6-WN5~Rah01u?l{`ntK12KV&)>hh@016Lr!(M`e5DVYAIj zp(4})wMzyd9aukNVZ4Q!q5l!{J76ekm$XME?>JPy%TN)!itg|KuPA7wp-0W@wFOqx z2e1|%Mm;#~G1FlVTtdAnp1|9v2(3GA4z4|@{?20sOnt(%S3|uuTcfVeLHGOrB!y2j zJV%A}=}B|oyhqhTPnj7PMs3$-s1vdkhTAM1>ZLUAwE6jM6KddhurfxSG4G6CsG~g* z^Wq=q%0(g7S@VZM1iIG_HRHahBpQPn@k(rp*HM3h75vS7;b?>k@fg%V5>d&x6HDMe z#}}x6!q1r{ZFP?QuQmRjhBP=A70Sh^_Ki;ae(X&BDC+f^_q;hkilGKt7W-m-$9?f&O^Q*jyfm-rZGT{jVmzhOTA2(>h>h~Lc-8H;&nNJKs0 z2r5)xu?SYYX@2wVhtbs6Vs88cl?z|7Acp^8Y=p7YM`1g>ih3DE|7ji^j}x{3w@_Hh zg;IZ+2VOy)V8ORc$E#6Ea|(yxSJb{Aa@!oi2e26RhgcM|-Z9x<7xj9cgE}wvp_bqd zHpKLI_5FbNZwCrdx)JsE+K>4$%{>!|;;5|ehno3e)V_X;dSJ=>W?OZ`KGfIXFbqmD zk(-8!(0<3;sFzZz2mFZ5^R3zx=A+j`+x^ew64B!S8$2?hYWUc6G!C^Lldw8oL7jZr zo|qZd!P36j(Q|2(mCIlgRS-(_J2AW zTF{V(2Xsa4`>Fq$mro+<0Y9U%^*HK)`hdJ)t@Qty?R)C2$(?hkfuwt99*_fde*x6e zR>rb86}1hIzjK)r>pBhE=PxlMW_fSkT7^+Fu8PXuGpGRud@$E@qt1a+s3W-!YFla{Y-y;^Fw%s=L!M{)^<|8CoUDjI)dQj1?W~3ERH`YKspdseO?oRy&)Pt6y z4ybLYkiS4BS3%3e9r6}fjQSu{R7e}4-gaG4FPYw`2*x?}A*cwBz<8XEN>V>h2B7_)oq`@%A2rh+ zs5P62n(0hb(yc&^dB5r=EmLx^t+P);-h!)A|?-qdwOJ z6`{Vk1gBsw%$L?&ubtNA;XYux&@ht={c#R{MXlK!U-O`KsHHgIcpf#tJE)m{#EIzZ z=iyG;xv1+K9gm>~bPMy_9AH?5dZf$WBvE%PMZ*Yuj61L~CZ_jrpY_kNHuVx2JlrSQ zNYvV%KqcX2)Ic7gvi%)a!}I}WB27_A+!G^l0;WaRMhc-6cB4Xk4Heo{f!ZFtPB8-O zqCU_cbzeN{WLt|m!jGX2mP@FRKSK@7C&=8F1C=}FQ1{nGBJ8qaorZp>2aiO3a5k#L z)u@plMCHiusQWyEO-M7N22vdBVjY`I4 zsO&t9TEiu!7X9+QZNj)~)3`shDyk6>rroY+&Ne#~SOr3f_*Pi{l zCzZUEm@diVPLx+tqlXtV&+lvJ*ww?H6_A`d&>kF+@IKt0lDsRze&UhvJg7X=&Q)__h!IvrViWL5v\n" "Language-Team: pt_BR \n" @@ -209,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 @@ -1815,6 +1817,320 @@ msgid "" msgstr "" "Recomendado mas modelo não convertido. Use 'Diagnóstico' para converter." +# | 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 "" +"✅ 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)" +msgstr "" +"✅ 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)" + +# | msgid "Zones Completed" +#: src/zebtrack/core/recording/live_analysis_post_processor.py:644 +msgid "Analysis Completed" +msgstr "Análise Concluída" + +#: src/zebtrack/core/recording/live_analysis_post_processor.py:647 +#, python-brace-format +msgid "" +"⚠️ 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}" +msgstr "" +"⚠️ Gravação concluída, mas nenhuma detecção foi encontrada.\n" +"\n" +"Possí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}" + +#: src/zebtrack/core/recording/live_analysis_post_processor.py:654 +msgid "Analysis Completed - No Detections" +msgstr "Análise Concluída - Sem Detecções" + +#: src/zebtrack/core/recording/live_analysis_post_processor.py:657 +#, python-brace-format +msgid "" +"⚠️ 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." +msgstr "" +"⚠️ Gravação concluída, mas a análise automática falhou.\n" +"\n" +"📁 Dados brutos salvos em:\n" +"{output_dir}\n" +"\n" +"Você pode analisar manualmente pela interface." + +# | msgid "Recording Pending" +#: src/zebtrack/core/recording/live_analysis_post_processor.py:661 +msgid "Recording Completed" +msgstr "Gravação Concluída" + +# | msgid "" "Excellent system! Supports 4+ aquariums simultaneously at high " +# | "performance." +#: src/zebtrack/core/recording/live_camera_mode.py:132 +#, python-brace-format +msgid "" +"The system supports {count} aquariums simultaneously. Real-time processing " +"enabled." +msgstr "" +"Sistema suporta {count} aquários simultaneamente. Processamento em tempo " +"real habilitado." + +#: src/zebtrack/core/recording/live_camera_mode.py:140 +msgid "Record without detection (better quality, process later)" +msgstr "Gravar sem detecção (melhor qualidade, processar depois)" + +#: src/zebtrack/core/recording/live_camera_mode.py:167 +#, python-brace-format +msgid "" +"The system supports only {supported} of the {requested} aquariums requested " +"for simultaneous processing. " +msgstr "" +"O sistema suporta apenas {supported} dos {requested} aquários solicitados " +"para processamento simultâneo. " + +#: src/zebtrack/core/recording/live_camera_mode.py:172 +#, python-brace-format +msgid "Recommended: record {count} separate sessions." +msgstr "Recomendado: gravar {count} sessões separadas." + +#: src/zebtrack/core/recording/live_camera_mode.py:176 +msgid "Recommended: process only 1 aquarium in this session." +msgstr "Recomendado: processar apenas 1 aquário nesta sessão." + +#: src/zebtrack/core/recording/live_camera_mode.py:183 +msgid "Process only 1 aquarium now (ignore the rest)" +msgstr "Processar apenas 1 aquário agora (ignorar demais)" + +#: src/zebtrack/core/recording/live_camera_mode.py:190 +#, python-brace-format +msgid "Split into {count} separate sessions" +msgstr "Dividir em {count} sessões separadas" + +#: src/zebtrack/core/recording/live_camera_mode.py:199 +msgid "Record without detection (process offline later)" +msgstr "Gravar sem detecção (processar offline depois)" + +# | msgid "System supports up to 2 aquariums simultaneously." +#: src/zebtrack/core/recording/live_camera_mode.py:204 +#, python-brace-format +msgid "⚠️ The system does not support {count} aquariums simultaneously." +msgstr "⚠️ Sistema não suporta {count} aquários simultaneamente." + +#: src/zebtrack/core/recording/live_camera_mode.py:210 +msgid "Maximum supported: 1 aquarium." +msgstr "Máximo suportado: 1 aquário." + +# | msgid "Aquarium" +# | msgid "Next Aquarium" +# | msgid "{name} ({count} aquariums)" +#: src/zebtrack/core/recording/live_camera_mode.py:212 +#, python-brace-format +msgid "Maximum supported: {count} aquariums." +msgstr "Máximo suportado: {count} aquários." + +# | msgid "" "System insufficient for real-time processing. Recommended: +# record +# | video " "only and process offline." +#: src/zebtrack/core/recording/live_camera_mode.py:230 +msgid "" +"The system is not sufficient for real-time processing. Recommended: record " +"the video and process it offline." +msgstr "" +"Sistema insuficiente para processamento em tempo real. Recomendado: gravar " +"vídeo e processar offline." + +#: src/zebtrack/core/recording/live_camera_mode.py:235 +msgid "⚠️ INSUFFICIENT HARDWARE for real-time detection." +msgstr "⚠️ HARDWARE INSUFICIENTE para detecção em tempo real." + +#: src/zebtrack/core/recording/live_camera_mode.py:236 +#, python-brace-format +msgid "CPU: {cores} cores (minimum 2)" +msgstr "CPU: {cores} cores (mínimo 2)" + +#: src/zebtrack/core/recording/live_camera_mode.py:237 +#, python-brace-format +msgid "RAM: {gb:.1f}GB available (minimum 4GB)" +msgstr "RAM: {gb:.1f}GB disponível (mínimo 4GB)" + +#: src/zebtrack/core/recording/live_camera_mode.py:246 +msgid "Record video without detection (only viable option)" +msgstr "Gravar vídeo sem detecção (única opção viável)" + +# | msgid "Delete {asset}" +# | msgid "Delete Selected" +#: src/zebtrack/core/recording/live_camera_mode.py:266 +msgid "Default mode selected." +msgstr "Modo padrão selecionado." + +#: src/zebtrack/core/recording/live_camera_mode.py:292 +#, python-brace-format +msgid "Session {index} of {total} (individual aquarium)" +msgstr "Sessão {index} de {total} (aquário individual)" + +#: src/zebtrack/core/recording/live_camera_mode.py:320 +msgid "Process multiple aquariums simultaneously in real time" +msgstr "Processar múltiplos aquários simultaneamente em tempo real" + +# | msgid "Analysing frame {current}/{total}..." +# | msgid "Processing aquarium {current}/{total}..." +#: src/zebtrack/core/recording/live_camera_mode.py:322 +msgid "Process one aquarium in real time" +msgstr "Processar um aquário em tempo real" + +#: src/zebtrack/core/recording/live_camera_mode.py:323 +msgid "Record video without detection (process offline later)" +msgstr "Gravar vídeo sem detecção (processar offline depois)" + +#: src/zebtrack/core/recording/live_camera_mode.py:324 +msgid "Record multiple sessions, one per aquarium" +msgstr "Gravar múltiplas sessões, uma por aquário" + +# | msgid "unknown" +#: src/zebtrack/core/recording/live_camera_mode.py:326 +msgid "Unknown mode" +msgstr "Modo desconhecido" + +#: src/zebtrack/core/recording/live_session_manager.py:333 +msgid "⏳ Warming up camera..." +msgstr "⏳ Aquecendo câmera..." + +#: src/zebtrack/core/recording/live_session_manager.py:341 +#, python-brace-format +msgid "" +"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" +msgstr "" +"Falha ao abrir câmera {index}.\n" +"\n" +"Possíveis causas:\n" +"• Câmera está em uso por outro programa\n" +"• Hardware com defeito\n" +"• Driver incompatível\n" +"\n" +"Tente:\n" +"• Fechar outros programas de câmera\n" +"• Reconectar o dispositivo USB\n" +"• Selecionar outra câmera" + +# | msgid "Error" +# | msgid "Model Error" +#: src/zebtrack/core/recording/live_session_manager.py:353 +#: src/zebtrack/ui/components/project_initializer.py:396 +msgid "Camera Error" +msgstr "Erro na Câmera" + +# | msgid "Select an active aquarium first." +# | msgid "🐟 Active Aquarium" +# | msgid "Draw the Aquarium" +#: src/zebtrack/core/recording/live_session_manager.py:690 +msgid "🔍 Looking for the aquarium..." +msgstr "🔍 Procurando aquário..." + +#: src/zebtrack/core/recording/live_session_manager.py:694 +msgid "⏳ Waiting for video..." +msgstr "⏳ Aguardando vídeo..." + +#: src/zebtrack/core/recording/live_session_manager.py:1120 +msgid "" +"⚠️ Video: 1 frame dropped — check the disk / OneDrive (the recording may " +"have gaps)" +msgstr "" +"⚠️ Vídeo: 1 frame descartado — verifique o disco / OneDrive (gravação pode " +"ter falhas)" + +#: src/zebtrack/core/recording/live_session_manager.py:1123 +#, python-brace-format +msgid "" +"⚠️ Video: {count} frames dropped — check the disk / OneDrive (the recording " +"may have gaps)" +msgstr "" +"⚠️ Vídeo: {count} frames descartados — verifique o disco / OneDrive " +"(gravação pode ter falhas)" + +#: src/zebtrack/core/recording/live_session_manager.py:1148 +#, python-brace-format +msgid "⏳ Analysing... ({seconds:.1f}s behind) - Recording OK" +msgstr "⏳ Analisando... ({seconds:.1f}s atrás) - Gravação OK" + +#: src/zebtrack/core/recording/live_session_manager.py:1153 +#, python-brace-format +msgid "⏳ Analysis is behind ({seconds:.1f}s) - Recording continues normally" +msgstr "⏳ Análise atrasada ({seconds:.1f}s) - Gravação continua normalmente" + +#: src/zebtrack/core/recording/live_session_manager.py:1157 +#, python-brace-format +msgid "" +"⚠️ Analysis is far behind ({seconds:.1f}s) - Recording OK, analysis queued" +msgstr "" +"⚠️ Análise muito atrasada ({seconds:.1f}s) - Gravação OK, análise em fila" + +#: src/zebtrack/core/recording/recording_service.py:155 +msgid "Camera configuration unavailable to start the recording." +msgstr "Configuração da câmera indisponível para iniciar a gravação." + +# | msgid "Could not save language" +# | msgid "Could not save the zone." +#: src/zebtrack/core/recording/recording_service.py:207 +msgid "Could not start the recording." +msgstr "Não foi possível iniciar a gravação." + #: src/zebtrack/io/arduino_manager.py:140 #, python-brace-format msgid "Could not connect to the Arduino on port {port}." @@ -1834,8 +2150,8 @@ msgid "Serial connection to the Arduino was lost." msgstr "Conexão serial com Arduino perdida." # | msgid "unknown" +# | msgid "Unknown error" #: src/zebtrack/ui/gui.py:145 -#| msgid "Unknown error" msgid "An unknown error occurred." msgstr "Ocorreu um erro desconhecido." @@ -1980,22 +2296,22 @@ msgstr "Primeira execução — otimizando para seu hardware..." msgid "Camera reconnected (gap: {seconds:.1f}s)" msgstr "Câmera reconectada (gap: {seconds:.1f}s)" -#: src/zebtrack/ui/ui_coordinator.py:825 +#: src/zebtrack/ui/ui_coordinator.py:823 #, python-brace-format msgid "Detecting aquarium: frame {frame}/100" msgstr "Detectando aquário: frame {frame}/100" # | msgid "Remove the trajectory generated for this video?" -#: src/zebtrack/ui/ui_coordinator.py:879 +# | msgid "Reports generated for {count} videos." +#: src/zebtrack/ui/ui_coordinator.py:877 #, python-brace-format -#| msgid "Reports generated for {count} videos." msgid "Batch report generated ({count} session): {batch_id}" msgstr "Relatório de lote gerado ({count} sessão): {batch_id}" # | msgid "Remove the trajectory generated for this video?" -#: src/zebtrack/ui/ui_coordinator.py:881 +# | msgid "Reports generated for {count} videos." +#: src/zebtrack/ui/ui_coordinator.py:879 #, python-brace-format -#| msgid "Reports generated for {count} videos." msgid "Batch report generated ({count} sessions): {batch_id}" msgstr "Relatório de lote gerado ({count} sessões): {batch_id}" @@ -5500,12 +5816,6 @@ msgstr "" "\n" "As gravações irão falhar até você selecionar outra câmera. Use 'Trocar câmera...' no bloco do animal antes de iniciar a sessão para escolher um dispositivo conectado." -# | msgid "Error" -# | msgid "Model Error" -#: src/zebtrack/ui/components/project_initializer.py:396 -msgid "Camera Error" -msgstr "Erro na Câmera" - # | msgid "Error" # | msgid "Model Error" # | msgid "Import error" diff --git a/src/zebtrack/locales/zebtrack.pot b/src/zebtrack/locales/zebtrack.pot index 4f52ee5a..d7a51bd1 100644 --- a/src/zebtrack/locales/zebtrack.pot +++ b/src/zebtrack/locales/zebtrack.pot @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: DRerio LogAI VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-08-13 13:24-0300\n" +"POT-Creation-Date: 2026-08-13 13:37-0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -174,6 +174,8 @@ msgstr "" #: 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 @@ -1450,6 +1452,243 @@ msgstr "" msgid "Recommended, but the model is not converted. Use 'Diagnostics' to convert." msgstr "" +#: src/zebtrack/core/recording/frame_processing_pipeline.py:684 +#, python-brace-format +msgid "🔍 Detecting aquarium... ({current}/{total})" +msgstr "" + +#: src/zebtrack/core/recording/live_analysis_post_processor.py:628 +#, python-brace-format +msgid "" +"✅ 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)" +msgstr "" + +#: src/zebtrack/core/recording/live_analysis_post_processor.py:644 +msgid "Analysis Completed" +msgstr "" + +#: src/zebtrack/core/recording/live_analysis_post_processor.py:647 +#, python-brace-format +msgid "" +"⚠️ 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}" +msgstr "" + +#: src/zebtrack/core/recording/live_analysis_post_processor.py:654 +msgid "Analysis Completed - No Detections" +msgstr "" + +#: src/zebtrack/core/recording/live_analysis_post_processor.py:657 +#, python-brace-format +msgid "" +"⚠️ 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." +msgstr "" + +#: src/zebtrack/core/recording/live_analysis_post_processor.py:661 +msgid "Recording Completed" +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:132 +#, python-brace-format +msgid "" +"The system supports {count} aquariums simultaneously. Real-time " +"processing enabled." +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:140 +msgid "Record without detection (better quality, process later)" +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:167 +#, python-brace-format +msgid "" +"The system supports only {supported} of the {requested} aquariums " +"requested for simultaneous processing. " +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:172 +#, python-brace-format +msgid "Recommended: record {count} separate sessions." +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:176 +msgid "Recommended: process only 1 aquarium in this session." +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:183 +msgid "Process only 1 aquarium now (ignore the rest)" +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:190 +#, python-brace-format +msgid "Split into {count} separate sessions" +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:199 +msgid "Record without detection (process offline later)" +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:204 +#, python-brace-format +msgid "⚠️ The system does not support {count} aquariums simultaneously." +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:210 +msgid "Maximum supported: 1 aquarium." +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:212 +#, python-brace-format +msgid "Maximum supported: {count} aquariums." +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:230 +msgid "" +"The system is not sufficient for real-time processing. Recommended: " +"record the video and process it offline." +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:235 +msgid "⚠️ INSUFFICIENT HARDWARE for real-time detection." +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:236 +#, python-brace-format +msgid "CPU: {cores} cores (minimum 2)" +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:237 +#, python-brace-format +msgid "RAM: {gb:.1f}GB available (minimum 4GB)" +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:246 +msgid "Record video without detection (only viable option)" +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:266 +msgid "Default mode selected." +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:292 +#, python-brace-format +msgid "Session {index} of {total} (individual aquarium)" +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:320 +msgid "Process multiple aquariums simultaneously in real time" +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:322 +msgid "Process one aquarium in real time" +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:323 +msgid "Record video without detection (process offline later)" +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:324 +msgid "Record multiple sessions, one per aquarium" +msgstr "" + +#: src/zebtrack/core/recording/live_camera_mode.py:326 +msgid "Unknown mode" +msgstr "" + +#: src/zebtrack/core/recording/live_session_manager.py:333 +msgid "⏳ Warming up camera..." +msgstr "" + +#: src/zebtrack/core/recording/live_session_manager.py:341 +#, python-brace-format +msgid "" +"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" +msgstr "" + +#: src/zebtrack/core/recording/live_session_manager.py:353 +#: src/zebtrack/ui/components/project_initializer.py:396 +msgid "Camera Error" +msgstr "" + +#: src/zebtrack/core/recording/live_session_manager.py:690 +msgid "🔍 Looking for the aquarium..." +msgstr "" + +#: src/zebtrack/core/recording/live_session_manager.py:694 +msgid "⏳ Waiting for video..." +msgstr "" + +#: src/zebtrack/core/recording/live_session_manager.py:1120 +msgid "" +"⚠️ Video: 1 frame dropped — check the disk / OneDrive (the recording may " +"have gaps)" +msgstr "" + +#: src/zebtrack/core/recording/live_session_manager.py:1123 +#, python-brace-format +msgid "" +"⚠️ Video: {count} frames dropped — check the disk / OneDrive (the " +"recording may have gaps)" +msgstr "" + +#: src/zebtrack/core/recording/live_session_manager.py:1148 +#, python-brace-format +msgid "⏳ Analysing... ({seconds:.1f}s behind) - Recording OK" +msgstr "" + +#: src/zebtrack/core/recording/live_session_manager.py:1153 +#, python-brace-format +msgid "⏳ Analysis is behind ({seconds:.1f}s) - Recording continues normally" +msgstr "" + +#: src/zebtrack/core/recording/live_session_manager.py:1157 +#, python-brace-format +msgid "⚠️ Analysis is far behind ({seconds:.1f}s) - Recording OK, analysis queued" +msgstr "" + +#: src/zebtrack/core/recording/recording_service.py:155 +msgid "Camera configuration unavailable to start the recording." +msgstr "" + +#: src/zebtrack/core/recording/recording_service.py:207 +msgid "Could not start the recording." +msgstr "" + #: src/zebtrack/io/arduino_manager.py:140 #, python-brace-format msgid "Could not connect to the Arduino on port {port}." @@ -1592,17 +1831,17 @@ msgstr "" msgid "Camera reconnected (gap: {seconds:.1f}s)" msgstr "" -#: src/zebtrack/ui/ui_coordinator.py:825 +#: src/zebtrack/ui/ui_coordinator.py:823 #, python-brace-format msgid "Detecting aquarium: frame {frame}/100" msgstr "" -#: src/zebtrack/ui/ui_coordinator.py:879 +#: src/zebtrack/ui/ui_coordinator.py:877 #, python-brace-format msgid "Batch report generated ({count} session): {batch_id}" msgstr "" -#: src/zebtrack/ui/ui_coordinator.py:881 +#: src/zebtrack/ui/ui_coordinator.py:879 #, python-brace-format msgid "Batch report generated ({count} sessions): {batch_id}" msgstr "" @@ -4526,10 +4765,6 @@ msgid "" "connected device." msgstr "" -#: src/zebtrack/ui/components/project_initializer.py:396 -msgid "Camera Error" -msgstr "" - #: src/zebtrack/ui/components/project_initializer.py:507 msgid "Wizard Error" msgstr "" diff --git a/tests/core/test_live_camera_phase5_hygiene.py b/tests/core/test_live_camera_phase5_hygiene.py index c0e0c267..c5127fc8 100644 --- a/tests/core/test_live_camera_phase5_hygiene.py +++ b/tests/core/test_live_camera_phase5_hygiene.py @@ -98,7 +98,7 @@ def test_publish_video_drop_status_emits_status_event( # carries the drop count and a hint about the disk. payload = event.data assert "7" in payload.message - assert "verifique" in payload.message.lower() + assert "check the disk" in payload.message.lower() def test_publish_video_drop_status_silent_without_event_bus( diff --git a/tests/i18n/test_no_untranslated_literals.py b/tests/i18n/test_no_untranslated_literals.py index be953036..1cdf8dd9 100644 --- a/tests/i18n/test_no_untranslated_literals.py +++ b/tests/i18n/test_no_untranslated_literals.py @@ -32,6 +32,7 @@ "src/zebtrack/coordinators", "src/zebtrack/core/app_runner.py", "src/zebtrack/core/application_bootstrapper.py", + "src/zebtrack/core/recording", "src/zebtrack/i18n.py", "src/zebtrack/io", "src/zebtrack/plugins", diff --git a/tests/test_live_camera_workflow_e2e.py b/tests/test_live_camera_workflow_e2e.py index 68a12f11..d3fc2050 100644 --- a/tests/test_live_camera_workflow_e2e.py +++ b/tests/test_live_camera_workflow_e2e.py @@ -153,7 +153,7 @@ def test_mode_selection_no_realtime(self, settings_obj): assert recommendation.recommended_mode == LiveCameraMode.RECORD_ONLY assert recommendation.can_process_realtime is False - assert any("HARDWARE INSUFICIENTE" in w for w in recommendation.warnings) + assert any("INSUFFICIENT HARDWARE" in w for w in recommendation.warnings) class TestRecorderPauseResume: From d752519aad1afb9e7f9ba7d0dbdfbc8b284666ad Mon Sep 17 00:00:00 2001 From: MarkSant Date: Thu, 13 Aug 2026 14:37:01 -0300 Subject: [PATCH 04/21] feat(i18n): traduzir core/project/** (fase 3, lote 3b) `core/project` a zero e no ratchet: 72 literais acentuados em 7 arquivos, mais uma duzia sem acento que o scanner nao ve. ACHADO PRINCIPAL -- o guia pos-criacao de projeto apontava para botoes que nao existem. `project_workflow_service._generate_post_creation_guide` instrui o pesquisador citando nomes de aba e de botao ENTRE ASPAS, retipados a mao. As fases 1 e 2 traduziram esses widgets; o guia nao acompanhou. Dois dos seis ja estavam errados ANTES desta migracao: - "aba 'Relatorios'" -> a aba se chama "Processamento e Relatorios" - "Clique em 'Adicionar e Processar Novos Videos'" -> esse rotulo nao existe em lugar nenhum do codigo; o botao e "Processar Videos Pendentes..." Agora cada rotulo citado e interpolado a partir do msgid DO PROPRIO WIDGET (`_(" - Open the '{tab}' tab").format(tab=_("Main Control"))`), entao o guia nao pode mais divergir em silencio. Os seis msgids ja existiam das fases 1-2 e foram reusados. UM `_` COMO DESCARTE de novo, o terceiro da fase: `asset_manager.py:434` (`_, entry = self._resolve_roi_template_entry(...)`). Desta vez o ruff NAO pegou -- so o mypy, com `"int" not callable` / `"None" not callable`. Aproveitei para eliminar os outros seis descartes latentes de `core/project` (zone_manager, project_manager): hoje estao em funcoes sem `_()`, mas sao armadilhas armadas para quem adicionar a primeira string traduzida. Outros defeitos: - `can_remove_asset` montava "Nao ha {label} registrada para este video." com o substantivo interpolado. O portugues concorda em genero com a palavra encaixada ("registrada"), coisa que nenhuma substituicao garante em traducao. Virou uma frase COMPLETA por asset. - `project_lifecycle_manager` gravava `analysis_profiles[0].name = "Padrao"` no project.json, enquanto o factory canonico `AssetManager._default_analysis_profile()` ja gravava `"default"`. Dois nomes para o mesmo perfil embutido. Alinhado ao canonico e mantido em INGLES: e valor PERSISTIDO, e um nome que mudasse com `ui.language` daria projetos diferentes por maquina. `"active_weight": _("Default")` (project_workflow_ service) e o oposto -- valor de EXIBICAO empurrado ao StateManager -- e leva `_()`. - Guardas que nomeiam parametros de codigo (`num_aquariums deve ser <= 100`, `save_location invalido`, `Asset type '{asset}' desconhecido`) e as `Field(description=...)` do Pydantic em `schemas.py` (avaliadas no corpo da classe, onde `_()` e proibido) passaram a INGLES FIXO, sem `_()`: sao documentacao de esquema e guardas de desenvolvedor, nao copy de interface. Asserts migrados em 7 arquivos de teste. Um foi DELIBERADAMENTE mantido em portugues: `test_detection_enforcement.py:140` casa contra `core/viewmodels/analysis_control_view_model.py`, que e do lote 3d. Esse arquivo tem uma SEGUNDA redacao da mesma validacao det/seg -- quando 3d migra-lo, deve reusar o msgid deste lote em vez de criar outro. Verde: ruff limpo, mypy limpo em 698 arquivos, 4213 fast, 1079 gui. Co-Authored-By: Claude Opus 5 --- docs/tasks/active/ROLLING_TASK_LOG.md | 5 +- src/zebtrack/core/project/asset_manager.py | 67 ++- .../core/project/project_lifecycle_manager.py | 81 ++- src/zebtrack/core/project/project_manager.py | 9 +- .../core/project/project_workflow_service.py | 79 +-- .../core/project/roi_template_manager.py | 36 +- src/zebtrack/core/project/schemas.py | 15 +- src/zebtrack/core/project/zone_manager.py | 11 +- .../locales/_pairs/pr3-core-project.json | 72 +++ .../locales/pt_BR/LC_MESSAGES/zebtrack.mo | Bin 144059 -> 152155 bytes .../locales/pt_BR/LC_MESSAGES/zebtrack.po | 542 ++++++++++++++++-- src/zebtrack/locales/zebtrack.pot | 423 ++++++++++++-- tests/core/test_asset_manager_basic.py | 4 +- tests/core/test_project_workflow_service.py | 14 +- .../test_roi_template_manager_validation.py | 8 +- tests/core/test_schemas.py | 6 +- tests/i18n/test_no_untranslated_literals.py | 1 + tests/test_detection_enforcement.py | 6 +- tests/test_project_manager.py | 4 +- 19 files changed, 1153 insertions(+), 230 deletions(-) create mode 100644 src/zebtrack/locales/_pairs/pr3-core-project.json diff --git a/docs/tasks/active/ROLLING_TASK_LOG.md b/docs/tasks/active/ROLLING_TASK_LOG.md index 139fc48d..4cb7279d 100644 --- a/docs/tasks/active/ROLLING_TASK_LOG.md +++ b/docs/tasks/active/ROLLING_TASK_LOG.md @@ -41,7 +41,10 @@ no topo do ratchet; depois, atualizar a seção "A migração" em - [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. -- [ ] Lote 3b: `core/project/**` (72 literais em 7 arquivos). +- [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. - [ ] Lote 3c: `core/services/**` (61 em 7 arquivos). - [ ] Lote 3d: `core/video/**`, `core/detection/**`, `core/viewmodels/**` (30 em 10 arquivos), incluindo simplificar o consumidor de `progress_notifier` 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/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/pt_BR/LC_MESSAGES/zebtrack.mo b/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.mo index 98cf8a0af1cab1300ea9fb75a36c1a6f5a995776..24a2293d3e247b3df9b8ba50ff4820f887175014 100644 GIT binary patch delta 28758 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 delta 21188 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 Oxu>FE`roZQgZ~ed+q-`N diff --git a/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.po b/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.po index a09189fc..8d208dcc 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-13 13:37-0300\n" +"POT-Creation-Date: 2026-08-13 14:19-0300\n" "PO-Revision-Date: 2026-08-08 21:41-0300\n" "Last-Translator: FULL NAME \n" "Language-Team: pt_BR \n" @@ -1817,6 +1817,479 @@ msgid "" msgstr "" "Recomendado mas modelo não convertido. Use 'Diagnóstico' para converter." +# | msgid "Select the Video for Diagnostics" +# | msgid "📹 Select Video for Drawing" +# | msgid "Select a video before applying the template." +#: src/zebtrack/core/project/asset_manager.py:108 +#| msgid "Select a video in the list before applying the template." +msgid "Project not initialised for saving ROI templates." +msgstr "Projeto não inicializado para salvar templates de ROI." + +#: src/zebtrack/core/project/asset_manager.py:233 +#: src/zebtrack/core/project/roi_template_manager.py:103 +#| msgid "The mask sidecar is empty." +msgid "The template name cannot be empty." +msgstr "O nome do template não pode ficar vazio." + +# | msgid "Select the Video for Diagnostics" +# | msgid "📹 Select Video for Drawing" +# | msgid "Select a video before applying the template." +#: src/zebtrack/core/project/asset_manager.py:236 +#| msgid "Error applying the template" +msgid "Invalid zone data for saving the template." +msgstr "Dados de zona inválidos para salvar o template." + +#: src/zebtrack/core/project/asset_manager.py:239 +#: 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/core/project/asset_manager.py:247 +#: src/zebtrack/core/project/project_manager.py:138 +msgid "Cannot save the template into the current project: no project loaded." +msgstr "" +"Não é possível salvar o template no projeto atual: projeto não carregado." + +#: src/zebtrack/core/project/asset_manager.py:261 +#, python-brace-format +#| msgid "Template '{name}' applied to the video being edited." +msgid "Template '{name}' already exists." +msgstr "Template '{name}' já existe." + +#: src/zebtrack/core/project/asset_manager.py:381 +msgid "Invalid template file: the 'data' block is missing." +msgstr "Arquivo de template inválido: bloco 'data' ausente." + +# | msgid "The selected video could not be located in the project." +# | msgid "Every selected video is already registered in the project." +#: src/zebtrack/core/project/asset_manager.py:439 +#| msgid "No video is currently registered in the project" +msgid "The template file is not registered in the project." +msgstr "Arquivo do template não registrado no projeto." + +# | msgid "Invalid selection" +# | msgid "Invalid target" +#: src/zebtrack/core/project/asset_manager.py:453 +#: src/zebtrack/core/project/asset_manager.py:487 +#| msgid "Invalid template" +msgid "Invalid template content." +msgstr "Conteúdo do template inválido." + +# | msgid "Video not found" +#: src/zebtrack/core/project/asset_manager.py:459 +#, python-brace-format +#| msgid "ROI '{name}' not found." +msgid "ROI template '{name}' not found in the project." +msgstr "Template de ROI '{name}' não encontrado no projeto." + +#: src/zebtrack/core/project/asset_manager.py:474 +#, 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/core/project/asset_manager.py:726 +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." + +#: src/zebtrack/core/project/asset_manager.py:736 +#| msgid "Remove the trajectory generated for this video?" +msgid "There is no arena recorded for this video." +msgstr "Não há arena registrada para este vídeo." + +#: src/zebtrack/core/project/asset_manager.py:737 +#| msgid "Remove every ROI saved for this video?" +msgid "There are no ROIs recorded for this video." +msgstr "Não há ROIs registradas para este vídeo." + +#: src/zebtrack/core/project/asset_manager.py:738 +#| msgid "Remove the trajectory generated for this video?" +msgid "There is no trajectory recorded for this video." +msgstr "Não há trajetória registrada para este vídeo." + +#: src/zebtrack/core/project/asset_manager.py:741 +#, python-brace-format +msgid "There is no {asset} recorded for this video." +msgstr "Não há {asset} registrado para este vídeo." + +#: src/zebtrack/core/project/asset_manager.py:745 +#| msgid "There were no unified reports to delete." +msgid "There are no reports or summaries to remove." +msgstr "Não há relatórios ou sumários para remover." + +#: src/zebtrack/core/project/asset_manager.py:750 +#| msgid "Remove the reports and summaries associated with this video?" +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/core/project/asset_manager.py:759 +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/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/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/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/core/project/project_lifecycle_manager.py:133 +#, python-brace-format +msgid "" +"Permission denied while saving the project: {path}\n" +"\n" +"Check that you have write permission on the folder.\n" +"\n" +"Error: {error}" +msgstr "" +"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/core/project/project_lifecycle_manager.py:143 +#, python-brace-format +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/core/project/project_lifecycle_manager.py:153 +#, python-brace-format +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/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" +"Error: {error}" +msgstr "" +"Erro inesperado ao salvar o projeto: {path}\n" +"\n" +"Por favor, verifique as permissões da pasta.\n" +"\n" +"Erro: {error}" + +#: 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." + +# | msgid "Remove video from project" +#: src/zebtrack/core/project/project_manager.py:829 +#| msgid "No video found in the project" +msgid "Video not found in the project." +msgstr "Vídeo não encontrado no projeto." + +#: src/zebtrack/core/project/project_workflow_service.py:129 +#, python-brace-format +msgid "" +"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 "" +"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/core/project/project_workflow_service.py:1184 +#| msgid "Default?" +msgid "Default" +msgstr "Padrão" + +#: src/zebtrack/core/project/project_workflow_service.py:1328 +#| msgid "Zone saved successfully." +msgid "🎉 Project created successfully!" +msgstr "🎉 Projeto criado com sucesso!" + +#: src/zebtrack/core/project/project_workflow_service.py:1330 +#, python-brace-format +#| msgid "OpenVINO: {status}" +msgid "📊 Video status:" +msgstr "📊 Status dos vídeos:" + +# | msgid "Failure: {error}" +#: src/zebtrack/core/project/project_workflow_service.py:1331 +#, python-brace-format +#| msgid "❌ Failures: {count}" +msgid " • Total videos: {count}" +msgstr " • Total de vídeos: {count}" + +# | msgid "- Frames analysed: {count}" +#: src/zebtrack/core/project/project_workflow_service.py:1332 +#, python-brace-format +#| msgid "" +#| "\" \"\n" +#| "\" \"✅ Finished: {count}" +msgid " • With arena defined: {count}" +msgstr " • Com arena definida: {count}" + +# | msgid "- Frames analysed: {count}" +#: src/zebtrack/core/project/project_workflow_service.py:1333 +#, python-brace-format +#| msgid "" +#| "\" \"\n" +#| "\" \"✅ Finished: {count}" +msgid " • With ROIs defined: {count}" +msgstr " • Com ROIs definidas: {count}" + +#: src/zebtrack/core/project/project_workflow_service.py:1334 +#, python-brace-format +msgid " • With trajectory ready: {count}" +msgstr " • Com trajetória pronta: {count}" + +# | msgid "Preparing video processing..." +#: src/zebtrack/core/project/project_workflow_service.py:1335 +#, python-brace-format +#| msgid "Recording and Processing Controls" +msgid " • Pending processing: {count}" +msgstr " • Pendentes de processamento: {count}" + +#: src/zebtrack/core/project/project_workflow_service.py:1337 +msgid "🚀 Recommended next steps:" +msgstr "🚀 Próximos passos recomendados:" + +#: 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/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}'" + +# | msgid "Global Model Configuration..." +# | msgid "Model Configuration" +#: src/zebtrack/core/project/project_workflow_service.py:1344 +#: src/zebtrack/ui/components/tab_builder.py:134 +msgid "Zone Configuration" +msgstr "Configuração de Zonas" + +#: src/zebtrack/core/project/project_workflow_service.py:1346 +#, python-brace-format +msgid " - Use the '{panel}' panel" +msgstr " - Use o painel '{panel}'" + +# | 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/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" + +# | 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/core/project/project_workflow_service.py:1353 +#| msgid "Start drawing the arena and the ROIs for this video." +msgid " - Adjust the arena and the ROIs as needed" +msgstr " - Ajuste arena e ROIs conforme necessário" + +# | msgid "Processing and Reports" +# | msgid "Processing Mode:" +# | msgid "Processing Videos" +#: src/zebtrack/core/project/project_workflow_service.py:1358 +#, python-brace-format +#| msgid "Process Pending Videos..." +msgid "{step}. Process the pending videos" +msgstr "{step}. Processar vídeos pendentes" + +#: 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/core/project/project_workflow_service.py:1360 +#| msgid "Processing Interval (N):" +msgid " - Confirm the processing intervals" +msgstr " - Confirme os intervalos de processamento" + +#: src/zebtrack/core/project/project_workflow_service.py:1361 +#, python-brace-format +msgid " - Click '{button}'" +msgstr " - Clique em '{button}'" + +# | 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..." + +# | msgid "Loading model system..." +# | msgid "Error Loading Model" +# | msgid "Error opening folder" +#: src/zebtrack/core/project/project_workflow_service.py:1366 +#, python-brace-format +#| msgid "Error Generating Reports" +msgid "{step}. Generate reports" +msgstr "{step}. Gerar relatórios" + +#: 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/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/core/project/project_workflow_service.py:1369 +#| msgid "Generating the unified report..." +msgid " - Generate individual or unified reports as needed" +msgstr " - Gere relatórios individuais ou unificados conforme necessário" + +#: src/zebtrack/core/project/project_workflow_service.py:1372 +msgid "💡 Tips:" +msgstr "💡 Dicas:" + +#: 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/core/project/project_workflow_service.py:1375 +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/core/project/project_workflow_service.py:1377 +msgid " • Adjust the zones before processing if necessary" +msgstr " • Ajuste zonas antes de processar se necessário" + +#: 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/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." + +# | msgid "Action unavailable" +#: src/zebtrack/core/project/roi_template_manager.py:115 +#, python-brace-format +#| msgid "{count} ROI(s) available." +msgid "No ROI available to save." +msgstr "Nenhuma ROI disponível para salvar." + +#: 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/core/project/roi_template_manager.py:129 +msgid "A custom path is required for save_location='custom'." +msgstr "Caminho personalizado é necessário para save_location='custom'." + +#: src/zebtrack/core/project/roi_template_manager.py:155 +#, python-brace-format +msgid "Template '{name}' already exists in {directory}." +msgstr "Template '{name}' já existe em {directory}." + +# | msgid "Could not connect to the Arduino on port {port}." +# | msgid "Could not open the video: {path}" +#: src/zebtrack/core/project/roi_template_manager.py:233 +#, python-brace-format +#| msgid "Could not open: {path}" +msgid "Template not found: {path}" +msgstr "Template não encontrado: {path}" + +#: src/zebtrack/core/project/roi_template_manager.py:253 +#, python-brace-format +#| msgid "Inference failed on frame {frame}: {error}" +msgid "Invalid JSON in {path}: {error}" +msgstr "JSON inválido em {path}: {error}" + +# | msgid "Invalid selection" +# | msgid "Invalid target" +#: src/zebtrack/core/project/roi_template_manager.py:260 +#, python-brace-format +#| msgid "Invalid template" +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" +#: src/zebtrack/core/project/zone_manager.py:587 +#| msgid "Zones not finalised" +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 @@ -1953,24 +2426,24 @@ msgstr "Processar apenas 1 aquário agora (ignorar demais)" msgid "Split into {count} separate sessions" msgstr "Dividir em {count} sessões separadas" -#: src/zebtrack/core/recording/live_camera_mode.py:199 +#: src/zebtrack/core/recording/live_camera_mode.py:197 msgid "Record without detection (process offline later)" msgstr "Gravar sem detecção (processar offline depois)" # | msgid "System supports up to 2 aquariums simultaneously." -#: src/zebtrack/core/recording/live_camera_mode.py:204 +#: src/zebtrack/core/recording/live_camera_mode.py:202 #, python-brace-format msgid "⚠️ The system does not support {count} aquariums simultaneously." msgstr "⚠️ Sistema não suporta {count} aquários simultaneamente." -#: src/zebtrack/core/recording/live_camera_mode.py:210 +#: src/zebtrack/core/recording/live_camera_mode.py:208 msgid "Maximum supported: 1 aquarium." msgstr "Máximo suportado: 1 aquário." # | msgid "Aquarium" # | msgid "Next Aquarium" # | msgid "{name} ({count} aquariums)" -#: src/zebtrack/core/recording/live_camera_mode.py:212 +#: src/zebtrack/core/recording/live_camera_mode.py:210 #, python-brace-format msgid "Maximum supported: {count} aquariums." msgstr "Máximo suportado: {count} aquários." @@ -1978,7 +2451,7 @@ msgstr "Máximo suportado: {count} aquários." # | msgid "" "System insufficient for real-time processing. Recommended: # record # | video " "only and process offline." -#: src/zebtrack/core/recording/live_camera_mode.py:230 +#: src/zebtrack/core/recording/live_camera_mode.py:228 msgid "" "The system is not sufficient for real-time processing. Recommended: record " "the video and process it offline." @@ -1986,55 +2459,55 @@ msgstr "" "Sistema insuficiente para processamento em tempo real. Recomendado: gravar " "vídeo e processar offline." -#: src/zebtrack/core/recording/live_camera_mode.py:235 +#: src/zebtrack/core/recording/live_camera_mode.py:233 msgid "⚠️ INSUFFICIENT HARDWARE for real-time detection." msgstr "⚠️ HARDWARE INSUFICIENTE para detecção em tempo real." -#: src/zebtrack/core/recording/live_camera_mode.py:236 +#: src/zebtrack/core/recording/live_camera_mode.py:234 #, python-brace-format msgid "CPU: {cores} cores (minimum 2)" msgstr "CPU: {cores} cores (mínimo 2)" -#: src/zebtrack/core/recording/live_camera_mode.py:237 +#: src/zebtrack/core/recording/live_camera_mode.py:235 #, python-brace-format msgid "RAM: {gb:.1f}GB available (minimum 4GB)" msgstr "RAM: {gb:.1f}GB disponível (mínimo 4GB)" -#: src/zebtrack/core/recording/live_camera_mode.py:246 +#: src/zebtrack/core/recording/live_camera_mode.py:244 msgid "Record video without detection (only viable option)" msgstr "Gravar vídeo sem detecção (única opção viável)" # | msgid "Delete {asset}" # | msgid "Delete Selected" -#: src/zebtrack/core/recording/live_camera_mode.py:266 +#: src/zebtrack/core/recording/live_camera_mode.py:264 msgid "Default mode selected." msgstr "Modo padrão selecionado." -#: src/zebtrack/core/recording/live_camera_mode.py:292 +#: src/zebtrack/core/recording/live_camera_mode.py:290 #, python-brace-format msgid "Session {index} of {total} (individual aquarium)" msgstr "Sessão {index} de {total} (aquário individual)" -#: src/zebtrack/core/recording/live_camera_mode.py:320 +#: src/zebtrack/core/recording/live_camera_mode.py:318 msgid "Process multiple aquariums simultaneously in real time" msgstr "Processar múltiplos aquários simultaneamente em tempo real" # | msgid "Analysing frame {current}/{total}..." # | msgid "Processing aquarium {current}/{total}..." -#: src/zebtrack/core/recording/live_camera_mode.py:322 +#: src/zebtrack/core/recording/live_camera_mode.py:320 msgid "Process one aquarium in real time" msgstr "Processar um aquário em tempo real" -#: src/zebtrack/core/recording/live_camera_mode.py:323 +#: src/zebtrack/core/recording/live_camera_mode.py:321 msgid "Record video without detection (process offline later)" msgstr "Gravar vídeo sem detecção (processar offline depois)" -#: src/zebtrack/core/recording/live_camera_mode.py:324 +#: src/zebtrack/core/recording/live_camera_mode.py:322 msgid "Record multiple sessions, one per aquarium" msgstr "Gravar múltiplas sessões, uma por aquário" # | msgid "unknown" -#: src/zebtrack/core/recording/live_camera_mode.py:326 +#: src/zebtrack/core/recording/live_camera_mode.py:324 msgid "Unknown mode" msgstr "Modo desconhecido" @@ -2079,11 +2552,11 @@ msgstr "Erro na Câmera" # | msgid "Select an active aquarium first." # | msgid "🐟 Active Aquarium" # | msgid "Draw the Aquarium" -#: src/zebtrack/core/recording/live_session_manager.py:690 +#: src/zebtrack/core/recording/live_session_manager.py:692 msgid "🔍 Looking for the aquarium..." msgstr "🔍 Procurando aquário..." -#: src/zebtrack/core/recording/live_session_manager.py:694 +#: src/zebtrack/core/recording/live_session_manager.py:695 msgid "⏳ Waiting for video..." msgstr "⏳ Aguardando vídeo..." @@ -2320,10 +2793,6 @@ msgstr "Relatório de lote gerado ({count} sessões): {batch_id}" msgid "Video Analysis" msgstr "Análise de Vídeo" -#: src/zebtrack/ui/builders/analysis_widgets.py:163 -msgid "Processing and Reports" -msgstr "Processamento e Relatórios" - # | msgid "Loading settings..." #: src/zebtrack/ui/builders/analysis_widgets.py:205 msgid "Advanced Settings" @@ -6232,10 +6701,6 @@ msgstr "Interações sociais: não aplicável para um único animal monitorado." msgid "Video {current} of {total}" msgstr "Vídeo {current} de {total}" -#: src/zebtrack/ui/components/tab_builder.py:61 -msgid "Main Control" -msgstr "Controle Principal" - # | msgid "Error Saving to Project" # | msgid "Save to Project" # | msgid "No Project" @@ -6243,12 +6708,6 @@ msgstr "Controle Principal" msgid "Close Project" msgstr "Fechar Projeto" -# | msgid "Global Model Configuration..." -# | msgid "Model Configuration" -#: src/zebtrack/ui/components/tab_builder.py:134 -msgid "Zone Configuration" -msgstr "Configuração de Zonas" - # | msgid "Global Model Configuration..." # | msgid "Model Configuration" #: src/zebtrack/ui/components/tab_builder.py:237 @@ -6280,13 +6739,6 @@ msgstr "Parar Gravação" msgid "Add Videos/Folders to the Project..." msgstr "Adicionar Vídeos/Pastas ao Projeto..." -# | msgid "Processing and Reports" -# | msgid "Processing Mode:" -# | msgid "Processing Videos" -#: src/zebtrack/ui/components/tab_builder.py:326 -msgid "Process Pending Videos..." -msgstr "Processar Vídeos Pendentes..." - # | msgid "The processing interval must be greater than 0." #: src/zebtrack/ui/components/validation_manager.py:134 msgid "The display interval must be greater than 0." @@ -6790,11 +7242,6 @@ msgstr "" "Templates armazenam o polígono principal e todas as ROIs para reutilizar em " "outros vídeos do projeto." -# | msgid "Select the Video for Diagnostics" -#: src/zebtrack/ui/components/zone_controls.py:693 -msgid "📹 Select Video for Drawing" -msgstr "📹 Selecionar Vídeo para Desenho" - #: src/zebtrack/ui/components/zone_controls.py:703 msgid "🔍 Search:" msgstr "🔍 Buscar:" @@ -6812,11 +7259,6 @@ msgstr "Hierarquia" msgid "Data" msgstr "Dados" -# | msgid "Confirm removal of the selected item?" -#: 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/components/zone_controls.py:801 msgid "Expand all" msgstr "Expandir tudo" diff --git a/src/zebtrack/locales/zebtrack.pot b/src/zebtrack/locales/zebtrack.pot index d7a51bd1..0966aa66 100644 --- a/src/zebtrack/locales/zebtrack.pot +++ b/src/zebtrack/locales/zebtrack.pot @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: DRerio LogAI VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-08-13 13:37-0300\n" +"POT-Creation-Date: 2026-08-13 14:19-0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -1452,6 +1452,367 @@ msgstr "" msgid "Recommended, but the model is not converted. Use 'Diagnostics' to convert." msgstr "" +#: src/zebtrack/core/project/asset_manager.py:108 +msgid "Project not initialised for saving ROI templates." +msgstr "" + +#: src/zebtrack/core/project/asset_manager.py:233 +#: src/zebtrack/core/project/roi_template_manager.py:103 +msgid "The template name cannot be empty." +msgstr "" + +#: src/zebtrack/core/project/asset_manager.py:236 +msgid "Invalid zone data for saving the template." +msgstr "" + +#: src/zebtrack/core/project/asset_manager.py:239 +#: src/zebtrack/core/project/roi_template_manager.py:106 +msgid "Select at least the arena or the ROIs to save." +msgstr "" + +#: src/zebtrack/core/project/asset_manager.py:247 +#: src/zebtrack/core/project/project_manager.py:138 +msgid "Cannot save the template into the current project: no project loaded." +msgstr "" + +#: src/zebtrack/core/project/asset_manager.py:261 +#, python-brace-format +msgid "Template '{name}' already exists." +msgstr "" + +#: src/zebtrack/core/project/asset_manager.py:381 +msgid "Invalid template file: the 'data' block is missing." +msgstr "" + +#: src/zebtrack/core/project/asset_manager.py:439 +msgid "The template file is not registered in the project." +msgstr "" + +#: src/zebtrack/core/project/asset_manager.py:453 +#: src/zebtrack/core/project/asset_manager.py:487 +msgid "Invalid template content." +msgstr "" + +#: src/zebtrack/core/project/asset_manager.py:459 +#, python-brace-format +msgid "ROI template '{name}' not found in the project." +msgstr "" + +#: src/zebtrack/core/project/asset_manager.py:474 +#, python-brace-format +msgid "ROI template '{name}' not found for the requested context." +msgstr "" + +#: src/zebtrack/core/project/asset_manager.py:726 +msgid "" +"Remove the reports and summaries before deleting the arena, the ROIs or " +"the trajectory." +msgstr "" + +#: src/zebtrack/core/project/asset_manager.py:736 +msgid "There is no arena recorded for this video." +msgstr "" + +#: src/zebtrack/core/project/asset_manager.py:737 +msgid "There are no ROIs recorded for this video." +msgstr "" + +#: src/zebtrack/core/project/asset_manager.py:738 +msgid "There is no trajectory recorded for this video." +msgstr "" + +#: src/zebtrack/core/project/asset_manager.py:741 +#, python-brace-format +msgid "There is no {asset} recorded for this video." +msgstr "" + +#: src/zebtrack/core/project/asset_manager.py:745 +msgid "There are no reports or summaries to remove." +msgstr "" + +#: src/zebtrack/core/project/asset_manager.py:750 +msgid "Remove the reports and summaries before deleting the video." +msgstr "" + +#: src/zebtrack/core/project/asset_manager.py:759 +msgid "" +"Remove the arena, the ROIs and the trajectory before deleting the video " +"from the project." +msgstr "" + +#: 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 "" + +#: 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 "" + +#: 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 "" + +#: src/zebtrack/core/project/project_lifecycle_manager.py:133 +#, python-brace-format +msgid "" +"Permission denied while saving the project: {path}\n" +"\n" +"Check that you have write permission on the folder.\n" +"\n" +"Error: {error}" +msgstr "" + +#: src/zebtrack/core/project/project_lifecycle_manager.py:143 +#, python-brace-format +msgid "" +"I/O error while saving the project: {path}\n" +"\n" +"Check the disk space and the permissions.\n" +"\n" +"Error: {error}" +msgstr "" + +#: src/zebtrack/core/project/project_lifecycle_manager.py:153 +#, python-brace-format +msgid "" +"Error serialising the project data: {path}\n" +"\n" +"The project data may be corrupted.\n" +"\n" +"Error: {error}" +msgstr "" + +#: 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" +"Error: {error}" +msgstr "" + +#: 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 "" + +#: src/zebtrack/core/project/project_manager.py:829 +msgid "Video not found in the project." +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:129 +#, python-brace-format +msgid "" +"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 "" + +#: src/zebtrack/core/project/project_workflow_service.py:1184 +msgid "Default" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1328 +msgid "🎉 Project created successfully!" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1330 +msgid "📊 Video status:" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1331 +#, python-brace-format +msgid " • Total videos: {count}" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1332 +#, python-brace-format +msgid " • With arena defined: {count}" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1333 +#, python-brace-format +msgid " • With ROIs defined: {count}" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1334 +#, python-brace-format +msgid " • With trajectory ready: {count}" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1335 +#, python-brace-format +msgid " • Pending processing: {count}" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1337 +msgid "🚀 Recommended next steps:" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1343 +#, python-brace-format +msgid "{step}. Review and adjust the imported zones" +msgstr "" + +#: 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 "" + +#: src/zebtrack/core/project/project_workflow_service.py:1344 +#: src/zebtrack/ui/components/tab_builder.py:134 +msgid "Zone Configuration" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1346 +#, python-brace-format +msgid " - Use the '{panel}' panel" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1346 +#: src/zebtrack/ui/components/zone_controls.py:693 +msgid "📹 Select Video for Drawing" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1349 +#, python-brace-format +msgid " - Double-click, or use '{button}', to review" +msgstr "" + +#: 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 "" + +#: src/zebtrack/core/project/project_workflow_service.py:1353 +msgid " - Adjust the arena and the ROIs as needed" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1358 +#, python-brace-format +msgid "{step}. Process the pending videos" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1359 +#: src/zebtrack/ui/components/tab_builder.py:61 +msgid "Main Control" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1360 +msgid " - Confirm the processing intervals" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1361 +#, python-brace-format +msgid " - Click '{button}'" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1361 +#: src/zebtrack/ui/components/tab_builder.py:326 +msgid "Process Pending Videos..." +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1366 +#, python-brace-format +msgid "{step}. Generate reports" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1367 +#: src/zebtrack/ui/builders/analysis_widgets.py:163 +msgid "Processing and Reports" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1368 +msgid " - Browse the hierarchy of groups, days and subjects" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1369 +msgid " - Generate individual or unified reports as needed" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1372 +msgid "💡 Tips:" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1373 +msgid " • Use the search box to locate videos quickly" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1375 +msgid " • The status symbols show which arenas, ROIs and trajectories exist" +msgstr "" + +#: src/zebtrack/core/project/project_workflow_service.py:1377 +msgid " • Adjust the zones before processing if necessary" +msgstr "" + +#: src/zebtrack/core/project/roi_template_manager.py:109 +msgid "Zone data cannot be empty." +msgstr "" + +#: src/zebtrack/core/project/roi_template_manager.py:112 +msgid "Invalid arena: at least 3 points are required." +msgstr "" + +#: src/zebtrack/core/project/roi_template_manager.py:115 +msgid "No ROI available to save." +msgstr "" + +#: src/zebtrack/core/project/roi_template_manager.py:123 +msgid "A project path is required to save a template into the project." +msgstr "" + +#: src/zebtrack/core/project/roi_template_manager.py:129 +msgid "A custom path is required for save_location='custom'." +msgstr "" + +#: src/zebtrack/core/project/roi_template_manager.py:155 +#, python-brace-format +msgid "Template '{name}' already exists in {directory}." +msgstr "" + +#: src/zebtrack/core/project/roi_template_manager.py:233 +#, python-brace-format +msgid "Template not found: {path}" +msgstr "" + +#: src/zebtrack/core/project/roi_template_manager.py:253 +#, python-brace-format +msgid "Invalid JSON in {path}: {error}" +msgstr "" + +#: src/zebtrack/core/project/roi_template_manager.py:260 +#, python-brace-format +msgid "Invalid template in {path}: {error}" +msgstr "" + +#: src/zebtrack/core/project/zone_manager.py:587 +msgid "Project data not initialised" +msgstr "" + #: src/zebtrack/core/recording/frame_processing_pipeline.py:684 #, python-brace-format msgid "🔍 Detecting aquarium... ({current}/{total})" @@ -1549,74 +1910,74 @@ msgstr "" msgid "Split into {count} separate sessions" msgstr "" -#: src/zebtrack/core/recording/live_camera_mode.py:199 +#: src/zebtrack/core/recording/live_camera_mode.py:197 msgid "Record without detection (process offline later)" msgstr "" -#: src/zebtrack/core/recording/live_camera_mode.py:204 +#: src/zebtrack/core/recording/live_camera_mode.py:202 #, python-brace-format msgid "⚠️ The system does not support {count} aquariums simultaneously." msgstr "" -#: src/zebtrack/core/recording/live_camera_mode.py:210 +#: src/zebtrack/core/recording/live_camera_mode.py:208 msgid "Maximum supported: 1 aquarium." msgstr "" -#: src/zebtrack/core/recording/live_camera_mode.py:212 +#: src/zebtrack/core/recording/live_camera_mode.py:210 #, python-brace-format msgid "Maximum supported: {count} aquariums." msgstr "" -#: src/zebtrack/core/recording/live_camera_mode.py:230 +#: src/zebtrack/core/recording/live_camera_mode.py:228 msgid "" "The system is not sufficient for real-time processing. Recommended: " "record the video and process it offline." msgstr "" -#: src/zebtrack/core/recording/live_camera_mode.py:235 +#: src/zebtrack/core/recording/live_camera_mode.py:233 msgid "⚠️ INSUFFICIENT HARDWARE for real-time detection." msgstr "" -#: src/zebtrack/core/recording/live_camera_mode.py:236 +#: src/zebtrack/core/recording/live_camera_mode.py:234 #, python-brace-format msgid "CPU: {cores} cores (minimum 2)" msgstr "" -#: src/zebtrack/core/recording/live_camera_mode.py:237 +#: src/zebtrack/core/recording/live_camera_mode.py:235 #, python-brace-format msgid "RAM: {gb:.1f}GB available (minimum 4GB)" msgstr "" -#: src/zebtrack/core/recording/live_camera_mode.py:246 +#: src/zebtrack/core/recording/live_camera_mode.py:244 msgid "Record video without detection (only viable option)" msgstr "" -#: src/zebtrack/core/recording/live_camera_mode.py:266 +#: src/zebtrack/core/recording/live_camera_mode.py:264 msgid "Default mode selected." msgstr "" -#: src/zebtrack/core/recording/live_camera_mode.py:292 +#: src/zebtrack/core/recording/live_camera_mode.py:290 #, python-brace-format msgid "Session {index} of {total} (individual aquarium)" msgstr "" -#: src/zebtrack/core/recording/live_camera_mode.py:320 +#: src/zebtrack/core/recording/live_camera_mode.py:318 msgid "Process multiple aquariums simultaneously in real time" msgstr "" -#: src/zebtrack/core/recording/live_camera_mode.py:322 +#: src/zebtrack/core/recording/live_camera_mode.py:320 msgid "Process one aquarium in real time" msgstr "" -#: src/zebtrack/core/recording/live_camera_mode.py:323 +#: src/zebtrack/core/recording/live_camera_mode.py:321 msgid "Record video without detection (process offline later)" msgstr "" -#: src/zebtrack/core/recording/live_camera_mode.py:324 +#: src/zebtrack/core/recording/live_camera_mode.py:322 msgid "Record multiple sessions, one per aquarium" msgstr "" -#: src/zebtrack/core/recording/live_camera_mode.py:326 +#: src/zebtrack/core/recording/live_camera_mode.py:324 msgid "Unknown mode" msgstr "" @@ -1645,11 +2006,11 @@ msgstr "" msgid "Camera Error" msgstr "" -#: src/zebtrack/core/recording/live_session_manager.py:690 +#: src/zebtrack/core/recording/live_session_manager.py:692 msgid "🔍 Looking for the aquarium..." msgstr "" -#: src/zebtrack/core/recording/live_session_manager.py:694 +#: src/zebtrack/core/recording/live_session_manager.py:695 msgid "⏳ Waiting for video..." msgstr "" @@ -1851,10 +2212,6 @@ msgstr "" msgid "Video Analysis" msgstr "" -#: src/zebtrack/ui/builders/analysis_widgets.py:163 -msgid "Processing and Reports" -msgstr "" - #: src/zebtrack/ui/builders/analysis_widgets.py:205 msgid "Advanced Settings" msgstr "" @@ -5077,18 +5434,10 @@ msgstr "" msgid "Video {current} of {total}" msgstr "" -#: src/zebtrack/ui/components/tab_builder.py:61 -msgid "Main Control" -msgstr "" - #: src/zebtrack/ui/components/tab_builder.py:82 msgid "Close Project" msgstr "" -#: src/zebtrack/ui/components/tab_builder.py:134 -msgid "Zone Configuration" -msgstr "" - #: src/zebtrack/ui/components/tab_builder.py:237 #: src/zebtrack/ui/components/tab_builder.py:276 msgid "AI Model Config." @@ -5111,10 +5460,6 @@ msgstr "" msgid "Add Videos/Folders to the Project..." msgstr "" -#: src/zebtrack/ui/components/tab_builder.py:326 -msgid "Process Pending Videos..." -msgstr "" - #: src/zebtrack/ui/components/validation_manager.py:134 msgid "The display interval must be greater than 0." msgstr "" @@ -5527,10 +5872,6 @@ msgid "" "other videos of the project." msgstr "" -#: src/zebtrack/ui/components/zone_controls.py:693 -msgid "📹 Select Video for Drawing" -msgstr "" - #: src/zebtrack/ui/components/zone_controls.py:703 msgid "🔍 Search:" msgstr "" @@ -5548,10 +5889,6 @@ msgstr "" msgid "Data" msgstr "" -#: src/zebtrack/ui/components/zone_controls.py:763 -msgid "📹 Load Frame from the Selected Video" -msgstr "" - #: src/zebtrack/ui/components/zone_controls.py:801 msgid "Expand all" msgstr "" diff --git a/tests/core/test_asset_manager_basic.py b/tests/core/test_asset_manager_basic.py index 6f766f2b..de7ff423 100644 --- a/tests/core/test_asset_manager_basic.py +++ b/tests/core/test_asset_manager_basic.py @@ -21,7 +21,7 @@ def test_ensure_roi_template_dir(tmp_path): assert target.exists() assert target.name == "roi_templates" - with pytest.raises(ValueError, match="Projeto não inicializado"): + with pytest.raises(ValueError, match="Project not initialised"): AssetManager.ensure_roi_template_dir("") @@ -116,7 +116,7 @@ def test_can_remove_asset_summary_dependency(): can_remove, message = manager.can_remove_asset(video_entry, "arena") assert can_remove is False - assert "Remova os relatórios" in (message or "") + assert "Remove the reports and summaries" in (message or "") def test_remove_summary_asset(monkeypatch, tmp_path): diff --git a/tests/core/test_project_workflow_service.py b/tests/core/test_project_workflow_service.py index 56204334..70b35514 100644 --- a/tests/core/test_project_workflow_service.py +++ b/tests/core/test_project_workflow_service.py @@ -96,8 +96,8 @@ def test_validate_project_parameters_det_multi_animal_invalid(self): assert is_valid is False assert error_msg is not None - assert "modo de detecção" in error_msg - assert "1 animal por aquário" in error_msg + assert "detection mode (det)" in error_msg + assert "1 animal per aquarium" in error_msg def test_validate_project_parameters_seg_multi_animal_valid(self): """Test validation succeeds for seg mode with multiple animals.""" @@ -422,7 +422,7 @@ def test_create_project_validation_failure(self): ) assert result["success"] is False - assert "modo de detecção" in result["error_message"] + assert "detection mode (det)" in result["error_message"] self.mock_project_manager.create_new_project.assert_not_called() def test_create_project_creation_failure(self): @@ -645,7 +645,7 @@ def test_generate_guide_with_videos(self): assert guide is not None assert guide["title"] == "Bem-vindo ao Projeto!" assert "2" in guide["message"] # Total videos - assert "Próximos passos" in guide["message"] + assert "Recommended next steps" in guide["message"] def test_generate_guide_suppressed_by_env(self): """Test guide generation suppressed by environment variable.""" @@ -701,8 +701,8 @@ def test_generate_guide_with_import_config(self): ) assert guide is not None - assert "arena definida: 1" in guide["message"] - assert "ROIs definidas: 1" in guide["message"] + assert "arena defined: 1" in guide["message"] + assert "ROIs defined: 1" in guide["message"] class TestProjectWorkflowServiceProjectLoadFailures(unittest.TestCase): @@ -1138,7 +1138,7 @@ def test_generate_guide_malformed_import_config(self): # Should still generate guide assert guide is not None - assert "Total de vídeos: 1" in guide["message"] + assert "Total videos: 1" in guide["message"] if __name__ == "__main__": diff --git a/tests/core/test_roi_template_manager_validation.py b/tests/core/test_roi_template_manager_validation.py index 66b2ca07..f85068ed 100644 --- a/tests/core/test_roi_template_manager_validation.py +++ b/tests/core/test_roi_template_manager_validation.py @@ -23,11 +23,11 @@ class TestROITemplateValidation: """Testes de validação de templates.""" def test_load_template_with_invalid_json(self, roi_manager, tmp_path): - """Template com JSON inválido deve falhar.""" + """A template with invalid JSON must fail.""" invalid_file = tmp_path / "invalid.json" invalid_file.write_text("{invalid json}", encoding="utf-8") - with pytest.raises(InvalidTemplateError, match="JSON inválido"): + with pytest.raises(InvalidTemplateError, match="Invalid JSON"): roi_manager.load_template(invalid_file) def test_load_template_with_missing_fields(self, roi_manager, tmp_path): @@ -78,7 +78,7 @@ def test_load_template_with_invalid_data_structure(self, roi_manager, tmp_path): encoding="utf-8", ) - with pytest.raises(InvalidTemplateError, match="pelo menos arena"): + with pytest.raises(InvalidTemplateError, match="at least an arena"): roi_manager.load_template(empty_template) # Caso 2: Template com ROIs incompletas (faltam roi_names e roi_colors) @@ -97,7 +97,7 @@ def test_load_template_with_invalid_data_structure(self, roi_manager, tmp_path): encoding="utf-8", ) - with pytest.raises(InvalidTemplateError, match="pelo menos arena"): + with pytest.raises(InvalidTemplateError, match="at least an arena"): roi_manager.load_template(incomplete_rois) def test_load_valid_template_succeeds(self, roi_manager, tmp_path): diff --git a/tests/core/test_schemas.py b/tests/core/test_schemas.py index f7ad4138..23f6d1d8 100644 --- a/tests/core/test_schemas.py +++ b/tests/core/test_schemas.py @@ -28,12 +28,12 @@ def test_roi_template_schema_accepts_rois(): def test_roi_template_schema_rejects_missing_data(): - with pytest.raises(ValueError, match="Template deve conter"): + with pytest.raises(ValueError, match="Template must contain"): ROITemplateSchema(version=1, name="Test", data={}) def test_roi_template_schema_rejects_partial_roi_keys(): - with pytest.raises(ValueError, match="Template deve conter"): + with pytest.raises(ValueError, match="Template must contain"): ROITemplateSchema( version=1, name="Test", @@ -42,7 +42,7 @@ def test_roi_template_schema_rejects_partial_roi_keys(): def test_roi_template_schema_rejects_future_version(): - with pytest.raises(ValueError, match="não suportado"): + with pytest.raises(ValueError, match="is not supported"): ROITemplateSchema(version=2, name="Test", data={"polygon": [[0, 0], [1, 0]]}) diff --git a/tests/i18n/test_no_untranslated_literals.py b/tests/i18n/test_no_untranslated_literals.py index 1cdf8dd9..ce485d2e 100644 --- a/tests/i18n/test_no_untranslated_literals.py +++ b/tests/i18n/test_no_untranslated_literals.py @@ -32,6 +32,7 @@ "src/zebtrack/coordinators", "src/zebtrack/core/app_runner.py", "src/zebtrack/core/application_bootstrapper.py", + "src/zebtrack/core/project", "src/zebtrack/core/recording", "src/zebtrack/i18n.py", "src/zebtrack/io", diff --git a/tests/test_detection_enforcement.py b/tests/test_detection_enforcement.py index e81e4c99..30ba3af9 100644 --- a/tests/test_detection_enforcement.py +++ b/tests/test_detection_enforcement.py @@ -38,8 +38,8 @@ def test_detection_mode_with_multiple_animals_blocked(): assert not is_valid assert error_msg is not None - assert "modo de detecção (det)" in error_msg - assert "1 animal por aquário" in error_msg + assert "detection mode (det)" in error_msg + assert "1 animal per aquarium" in error_msg def test_detection_mode_with_single_animal_allowed(): @@ -137,4 +137,6 @@ def test_single_video_detection_mode_enforcement(): call_ev = mock_event_bus.publish.call_args[0][0] assert call_ev.type == UIEvents.SHOW_ERROR assert "Configuração Inválida" in call_ev.data.title + # Producer is core/viewmodels/analysis_control_view_model.py, not yet + # migrated: still Portuguese on purpose. assert "modo de detecção (det)" in call_ev.data.message diff --git a/tests/test_project_manager.py b/tests/test_project_manager.py index beed08c0..d184503e 100644 --- a/tests/test_project_manager.py +++ b/tests/test_project_manager.py @@ -850,7 +850,7 @@ def test_can_remove_arena_blocked_by_summary(self): allowed, reason = pm.can_remove_asset(video_path, "arena") self.assertFalse(allowed) - self.assertIn("relatórios", reason or "") + self.assertIn("reports", reason or "") def test_remove_rois_clears_zone_and_file(self): pm, video_path, files = self._create_manager_with_assets( @@ -925,7 +925,7 @@ def test_load_nonexistent_project(self): project_path = os.path.join(self.test_dir, "nonexistent_project") os.makedirs(project_path, exist_ok=True) # Create dir but no config - with pytest.raises(ProjectInvalidError, match="não encontrado"): + with pytest.raises(ProjectInvalidError, match="not found"): pm.load_project(project_path) def test_update_video_status_and_get_next(self): From 6fbee7f2b8e139f1152b113cfb164f041a08f29b Mon Sep 17 00:00:00 2001 From: MarkSant Date: Thu, 13 Aug 2026 14:39:28 -0300 Subject: [PATCH 05/21] fix(i18n): scanner acusava docstring de atributo como string de interface `_exempt_docstring` isentava apenas `body[0]` -- a docstring de modulo, classe ou funcao. Uma docstring de ATRIBUTO (PEP 258: o paragrafo logo abaixo de um membro de enum ou de um atributo de classe) e um `ast.Expr` de string como qualquer outro, so que nao e o primeiro do corpo, entao passava direto e era reportada como literal por traduzir. Custava tres falsos positivos em `core/services/external_trigger_gate.py`, onde cada membro do `ExternalTriggerDecision` documenta em portugues por que aquela decisao existe -- prosa que o projeto decidiu MANTER (regra 10 do guia). Sem esse conserto, fechar `core/services` exigiria ou traduzir documentacao ou inflar o allowlist com texto que nao e contrato nenhum. A regra nova e mais geral e mais barata de justificar: uma string que e um EXPRESSION-STATEMENT e avaliada e descartada -- ela nao tem como chegar a um widget. A unica razao para escrever uma e documentar. Total do pacote cai de 786 para 783 literais sem tocar em uma linha de `src/zebtrack`. Os 60 testes de `tests/i18n` seguem verdes, ou seja, a isencao nao escondeu nenhuma string real nos pacotes ja migrados. Co-Authored-By: Claude Opus 5 --- scripts/i18n_scan.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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) From 2b3baeb58454886d21c7e49560449f45eb70c4c1 Mon Sep 17 00:00:00 2001 From: MarkSant Date: Thu, 13 Aug 2026 22:12:48 -0300 Subject: [PATCH 06/21] feat(i18n): traduzir core/services/** (fase 3, lote 3c) `core/services` a zero e no ratchet: 63 literais em 7 arquivos. ACHADO PRINCIPAL -- ramificacao pelo TEXTO da excecao, o antipadrao que o guia proibe, a um passo de ser detonado por esta propria migracao. `detector_service._validate_range` fazia: try: val = float(value) if val < min_val or val > max_val: raise ValueError(f"{param} deve estar entre {min} e {max}, ...") except (TypeError, ValueError) as e: if isinstance(e, ValueError) and "deve estar entre" in str(e): raise raise ValueError(f"{param} deve ser um numero valido") from e O `if` existia so para distinguir "fora da faixa" de "nao e numero", porque as duas coisas eram levantadas de dentro do MESMO `try`. Traduzir a mensagem da faixa faria o `in str(e)` falhar e o erro seria reetiquetado como "nao e um numero" -- ramo ERRADO, sem excecao nenhuma para denunciar. A correcao nao e traduzir com cuidado: e tirar o range check de dentro do `try`, o que elimina a necessidade de distinguir. Comentario no local registra o porque. Outros pontos: - `arduino_ack_semantics.describe_inversion` encaixava "entrada"/"saida" DUAS vezes na mesma frase, com um "uma {edge_pt} deveria {expected}" que so funciona porque os dois substantivos sao femininos em portugues. Virou duas frases completas escolhidas pelo `edge` -- que e sentinela de codigo, nao texto exibido, entao a ramificacao continua legitima. - `ArduinoBinding.describe` idem, em versao mais simples. - Mais dois `_` como descarte em `weight_manager` (`_, current_default =` e um `for _, filename, *_` numa generator expression). O quarto e o quinto da fase. - Os `metadata["description"]` gravados no `metadata.json` do modelo convertido passaram a INGLES FIXO, sem `_()`: sao valores escritos em disco, e uma descricao que mudasse com `ui.language` daria arquivos diferentes por maquina para o mesmo modelo. Mesma logica separa `details["last_conversion_error"]` (GRAVADO, ingles) do `OpenVINOExportError` (LEVANTADO ao operador, traduzido) -- a mesma frase, dois papeis distintos. - As guardas de parametro do detector (`conf_threshold`, `track_buffer`) ficaram em ingles sem `_()`: nomeiam parametros de codigo e seus erros sobem pelo EventBus, que os engole antes de qualquer dialogo. Asserts migrados em 8 arquivos. QUATRO deles so quebram sob `-m gui` (test_calibration_step) -- a suite rapida passou verde com o bug presente, que e exatamente por que o `-m gui -n0` e obrigatorio antes do push. Verde: ruff limpo, mypy limpo em 698 arquivos, 4214 fast, 1079 gui. Co-Authored-By: Claude Opus 5 --- docs/tasks/active/ROLLING_TASK_LOG.md | 4 +- .../core/services/arduino_ack_semantics.py | 20 +- .../core/services/arduino_bindings.py | 9 +- .../core/services/detector_service.py | 39 +- .../core/services/model_override_service.py | 15 +- src/zebtrack/core/services/model_service.py | 16 +- src/zebtrack/core/services/weight_manager.py | 67 ++- src/zebtrack/core/services/wizard_service.py | 98 ++-- .../locales/_pairs/pr3-core-services.json | 67 +++ .../locales/pt_BR/LC_MESSAGES/zebtrack.mo | Bin 152155 -> 159177 bytes .../locales/pt_BR/LC_MESSAGES/zebtrack.po | 484 +++++++++++++++--- src/zebtrack/locales/zebtrack.pot | 331 ++++++++++-- ...st_wizard_service_camera_friendly_names.py | 4 +- tests/core/test_arduino_ack_semantics.py | 2 +- tests/core/test_detector_service.py | 4 +- tests/core/test_weight_manager.py | 2 +- .../test_wizard_service_multi_aquarium.py | 14 +- tests/i18n/test_no_untranslated_literals.py | 1 + tests/test_class_names_validation.py | 2 +- tests/ui/wizard/test_calibration_step.py | 8 +- 20 files changed, 974 insertions(+), 213 deletions(-) create mode 100644 src/zebtrack/locales/_pairs/pr3-core-services.json diff --git a/docs/tasks/active/ROLLING_TASK_LOG.md b/docs/tasks/active/ROLLING_TASK_LOG.md index 4cb7279d..2a703748 100644 --- a/docs/tasks/active/ROLLING_TASK_LOG.md +++ b/docs/tasks/active/ROLLING_TASK_LOG.md @@ -45,7 +45,9 @@ no topo do ratchet; depois, atualizar a seção "A migração" em 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. -- [ ] Lote 3c: `core/services/**` (61 em 7 arquivos). +- [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. - [ ] Lote 3d: `core/video/**`, `core/detection/**`, `core/viewmodels/**` (30 em 10 arquivos), incluindo simplificar o consumidor de `progress_notifier` para só "step:" depois de migrar o produtor. 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..9fdbc2d3 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")) @@ -519,9 +530,9 @@ def _validate_range( if "track_buffer" in params_dict: try: if int(params_dict["track_buffer"]) < 1: - raise ValueError("track_buffer deve ser pelo menos 1") + raise ValueError("track_buffer must be at least 1") except (TypeError, ValueError) as e: - raise ValueError("track_buffer deve ser um número inteiro") from e + raise ValueError("track_buffer must be an integer") from e 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/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/pt_BR/LC_MESSAGES/zebtrack.mo b/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.mo index 24a2293d3e247b3df9b8ba50ff4820f887175014..42cf5e0de16ebff6b58028f035494d31fdcaaa73 100644 GIT binary patch delta 28397 zcmb8%2Ygh;-v9A)mQX^6P(ojN5kf+f-m8>=^v;rOl7%E2HVHi}mW!e^MTj6pz=8-^ z0!9%*1Qi9W2sTi$f+*6g2+!wx<`DJj{onilzh2M(edd%oGr#%GoY~0peDqM{Yad01 zPnM5ZVe$Xw6t=ADchYgtXP2DZVQup-`q^)VNl;|tgtzr!k6cbsW> zCmcX|3{tW6D9*(fQ4Q%d-m+TZaBPNmVQb!RJG|f@Lg2Tk7EP;4AZd~t(b{3um}F&C;kd~t0c>6 zfpbw~{RCFU_pmPhfW5IovT4v*)Qjh%BC;Gc6wjcB?qyU@KSSMj0c&C56jQE`%Ja4< z#9yIEii}wjL)Jf+=F`2o9^|Ks7PJJVrT^{t2jiWB2obr(FX4MPN)X; zb+3;NyEmj^Di>}?h4ws-#KsBhEj73_;jBz(Ffc{D7^o{xmbDW3dM1d8kmX!3f;$numJsS-b%FI&>9q)%;#M9K1gp{vrXb#l8sl-yxk#LhS}{}cPF#TDz8sX9Wo{gWYRCjsPf}4W zo`sB+H6K;cR#e2EQJJybhZQLwnr*WEGi*iqXRLtD=9r}Jj_Sx5R0O8ZA^uf3n8S(s zxB_p)=dmeXM7_|nl!)H10a?s#*^V#i#(Uj+5Gt5P;121DuEcGu_QB%~Ajlg?smTvrw za%tvbJQgRz2I3gh(!Cj%;5l52^A2yA;d5A%XfsPb2f zO!9q;>gkVI2+J-u5vzbDDR*@ph(#%nLFLY59E~%vlIH(A98~ATX;jaP(9B|33%g(= zRPv>uDwu<%@E%kWu1Bp8yRZfxcCVj7W%4&gW)bC2m*J}OttXl&ho~2xMkU{mSQbk$ZUeCbudJ@Ol5aBhK(%lYvi4YOkO^W%GwDfOYYjHX`uCf4V>qTzo`z51hp31yT4i$P z8!SipHynzkR-0Y34{xTtF3f>mcplrL{ea2-_NYiK#ag%tr{aFM+*8Td^=M zM5lqM5O2at_#8&#Td4X@qwf33Eth!E3{4eeAqra!IHjtnP*RnB z*nFM3;!MiZ@I0PDh4|w~Ob;%g%4Hul$o#AgVzV zFsy|pm4gVJi90a|H9cBBW*+E{df`y)jsYx&Td^7L#P{)YEQ-6gnCB0mUi7Z(XLt+c zbEqMl_Bip^*erP5>}XbEP0Amm7k@)dpXyu9!V-h6DNn{YydM?P*HQJnjpgw(RD&;J z4J^LR3}GYGbKS5ZW^Na6}hz-i5u|= z+=_l2N1+wIgnI5LEQ7_KG(Tjb!yKrEov%qC4sDVpRE7n$2#qXjj_yRBB6;x84+iN0m6;&?zq8Y+!sOP+>$i<;@ zr!T6(k6wje5dIi4C1nDbZz_1uf7 z>qk)ce~jwT_o&>e_>#$iSX77mg*ng*l2I)SVNINkjc^rey1axMq94(+Sqt`?v5Y!k zLOU5-a48+5aRci9U04T?p&IxLR>vYQo1qHVU|pP;{j}lXHXF-f6!Ub!d5#D)WQL%8-u9OuRtZ^BdA%i z%Pqf!s`v}k82^YGic+tc6|y0=qTC*pJ3;sS9T-7*HMYm~7_0gJAqUFN60e(Nsf-F$ zeJqbH(2u>a7(R|gaR-jX-KdHy95M~7f@<&$*bcj)hGq_GD7WK0JcUub-|`(cJ;+9d zbSXZ9n^8T8KVtUv$*3f{f{IY(H_VF~VOz?bu^-OBCb$pPfwOo6UUR+SP5#t z9$iB zsv+~yX&|ZrTTu7y#=GzkR>u*an1w78<0#*Yitqu{doE)w^3N*#sj09&s)e1gAx^;1ylsOeQqK&5j9Ois0Po$Qn(1K;A)J)9nSeMyH*Y~=I2p8 zyNaE$@)z_TM__y0ip}wBRE3qlH1oc`YZFvG%~3Dvh-$zHoQcU;0YAk`co~cEeyikH z=4%v%3T0K)ENG0aF$>k=EvR|@0k*`5uT9dm#kQ1#s1R>PCEW+8-1;09@}E#cR`eT_ z8&xn|h!Y(+P|v%d9vJ1CiWMo(cU_4^C~rm0nmkmG_hJJ)?4G}f>c}-zM@pYIe{va( z9VzGGNc`b6R>#Uz=X5Jzaa-fPN-+OsEXF1Ubq95{rgd4`3Y9VA5ckI;yZJHT~q|y zp{D80sD}Dcx$r6$#?NpWevQh3nc?qEg-fv%C$^wo^epC(-LIj>vd?+bgCVXyRKuoW zXAd>J6!i+6UuL(dj1P45~VJfoC??AKotzYI2?-_lEv5yU&E$Y=Aucmj@XQH zHfjhqp%$9$sCwQ)FMfjE(Yj<_)B_t)4532*05Z$M)}tJFIq?-LBt)K`lU+P@ymXt0~vQ2+Ex>3cF)boAm(8QJ(M{4b}Y5 z;6Oci02TU^7=u->n6Fen)bv_{YQRp^^`lr13;k}AuMui&yJ0mPf~_zO2jfPpffunc zmb*&)bz=t(s^Cb}ESQFR;Y?KM*Q0Xa2r9I{qQ<)ZHS^hRg?e#ERPqjR&yU9dWj`v3 z-*nG^fQrCp*NDH5gNvL&QA7A0 z*2bFricti*qJ}sd)xfo`+dX01X~|wrROQ0ksGglgwfHwo#KMJaCpo8J1cD{-a;1J5sVrMKKE@C?&>WAYvk%z6ZESaLv_eMn` z3kTvOsG+!oZLxMS+jl?(}3`s94KU^OPU^a zL4`UUHP+jae6&75-S=K8+u6+)DQ!aA4%MJ^)b$5YkvWVSg5S`KHOiQBXVk2kgo8By z7nlRix)oBvm3*y9Gv7pq3U1Fb~cMUu_fim>bA4x>V$e=5-MwN z#iwu|YKSsx*w%wssHW}w(di*nL+`I;rroQkp*e?&a0RdJd|SGB?XVNde{n(=o<@b} z1S-U(YMW5CM^%u3>e-#R0bjs0oLI-UcHAKhTqle1wCigTh+Q`6(psIOiJe3%CYVFcw%%}mz*f+Z<8kFlN4 zXh%$@+!xirC$SgqMde1}=B6QSP#>Y5sOLvvZ4CQ4i00r9tb`BZOw7lw7#nLUo`!1q zd@P6epeo#idQrZ6{u|V{d7u_iu*7Uzeh#nXH-K=v@t{3z_kmi0i&=Trl8h|jo1)BMCbeO zxzWtmn&|Wpb%PJpu!X2C*Gg1_UqZdGdt38W8;^>>Y}EPtumV1T`h9T#HTDOwFdj!G z^@+C3f1Nnxp7_SS@jIN$`75X%%xY)WhlQwQ-GK__5!8^KM%{NF6%o6=u>xvcX^3jz z&8YiRQIXAR&-~ZIFrO2uV6S__o9MI@)$$W=`7ElU3#fTsvV-mXg`*`_r~Ei7mky$) z*A>)yQM04DzbPt`Ls9jF!yI(wU=`{`A7Thkqjogo_@jWPOvq7PNk z0#sxkL=C}nSRap~J~qFi?yK9yc7EV=LOmZI>K@F*nw(gRx^Xuu#K&+FUciYsw5xeh z9@e3J7?qUgQ1iWdH#42{P_yA})B<-J7vLrQ7tZPK&VRdySx}--5vY%vR;^GK^g?Cr zc<>3RY+eZEB{lQqzceJfN?`=F9*5^5c|6>H%lRJLzL<;rnX4xB;V zUv`igiWaC9w-bhyEMqy)beN7R---&=T2v@^q8>Pe8uQbrZ$Qa-vp&>BCpl5s-Wk<_ z0j@q&ebZ6(FF`fzv3TacZhVsyO1dvlV|Eq$VYR_#Nc=d0@=8=PeTVA#&!~z$Lrepz zq8i)`)uA4!hR36FCJmJfb5W68J|t{TY~_UJ|9(_Y-bGdXm0P~*UN3jE$(7ou2DCsm zyd!Gm9D$0=6jTz=MXjWZQ4QINUVInT(Mw?tR8hI1=Ec=eOKuEmf$89$pNuh-v(bwW zq3(Oxz5Wg=v|pkcZVfXFO*E>3JyAI_0X5C%p&}n%%Yj1iH0s7fs8D^0s_+u3rDcYj z&^ARys5`2{;i!tTah1*13)@kyH`3fc0X1YrlAY58RboQBfjmPj~r_jq`Ii{gHhA)PE=%$pz8S&2WtM?<4jh^ z<6JI;P)TwQTVav$=Cj-ZRrb5()i{vy0aSy_PcYY8U~S6Dt_xAw{x~Yf-p02088+hm zR@I3nX?mch&u~-)38*3X7wQEMqK4u{RC1j_jpg^K`%6vY2L(3Abex7-nm@(iy6!Uz zRRT_+{0N3$;h^wjlck680OdjSL44B`9wNdvCMde5U zHGAf}jq zKx5dE17-C9RLhs5dh{qNN!~_P@FD73aTZn4kC=sJW|)QOcGS?kgIZ`l!9{ou`{G=} z8iOz66ugAtYz`*QvaKWd9u~n(vrP}Tpc=Fj@4zEC9*55{_vN7?vkNs;uVEoPj@spZ zg!&8@ookH3Rt#+|R5B0CVg4%%lXJ|N-in$9yHGuR3l;LyuD`pMn`a_aA3Je99^-H^ zDnffvFL(`A&&Q})bQ!h26ldCNBhx<2fmW>XsPFXcs8DZ3HDnLA!lS75AacIR`evvk z?2noqx1lONj<;gbTg(D<7d}Jz7;086zST6e*lp&c)IQ8XFD}Tg_%dpYirj8~Hn+yM zlpn?}c+$1zzf1%s;7HEjiOTLzQ9Y}+z+CT)`ZlDX7wr3?FkZl)uBw_cx+1fKJ0`0P%nsFXc~G0`Y0!&et5is%Jy@p`s&<8F4_F88L#qw z>yx|9Hv8lvlf}&z+s=P{o`Vnaz#&|RsY}e5mRM?rpeC;2d`BFFAE4%Wqh+@B0xyU` zmCxQ|hV%kzdPdx9lC&<~p!wgB15KNuu9Hw}cp55cvT-opg<5()LN8uM&GSmj%`|O> z8p<}P+?k6?zI##A@o}t$&!e9E2y1KpU*JH~r^0>a#yY45wLrbFi|c4q5@w=akc(P^ zx1pxlA=GR+j(Xv@s9EqEYW|mAVd@=;sy7kCn>d)qK`z!=X+E>NP%BrZT(dQ6iNh$5 z!kcj$s$%PYGykJp>$t|bc13k09uqOSjr{ju^I6M+QGp&Y_H@dwvgYt5|ktTSUCi(|On2Y2JfFb6|8 zn7H0l{1mFkhtQ8Fu{-vB&@8=opss(3-SB5j!}c3YPHaX^*FBhwpP{BR8-Q zYY1vuu1Dp}PSghEIBFsK7}cRus1E#qF`ECSA2%P9PN*Jcqm%8Zuh|CF!tpq^z(d#^ zuVNg=Y&AQYNqCj=x2O=G*=AmJ4b{OK+fCB8Mtx&$!LX9$Ne(*WyQpbc{t1%{?NFi0 zKuxPKYPQ^s8taw#Dy~DlAn8dHfqAH$xDS=2d8p?u|+GKT0 zROox7Djb2cG2JbncCXucW<@K93iTLNgyy1V#~M^JKY{J=D{PJ49cJS)0+nmycZ5xH zq;o>qc`Is(U4=@b7f>xeh8pYAJIx;^I$}f03s7Ub-E{{lS9YNy_dV)GzoSB4`WbV7 zCsagwggMy3!5~b)LiuJ#lZCB?MrHd^)R6s-s@VIi$?7;%mJdMX&?Hn2%y!Q& zMKxd(YN2}vRd4tc4z$+)gf(qu!*izREq9q04nS3$jf%)ZRB~=ag?|3@6?!Skq!%j`21R>s+S=lM>|lV zeitX>FR0Lsd)Z8@B=k~Vi&|3mU@7YP0F_iiQT|PZCi*pMiS8QdE8GP}49U)v?34 z0nek}vpjsr-1snR-tIxo)Av!4DSFu00F?v1P-B>YYCslxaUN>LTaR7v7}my0N6f5f zi^D08d&A8Cr?C&^@Lmp-JVoC$Ho+#82cnW^ChD88z&*bW)wB1pE1pN~WLmssX2&_4 zMtS_(X8ym4%B8c|5X-+~R@nAPN5j?x4%G9-u8(3<%KNYxeveu(s=sTJtuN~9mw<7& z95v=IqlVxkR0A)%mON_C*GCOOCseXe#;ThCcXQB_6OXtze2Yr9vhSIXMLpEYb_3ps zNvO!I!(zAvUGwR9)qgLn(7=XE}?oquH30d@T_Dnh5-@-@^Jt@?*FbRq|nIB0<{ zqO$oSD&$2zGM~*BSd8hEg39J;ADad*Kn=k<9EWeY=bN1{)3X<A6nm< zU$-~mDvHZcW837c$%Rg+AsUVv%4GNaBGd~XLPhit>bXyGi{}4D4itfP-ik`>G!aE{W(iG%c(ENSL}O6%J%}2k)2J1)@MW`*)N*Zz3VCz93A>>pw+OXD9zd<6m#{U~{?S|? ziCR~dqVvE1^EwAgy5CR{DDsmDd398eo1@n59;g*>DryL_Q1>rJHE@&b3-0yfSd{ZW zppyA#R1y~c*)%BXXXd{y^y7qHGy(_V6zqe~VGp!_G3R?>JIeQ=dh`a~iV?q>=kGu@ z7&hT1c8) zF%`8#)iV@pV=^ks7oygKHK=-?N9D{hR0A%C-GfrUlbxJsh`RB9)GwnvT!y<)75Bet zOhb+RQdEOC;99c#8C1`Q*&gS5BI-rcQTvBwsO#HNLm1w}fkJZ})x$4P*?z&jp{~c{ zyr3g0$By#u@B1K15uVsG>oGS4StJIeQ?mgIL(vmi3U<9vRrA`uE(Jvh*O9D%A} zE-J+PF$T}0dg?9gan^$tsA-vjnjI@pFL(+Sxo=R<6)IvH+ya|YPQlK&0+m}wvApJg z^`fT2=BSsHwY2_*ibVNH)1bbn^9xYZZ!PNjd#H|;DDJVYU>qvK zF(o|Cir5iLY5pg0pc|)Q1m>bDT!VFOasVq+K3>w}EGXwtJ*Zd8V|B$;jKgi%1wX-t zSgW+hS^4^4OUlWp_dI~gnSJQ|-~Tzrfs*Sqs^ulhc$~j#cS9}BD^Lr{bLcEcs0I}- z>v58_2I>X%Tt}hmS%lhZy@VS3Z?Py|K_y{CIgk1NS14yL)W^wOXolJ-EJH0gt5MTw z6QP4?%D~zk+aklMA*n#prI0BELzLNE-n&$`OR?3Sp zfZ_JlJkGyxeh8azBC@*2T92*pG#*8Td`}HianqV+tW!`gUV~bq-^K=5u@)gGX*;4C zGQeww;!bQs`BhXdUB$MV|1q^qMafu)3u{nGw-+^UKX)x#$77A6+zH3yO6-M~P|4T6 zuE+V0ROz^m@{j1u_j(>@1wD$Y*I(Zx=R$0*`M;L~tyJf5IyP=#7NQ4H^Z9GnS`D2R zTO&~oTju%#YHZJ7V~lQOvc5Yim!_iDm9^Lh-*B&2YRr9_|6MrHyi7q=v;^aEH@3i% zO-#jIP~Z6gcEcU0Q2vN|uE7mv2#2|bQA4l=hvHYLA#2~%EL=f!zW>`e(86&NQ?X_< zkMobt?nb>JBF2QS6)IUr;Z)pfkomx&c7dC z6UY3wNR~k@%oxVEG!@;3wYf15mA&s_PqbT^>%CFur=vo=5fy=>sI}kI+C(%CNiu6B zZpLMpjqTcatjF+)Hq8G>8Zi1slUxaHO}6jG{@l={9ZAOZ={SM%?)Dz%|BObp4h#|J zpTIjgf32f=(Vd+<&USqtmf`#njK&jK7=J{qgjeuqEEVo-LRO@U`8dS7cE$$0U;yfY z%&umEx*bbU-iXTj?Wnc<6h6oOKcQB-r@EW$e+Jc%%UB8v_b~H53iVt#hXW;#`cvdr=X11T`I-_c9I6MqOWkS}|9nK5E-g3)umr-mqo& zHa&~N$GH%PK|Fzaa9|(PkV&W(&qO8HV$}7g(2EC9FZdF*j$A=~q-ymw*Kb5E$)ixy zI@2kWe~)vZui7hE6i=aAeg-vmS5XyJ?q|+7N6qIRs4>lQ%d@d2<;SrkzK)92aku;( zs=>da8d&Nk>ec*@5_-nB`8^Wp?lM>0@3 zG86UOR#Zox!Fu=xhSlPW?uGILOaq#rmd=i-v6_RL*UzH9lBX~Zt$`-wEwB;AeyARX zP!akUF2iR~9qBX341o{zt(iNB`LEDF$_YhaKNiCGQA6-C7Qr7-S^X0#gXj@WTFO}j#e8SHrd*Y6KZ*9)WR|Zt78u8fz_z#vk?`U zCs8jthZ^f^sD_msV$PRGHM|bi!p5j?MnBX7G#!=1XTuyQ#ARCh?GfhrUZ{vo!ikuJlkfy;>^qM%&y7MQ^ITl3`M-q&g{bc+ zQ&9?Pg}V)x;!?a9YmGKz{WPjU2T`--gzGufi|jGxb6ySmQXY;Ps&#k=ZbwC+%~;9& zi|0VoA_Y~!eAm^e$n3x-Hh((7Zj^r=XOgqyc(bD!jHfw&2M)n$6O8*&AFYZLO+&_@ zKF6C-EAA(Ff%jWYCV8BH$xy>*l4}<#guh@ij-Jf-n+EJdC0oTr(||gtp0z=(e7#Y* zF&?$>Y(UMfe5{Y}VsZQhy%^zV{%fr3bD)skjG8vds0HGF)B`V|?mLK@|Cdp-Au7p) zvNme$+oPuKNYs5((TfXFNxcR2+;gY~zM91R*NGFHPy@b2RbVBXiYuUc&kp{ z*2XEQTv>!=@gdZGJ5dcfhd)zcVNPya9nx-kKDV>W83U4WWq zPon1UQPhJ!q1N&$Q%wbRQK9XC&G05vgl1xMT#V}J3#i=r02TVLP%C2i3J2<8wNx|4 ztx-cT8TI1ns28t7J@`E8{Ap07;_^|yR1asR_`T*zrZ+JV@+V{kL$hLv7wEa@15g!%Q?k+tAfdXUGpU-`V+YrNKXi+aU!R9@xiHnU#8z1^6M4eU^@Sk5gaVZ!jD0|V)a{+T)66kn#- z=bi2k&GKfZ`BGE8KJN53nj><&88cscx^axRUzRt`H_PiwN}?~`M1K|pg6UpgLPB=P zmoO_PZ?k__WTU!s(zDYh`$IY2u}yfMMdVpTI$Lsrlm zpW#m*(Qi<^H;_per)391dHZLi7HXWt!|J6QN?MSWkfI3bT!t?zg&>KXyn1tfEL7AD z`0{1zD?J@PQL~;eAuDy3w}sa?E!!6gWT$0%)3P(Oh?qAan4T0!&JHnJAFh3^Vz*FY zb|5{drf`uMPfq2^ZR_r<9?Y2Kyv#o{%O6VjrCx7|hI@J0jTQ1Lt-rBYKVL>hYQRrZ zsX{Fa1*a%VzAU$SF;<_{;AEc8^k-@GiT6J~5tG;P(Z-&ZgNC*BrUeuIsot2GX{l~% z1kXAc{$&451z!PC28TkyP)^?F zt)(j#rGKe(eEDPhCNxj;XQc!abKH(Fu)gWOK&m23KSEw#dVpN}Ba!m#muh%MxXGMA z*sl+B@?RqtV<}n4*Xu!cAIbDh*Aof8)WGDB(^_wk;DiE+{>;2m2ZngUG$ApN=?tx@ z#L2k-&p118|I>rYK5u%#*wC>+`gGcm7~^%PtQn*%Z&_uAd0SE-l$jNiH|&*?R z6R*rDM$wu7dHb$Rw0n}tvobjqNM{sjZ$V{~{aG{o)Dp)4C3;)N(i$=((;7zO3z8zm zS@LrC6}KxDne7h+tzpZTb6R&p@i$#B1qJPkVIkm}2b@eWg`w2CD7 zvRTX$vzavs^!bl0$c*{xn`Q(Ovr?QFMd(F&1Fr6{3!7vubuikFU9r^eva_Wf(NdFH#X%kCsG8BoK`CUEsX}j`&=xBb42-_Di&{@rAur3x1?e&$$X`dNJXY8FI zIwS6N2A}oNpP3mT%?sv6uCJK=V%Q{nrZeXAR!picadCk7lps}sf9Nsm>tUW zHoCDzY(a)JcEZUw``?~*@-S=?p6Or~!o;B8WJ!WABa07;`?X+BI&(STPfg4u6PTYI zKa!)p}dsX1G%^LittmcvB(~eipZPn0zs9sD4i;DBv`Om9b?mG?beoaRQ zvzfzA>hhIhFW~e#Rk`@bDsR5VG5J*++4n_s3QlEJnUf`Q+Osn08>a8T9OrOiC>Y4h zan>C6C;HOq3PAPXhyM`n=Ea+){D;>2FCV6dm` z-(WU(>_fSeO=s5ryilOvp-i@)-Q0SdO`syLMXF#6di`pqKRq$XjxvzIN8cAt406|> z_o56=l5cu2)Rg$C4FSJ5JB@^9(Kq#TK;3+AwJ$ZbgkmiE@jo;^#_CH)R38%}Jtf#Y zzuHi{OR<~HKGK`TmUsUKZBWy)1AJUE*e>#&`}ghYVDgxq68Rotr@#j?bN?nH;WM9_ z{f|(4>;7%&G~555Hm~{n{dS$m`h)z=QlF(twNcVuOmE5X zv7@@arOp35-7XuEyFFwVXT;q8hu8^c@G;T|czq0~k8OTRF!zOQyL$e`kX@*7?!MJ_ zjUr~WbAQOPYv#sYwu|Ino?+XyoAsLM%i^o7hn>wW@dyz7ME`VxlgY#?SQZ$Q+{^dd zdtwF#8Bv;<6$&ILbBEc_1{pxY7b@5^u#QakB?fb!US+SU8O#U}Jrd#HlFvMwJ8i99 zA^)b;_D98X1I0Y0yw1lwQNJbVr*^=M4(%};^cbs0?u25V=nDUES4@8NCcCMp)AjUo zHoY|0S&M?+e%cPFE3kfk52a^?XdwIN{dsJV3W6S!-{WDsd!hC>`;!Si9W~ovJ!Lk< z)Aw&+SLTcH_9RM9kh0TsBdKk6!@0L_wp&%-zu1htM%2vRtPr`|OQ#?CU+lO0cpAqS1>>C&3b3Svn4e8vu@j>@#{bdgKN8EA zmCY`DHmN|qX~ty*LTW4({iR!rKCo9c>dO1LaD8X2dE#UMlhD~ZC-_(x*q>Ph_3d zZu`GKSC9@Feo~8to)M3;2L1JI;lI7^Z^NJv{r!yvspG`vzcjDl=D)S6#orqB*L(kc z0%(K(Mj6iyjsDb+!~ma*e{8n4`c2AuYUfrd>seVNIh38D%>So})h2&aSF>SNra#1suW$NI$~CtBUPJESgLaki&W@iC3!8SQGPBdyLX)OeX%*2o z>CfBzKi?fJ_}a5lxf%M$9zQ?2yyr@dh{5dYa``8rRV%ZXVNG=YI%RqgV_m;k#g561 ziuLqt(35>lDnA&MxXzZsY1bd0M`kw9Q zYLedl;>F}vj`PHK8WBv`zvZvloXX$mShDx;;_H{@HiEydu&HsTN(}#N@O+Ar0+}hf zZy&d#@^6jvoc@O&0mMybEcmG-rR57d%i08 zE#SQ6)PjHdH4yysZvnF>%k~!h8b~8cv`x@5Zd%H>DB$y^23Ts`p8{@M|DpP>yzzgl Vzo5USdcI@M8}i@m;8|4Z{{WpZn$!RQ delta 22224 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 K>vV9<4EaA{8;sTf diff --git a/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.po b/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.po index 8d208dcc..e1bde44b 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-13 14:19-0300\n" +"POT-Creation-Date: 2026-08-13 21:55-0300\n" "PO-Revision-Date: 2026-08-08 21:41-0300\n" "Last-Translator: FULL NAME \n" "Language-Team: pt_BR \n" @@ -1820,111 +1820,111 @@ msgstr "" # | msgid "Select the Video for Diagnostics" # | msgid "📹 Select Video for Drawing" # | msgid "Select a video before applying the template." -#: src/zebtrack/core/project/asset_manager.py:108 -#| msgid "Select a video in the list 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/core/project/asset_manager.py:233 +# | 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 mask sidecar is empty." msgid "The template name cannot be empty." msgstr "O nome do template não pode ficar vazio." # | msgid "Select the Video for Diagnostics" # | msgid "📹 Select Video for Drawing" # | msgid "Select a video before applying the template." -#: src/zebtrack/core/project/asset_manager.py:236 -#| msgid "Error 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/core/project/asset_manager.py:239 +#: 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/core/project/asset_manager.py:247 +#: 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 "" "Não é possível salvar o template no projeto atual: projeto não carregado." -#: src/zebtrack/core/project/asset_manager.py:261 +# | msgid "Template '{name}' applied to the video being edited." +#: src/zebtrack/core/project/asset_manager.py:259 #, python-brace-format -#| msgid "Template '{name}' applied to the video being edited." msgid "Template '{name}' already exists." msgstr "Template '{name}' já existe." -#: src/zebtrack/core/project/asset_manager.py:381 +#: 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." # | msgid "The selected video could not be located in the project." # | msgid "Every selected video is already registered in the project." -#: src/zebtrack/core/project/asset_manager.py:439 -#| msgid "No video is currently 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 "Invalid selection" # | msgid "Invalid target" -#: src/zebtrack/core/project/asset_manager.py:453 -#: src/zebtrack/core/project/asset_manager.py:487 -#| msgid "Invalid template" +# | 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." # | msgid "Video not found" -#: src/zebtrack/core/project/asset_manager.py:459 +# | msgid "ROI '{name}' not found." +#: src/zebtrack/core/project/asset_manager.py:453 #, python-brace-format -#| msgid "ROI '{name}' not found." msgid "ROI template '{name}' not found in the project." msgstr "Template de ROI '{name}' não encontrado no projeto." -#: src/zebtrack/core/project/asset_manager.py:474 +#: 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/core/project/asset_manager.py:726 +#: 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." -#: src/zebtrack/core/project/asset_manager.py:736 -#| msgid "Remove the trajectory generated for this video?" +# | 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/core/project/asset_manager.py:737 -#| msgid "Remove every ROI saved for this video?" +# | 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/core/project/asset_manager.py:738 -#| msgid "Remove the trajectory generated for this video?" +# | 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." -#: src/zebtrack/core/project/asset_manager.py:741 +#: 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/core/project/asset_manager.py:745 -#| msgid "There were no unified reports to delete." +# | 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/core/project/asset_manager.py:750 -#| msgid "Remove the reports and summaries associated with this video?" +# | 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/core/project/asset_manager.py:759 +#: src/zebtrack/core/project/asset_manager.py:749 msgid "" "Remove the arena, the ROIs and the trajectory before deleting the video from" " the project." @@ -2038,8 +2038,8 @@ msgstr "" "Por favor, verifique as permissões da pasta e se o caminho é válido." # | msgid "Remove video from project" +# | msgid "No video found in the project" #: src/zebtrack/core/project/project_manager.py:829 -#| msgid "No video found in the project" msgid "Video not found in the project." msgstr "Vídeo não encontrado no projeto." @@ -2056,44 +2056,43 @@ msgstr "" "\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." +# | msgid "Default?" #: src/zebtrack/core/project/project_workflow_service.py:1184 -#| msgid "Default?" msgid "Default" msgstr "Padrão" +# | msgid "Zone saved successfully." #: src/zebtrack/core/project/project_workflow_service.py:1328 -#| msgid "Zone saved successfully." msgid "🎉 Project created successfully!" msgstr "🎉 Projeto criado com sucesso!" +# | msgid "OpenVINO: {status}" #: src/zebtrack/core/project/project_workflow_service.py:1330 -#, python-brace-format -#| msgid "OpenVINO: {status}" msgid "📊 Video status:" msgstr "📊 Status dos vídeos:" # | msgid "Failure: {error}" +# | msgid "❌ Failures: {count}" #: src/zebtrack/core/project/project_workflow_service.py:1331 #, python-brace-format -#| msgid "❌ Failures: {count}" msgid " • Total videos: {count}" msgstr " • Total de vídeos: {count}" # | msgid "- Frames analysed: {count}" +# | msgid "" +# | "\" \"\n" +# | "\" \"✅ Finished: {count}" #: src/zebtrack/core/project/project_workflow_service.py:1332 #, python-brace-format -#| msgid "" -#| "\" \"\n" -#| "\" \"✅ Finished: {count}" msgid " • With arena defined: {count}" msgstr " • Com arena definida: {count}" # | msgid "- Frames analysed: {count}" +# | msgid "" +# | "\" \"\n" +# | "\" \"✅ Finished: {count}" #: src/zebtrack/core/project/project_workflow_service.py:1333 #, python-brace-format -#| msgid "" -#| "\" \"\n" -#| "\" \"✅ Finished: {count}" msgid " • With ROIs defined: {count}" msgstr " • Com ROIs definidas: {count}" @@ -2103,9 +2102,9 @@ msgid " • With trajectory ready: {count}" msgstr " • Com trajetória pronta: {count}" # | msgid "Preparing video processing..." +# | msgid "Recording and Processing Controls" #: src/zebtrack/core/project/project_workflow_service.py:1335 #, python-brace-format -#| msgid "Recording and Processing Controls" msgid " • Pending processing: {count}" msgstr " • Pendentes de processamento: {count}" @@ -2154,17 +2153,17 @@ msgstr " - Clique duas vezes, ou use '{button}', para revisar" msgid "📹 Load Frame from the Selected Video" msgstr "📹 Carregar Frame do Vídeo Selecionado" +# | msgid "Start drawing the arena and the ROIs for this video." #: src/zebtrack/core/project/project_workflow_service.py:1353 -#| msgid "Start drawing the arena and the ROIs for this video." msgid " - Adjust the arena and the ROIs as needed" msgstr " - Ajuste arena e ROIs conforme necessário" # | 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 "Process Pending Videos..." msgid "{step}. Process the pending videos" msgstr "{step}. Processar vídeos pendentes" @@ -2173,8 +2172,8 @@ msgstr "{step}. Processar vídeos pendentes" msgid "Main Control" msgstr "Controle Principal" +# | msgid "Processing Interval (N):" #: src/zebtrack/core/project/project_workflow_service.py:1360 -#| msgid "Processing Interval (N):" msgid " - Confirm the processing intervals" msgstr " - Confirme os intervalos de processamento" @@ -2194,9 +2193,9 @@ msgstr "Processar Vídeos 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 "Error Generating Reports" msgid "{step}. Generate reports" msgstr "{step}. Gerar relatórios" @@ -2209,8 +2208,8 @@ msgstr "Processamento e Relatórios" msgid " - Browse the hierarchy of groups, days and subjects" msgstr " - Navegue pela hierarquia de grupos, dias e sujeitos" +# | msgid "Generating the unified report..." #: src/zebtrack/core/project/project_workflow_service.py:1369 -#| msgid "Generating the unified report..." msgid " - Generate individual or unified reports as needed" msgstr " - Gere relatórios individuais ou unificados conforme necessário" @@ -2222,12 +2221,12 @@ msgstr "💡 Dicas:" msgid " • Use the search box to locate videos quickly" msgstr " • Use a busca para localizar vídeos rapidamente" -#: src/zebtrack/core/project/project_workflow_service.py:1375 +#: 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/core/project/project_workflow_service.py:1377 +#: 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" @@ -2240,9 +2239,8 @@ msgid "Invalid arena: at least 3 points are required." msgstr "Arena inválida: é necessário ao menos 3 pontos." # | msgid "Action unavailable" +# | msgid "{count} ROI(s) available." #: src/zebtrack/core/project/roi_template_manager.py:115 -#, python-brace-format -#| msgid "{count} ROI(s) available." msgid "No ROI available to save." msgstr "Nenhuma ROI disponível para salvar." @@ -2250,34 +2248,34 @@ msgstr "Nenhuma ROI disponível para salvar." 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/core/project/roi_template_manager.py:129 +#: 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/core/project/roi_template_manager.py:155 +#: 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}." # | msgid "Could not connect to the Arduino on port {port}." # | msgid "Could not open the video: {path}" -#: src/zebtrack/core/project/roi_template_manager.py:233 +# | msgid "Could not open: {path}" +#: src/zebtrack/core/project/roi_template_manager.py:230 #, python-brace-format -#| msgid "Could not open: {path}" msgid "Template not found: {path}" msgstr "Template não encontrado: {path}" -#: src/zebtrack/core/project/roi_template_manager.py:253 +# | msgid "Inference failed on frame {frame}: {error}" +#: src/zebtrack/core/project/roi_template_manager.py:249 #, python-brace-format -#| msgid "Inference failed on frame {frame}: {error}" msgid "Invalid JSON in {path}: {error}" msgstr "JSON inválido em {path}: {error}" # | msgid "Invalid selection" # | msgid "Invalid target" -#: src/zebtrack/core/project/roi_template_manager.py:260 +# | msgid "Invalid template" +#: src/zebtrack/core/project/roi_template_manager.py:256 #, python-brace-format -#| msgid "Invalid template" msgid "Invalid template in {path}: {error}" msgstr "Template inválido em {path}: {error}" @@ -2285,8 +2283,8 @@ msgstr "Template inválido em {path}: {error}" # | msgid "Validation finished" # | msgid "✓ Drawing Finished" # | msgid "Processing Finished" +# | msgid "Zones not finalised" #: src/zebtrack/core/project/zone_manager.py:587 -#| msgid "Zones not finalised" msgid "Project data not initialised" msgstr "Dados do projeto não inicializados" @@ -2604,6 +2602,358 @@ msgstr "Configuração da câmera indisponível para iniciar a gravação." msgid "Could not start the recording." msgstr "Não foi possível iniciar a gravação." +#: src/zebtrack/core/services/arduino_ack_semantics.py:84 +#, python-brace-format +msgid "" +"{roi} enter → token {token} → the firmware replied \"{ack}\", but an enter " +"should switch it ON" +msgstr "" +"{roi} entrada → token {token} → o firmware respondeu \"{ack}\", mas uma " +"entrada deveria ligar" + +#: src/zebtrack/core/services/arduino_ack_semantics.py:88 +#, python-brace-format +msgid "" +"{roi} exit → token {token} → the firmware replied \"{ack}\", but an exit " +"should switch it OFF" +msgstr "" +"{roi} saída → token {token} → o firmware respondeu \"{ack}\", mas uma saída " +"deveria desligar" + +#: src/zebtrack/core/services/arduino_bindings.py:54 +#, python-brace-format +msgid "token {token}: enter for {enter_rois} and exit for {exit_rois}" +msgstr "token {token}: entrada de {enter_rois} e saída de {exit_rois}" + +#: src/zebtrack/core/services/detector_service.py:132 +#, python-brace-format +msgid "No {method} model is available for animal detection." +msgstr "Nenhum modelo {method} está disponível para detecção de animais." + +# | msgid "Could not connect to the Arduino on port {port}." +# | msgid "Could not open the video: {path}" +#: src/zebtrack/core/services/detector_service.py:141 +#, python-brace-format +msgid "Could not find the weight matching the path: {path}" +msgstr "Não foi possível encontrar o peso correspondente ao caminho: {path}" + +# | msgid "" "The OpenVINO model directory does not contain the required .xml +# | files. " "Please reconvert the model." +#: src/zebtrack/core/services/detector_service.py:156 +msgid "" +"OpenVINO model path not found or invalid. Please convert the model first." +msgstr "" +"Caminho do modelo OpenVINO não encontrado ou inválido. Por favor, converta o" +" modelo primeiro." + +#: src/zebtrack/core/services/detector_service.py:165 +msgid "YOLO .pt model path not found or invalid." +msgstr "Caminho do modelo YOLO .pt não encontrado ou inválido." + +# | msgid "Error Saving to Project" +# | msgid "Save to Project" +#: src/zebtrack/core/services/model_override_service.py:158 +#: src/zebtrack/core/services/model_override_service.py:354 +#: src/zebtrack/ui/components/dialog_manager.py:444 src/zebtrack/ui/gui.py:576 +msgid "No Project" +msgstr "Nenhum Projeto" + +# | msgid "Open a project before adjusting the specific calibration." +#: src/zebtrack/core/services/model_override_service.py:159 +msgid "Open a project before copying the global settings." +msgstr "Abra um projeto antes de copiar configurações globais." + +# | msgid "The detector settings were saved for this project." +# | msgid "The current global settings have been copied into this project." +#: src/zebtrack/core/services/model_override_service.py:195 +msgid "Global settings applied to the project." +msgstr "Configurações globais aplicadas ao projeto." + +# | msgid "The detector settings were saved for this project." +# | msgid "The current global settings have been copied into this project." +#: src/zebtrack/core/services/model_override_service.py:312 +#, python-brace-format +msgid "Global settings applied to the project at {path}." +msgstr "Configurações globais aplicadas ao projeto em {path}." + +# | msgid "Open a project before adjusting the specific calibration." +#: src/zebtrack/core/services/model_override_service.py:355 +msgid "Open a project before saving calibration overrides." +msgstr "Abra um projeto antes de salvar overrides de calibração." + +#: src/zebtrack/core/services/model_override_service.py:372 +msgid "Project overrides updated from this calibration." +msgstr "Overrides do projeto atualizados a partir desta calibração." + +# | msgid "No video selected" +# | msgid "No active weight selected." +#: src/zebtrack/core/services/model_service.py:115 +msgid "No weight selected." +msgstr "Nenhum peso selecionado." + +# | msgid "Video not found" +# | msgid "Weight not found" +#: src/zebtrack/core/services/model_service.py:119 +msgid "Weight details not found." +msgstr "Detalhes do peso não encontrados." + +#: src/zebtrack/core/services/model_service.py:124 +msgid "The OpenVINO model is ready." +msgstr "O modelo OpenVINO está pronto." + +# | msgid "Conversion to OpenVINO failed." +#: src/zebtrack/core/services/model_service.py:126 +msgid "Conversion to OpenVINO is required." +msgstr "Necessita de conversão para OpenVINO." + +# | msgid "OpenVINO caches deleted" +#: src/zebtrack/core/services/model_service.py:128 +msgid "OpenVINO is disabled." +msgstr "O OpenVINO está desativado." + +# | msgid "Video not found" +# | msgid "ROI '{name}' not found." +# | msgid "ROI template '{name}' not found in the project." +#: src/zebtrack/core/services/model_service.py:491 +#, python-brace-format +msgid "Weight '{name}' not found in the configuration." +msgstr "Peso '{name}' não encontrado na configuração." + +# | msgid "Could not save config.local.yaml: {error}" +# | msgid "Could not save the file: {error}" +#: src/zebtrack/core/services/weight_manager.py:672 +#, python-brace-format +msgid "Could not save the weights configuration file: {error}" +msgstr "Não foi possível salvar o arquivo de configuração de pesos: {error}" + +#: src/zebtrack/core/services/weight_manager.py:875 +#, python-brace-format +msgid "" +"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." +msgstr "" +"Um arquivo de peso com o nome '{name}' já existe no diretório de pesos.\n" +"\n" +"Arquivo existente: {existing}\n" +"Arquivo sendo adicionado: {incoming}\n" +"\n" +"Por favor, renomeie um dos arquivos antes de adicionar." + +# | msgid "Failed to update the project settings: {error}" +# | msgid "Failed to start the recording: {error}" +# | msgid "Failed to start the analysis: {error}" +#: src/zebtrack/core/services/weight_manager.py:897 +#, python-brace-format +msgid "Failed to copy the external weight file: {error}" +msgstr "Falha ao copiar o arquivo de peso externo: {error}" + +# | msgid "Could not connect to the Arduino on port {port}." +# | msgid "Could not open the video: {path}" +# | msgid "Could not open: {path}" +# | msgid "Template not found: {path}" +#: src/zebtrack/core/services/weight_manager.py:902 +#, python-brace-format +msgid "The model file was not found: {path}" +msgstr "O arquivo de modelo não foi encontrado: {path}" + +#: src/zebtrack/core/services/weight_manager.py:907 +#, python-brace-format +msgid "The model path is invalid or inaccessible: {error}" +msgstr "O caminho do modelo é inválido ou inacessível: {error}" + +# | msgid "Template '{name}' applied to the video being edited." +# | msgid "Template '{name}' already exists." +#: src/zebtrack/core/services/weight_manager.py:914 +#, python-brace-format +msgid "A weight named '{name}' already exists." +msgstr "Um peso com o nome '{name}' já existe." + +# | msgid "Video not found" +# | msgid "ROI '{name}' not found." +#: src/zebtrack/core/services/weight_manager.py:979 +#, python-brace-format +msgid "Weight '{name}' not found." +msgstr "Peso '{name}' não encontrado." + +#: src/zebtrack/core/services/weight_manager.py:984 +msgid "You cannot delete the last available weight." +msgstr "Você não pode excluir o último peso disponível." + +#: src/zebtrack/core/services/weight_manager.py:1480 +msgid "OpenVINO model .xml file not found after the conversion." +msgstr "Arquivo .xml do modelo OpenVINO não encontrado após a conversão." + +#: src/zebtrack/core/services/weight_manager.py:1621 +msgid "INT8 model .xml file not found after the conversion." +msgstr "Arquivo .xml do modelo INT8 não encontrado após conversão." + +# | msgid "Live camera (idx {index})" +#: src/zebtrack/core/services/wizard_service.py:69 +msgid "Invalid camera index" +msgstr "Índice de câmera inválido" + +# | msgid "The {label} must be between 0 and 1." +# | msgid "The number of zones must be between 2 and 10" +#: src/zebtrack/core/services/wizard_service.py:72 +msgid "The camera index must be between 0 and 10" +msgstr "Índice de câmera deve estar entre 0 e 10" + +#: src/zebtrack/core/services/wizard_service.py:80 +msgid "The Arduino port must be specified when Arduino is enabled" +msgstr "Porta Arduino deve ser especificada quando Arduino está ativado" + +# | msgid "External trigger mode requires a configured Arduino." +#: src/zebtrack/core/services/wizard_service.py:88 +msgid "External trigger mode requires Arduino to be enabled" +msgstr "Modo de trigger externo requer Arduino ativado" + +# | msgid "The processing interval must be greater than 0." +#: src/zebtrack/core/services/wizard_service.py:95 +msgid "The recording duration must be greater than zero" +msgstr "Duração de gravação deve ser maior que zero" + +#: src/zebtrack/core/services/wizard_service.py:99 +msgid "The recording duration cannot exceed 2 hours (7200 seconds)" +msgstr "Duração de gravação não pode exceder 2 horas (7200 segundos)" + +# | msgid "The {label} must be between 0 and 1." +# | msgid "The number of zones must be between 2 and 10" +#: src/zebtrack/core/services/wizard_service.py:106 +msgid "The countdown must be between 1 and 60 seconds" +msgstr "Contagem regressiva deve estar entre 1 e 60 segundos" + +#: src/zebtrack/core/services/wizard_service.py:376 +#, python-brace-format +msgid "{name} [index {index}] - {resolution}" +msgstr "{name} [índice {index}] - {resolution}" + +#: src/zebtrack/core/services/wizard_service.py:383 +#, python-brace-format +msgid "Camera #{number} [index {index}] - {resolution}" +msgstr "Câmera #{number} [índice {index}] - {resolution}" + +# | msgid "The {label} must be between 0 and 1." +# | msgid "The number of zones must be between 2 and 10" +#: src/zebtrack/core/services/wizard_service.py:680 +msgid "The number of days must be between 1 and 365" +msgstr "Número de dias deve estar entre 1 e 365" + +# | msgid "The {label} must be between 0 and 1." +# | msgid "The number of zones must be between 2 and 10" +#: src/zebtrack/core/services/wizard_service.py:685 +msgid "The number of groups must be between 1 and 6" +msgstr "Número de grupos deve estar entre 1 e 6" + +# | msgid "The {label} must be between 0 and 1." +# | msgid "The number of zones must be between 2 and 10" +#: src/zebtrack/core/services/wizard_service.py:731 +msgid "The number of aquariums must be between 1 and 100" +msgstr "Número de aquários deve estar entre 1 e 100" + +# | msgid "The {label} must be between 0 and 1." +# | msgid "The number of zones must be between 2 and 10" +#: src/zebtrack/core/services/wizard_service.py:738 +msgid "The number of animals per aquarium must be between 1 and 100" +msgstr "Número de animais por aquário deve estar entre 1 e 100" + +# | msgid "FPS must be greater than 0." +# | msgid "Maximum Distance must be greater than 0." +#: src/zebtrack/core/services/wizard_service.py:744 +#: src/zebtrack/core/services/wizard_service.py:824 +msgid "The aquarium width must be greater than zero" +msgstr "Largura do aquário deve ser maior que zero" + +# | msgid "FPS must be greater than 0." +# | msgid "Maximum Distance must be greater than 0." +#: src/zebtrack/core/services/wizard_service.py:748 +#: src/zebtrack/core/services/wizard_service.py:828 +msgid "The aquarium height must be greater than zero" +msgstr "Altura do aquário deve ser maior que zero" + +# | msgid "The {label} must be between 0 and 1." +# | msgid "Thigmotaxis distance must be between 0.1 and 10.0 cm" +#: src/zebtrack/core/services/wizard_service.py:757 +msgid "The analysis interval must be between 1 and 30 frames" +msgstr "Intervalo de análise deve estar entre 1 e 30 frames" + +# | msgid "The processing interval must be greater than 0." +# | msgid "The display interval must be greater than 0." +#: src/zebtrack/core/services/wizard_service.py:761 +msgid "The display interval must be between 1 and 30 frames" +msgstr "Intervalo de exibição deve estar entre 1 e 30 frames" + +#: src/zebtrack/core/services/wizard_service.py:774 +#, python-brace-format +msgid "Invalid ROI inclusion rule. It must be one of: {rules}" +msgstr "Regra de inclusão ROI inválida. Deve ser uma de: {rules}" + +# | msgid "The number of frames must be a whole number." +#: src/zebtrack/core/services/wizard_service.py:802 +msgid "The number of aquariums must be at least 1" +msgstr "O número de aquários deve ser pelo menos 1" + +#: src/zebtrack/core/services/wizard_service.py:805 +msgid "The number of aquariums cannot exceed 100" +msgstr "O número de aquários não pode exceder 100" + +# | msgid "The number of frames must be a whole number." +#: src/zebtrack/core/services/wizard_service.py:812 +msgid "The number of animals per aquarium must be at least 1" +msgstr "O número de animais por aquário deve ser pelo menos 1" + +#: src/zebtrack/core/services/wizard_service.py:818 +msgid "The number of animals per aquarium cannot exceed 100" +msgstr "O número de animais por aquário não pode exceder 100" + +# | msgid "Invalid Configuration" +#: src/zebtrack/core/services/wizard_service.py:874 +msgid "Invalid multi-aquarium configuration" +msgstr "Configuração multi-aquário inválida" + +#: src/zebtrack/core/services/wizard_service.py:896 +msgid "Exactly 2 aquariums must be configured" +msgstr "Exatamente 2 aquários devem ser configurados" + +#: src/zebtrack/core/services/wizard_service.py:920 +#, python-brace-format +msgid "" +"Aquarium {index} has a very small area ({area} px²). It may affect detection" +" accuracy." +msgstr "" +"Aquário {index} tem área muito pequena ({area} px²). Pode afetar precisão da" +" detecção." + +#: src/zebtrack/core/services/wizard_service.py:949 +msgid "" +"The aquarium polygons appear to overlap. This may cause duplicate " +"detections." +msgstr "" +"Polígonos dos aquários parecem se sobrepor. Isso pode causar detecções " +"duplicadas." + +#: src/zebtrack/core/services/wizard_service.py:965 +#, python-brace-format +msgid "The regex does not capture the expected fields: {fields}" +msgstr "Regex não captura campos esperados: {fields}" + +#: src/zebtrack/core/services/wizard_service.py:980 +#, python-brace-format +msgid "The regex does not match these files: {files}" +msgstr "Regex não corresponde a arquivos: {files}" + +# | msgid "Invalid selection" +# | msgid "Invalid target" +# | msgid "Invalid template" +# | msgid "Invalid template in {path}: {error}" +#: src/zebtrack/core/services/wizard_service.py:987 +#, python-brace-format +msgid "Invalid regex pattern: {error}" +msgstr "Padrão regex inválido: {error}" + #: src/zebtrack/io/arduino_manager.py:140 #, python-brace-format msgid "Could not connect to the Arduino on port {port}." @@ -2661,12 +3011,6 @@ msgstr "Nenhuma tarefa em andamento." msgid "Analysis configuration: {value}" msgstr "Configuração de análise: {value}" -# | msgid "Error Saving to Project" -# | msgid "Save to Project" -#: src/zebtrack/ui/components/dialog_manager.py:444 src/zebtrack/ui/gui.py:576 -msgid "No Project" -msgstr "Nenhum Projeto" - #: src/zebtrack/ui/components/dialog_manager.py:445 src/zebtrack/ui/gui.py:577 msgid "Open a project before adjusting the specific calibration." msgstr "Abra um projeto antes de ajustar a calibração específica." diff --git a/src/zebtrack/locales/zebtrack.pot b/src/zebtrack/locales/zebtrack.pot index 0966aa66..a1e3ddb0 100644 --- a/src/zebtrack/locales/zebtrack.pot +++ b/src/zebtrack/locales/zebtrack.pot @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: DRerio LogAI VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-08-13 14:19-0300\n" +"POT-Creation-Date: 2026-08-13 21:55-0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -1452,89 +1452,89 @@ msgstr "" msgid "Recommended, but the model is not converted. Use 'Diagnostics' to convert." msgstr "" -#: src/zebtrack/core/project/asset_manager.py:108 +#: src/zebtrack/core/project/asset_manager.py:107 msgid "Project not initialised for saving ROI templates." msgstr "" -#: src/zebtrack/core/project/asset_manager.py:233 +#: 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 "" -#: src/zebtrack/core/project/asset_manager.py:236 +#: src/zebtrack/core/project/asset_manager.py:234 msgid "Invalid zone data for saving the template." msgstr "" -#: src/zebtrack/core/project/asset_manager.py:239 +#: 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 "" -#: src/zebtrack/core/project/asset_manager.py:247 +#: 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 "" -#: src/zebtrack/core/project/asset_manager.py:261 +#: src/zebtrack/core/project/asset_manager.py:259 #, python-brace-format msgid "Template '{name}' already exists." msgstr "" -#: src/zebtrack/core/project/asset_manager.py:381 +#: src/zebtrack/core/project/asset_manager.py:378 msgid "Invalid template file: the 'data' block is missing." msgstr "" -#: src/zebtrack/core/project/asset_manager.py:439 +#: src/zebtrack/core/project/asset_manager.py:434 msgid "The template file is not registered in the project." msgstr "" -#: src/zebtrack/core/project/asset_manager.py:453 -#: src/zebtrack/core/project/asset_manager.py:487 +#: src/zebtrack/core/project/asset_manager.py:447 +#: src/zebtrack/core/project/asset_manager.py:479 msgid "Invalid template content." msgstr "" -#: src/zebtrack/core/project/asset_manager.py:459 +#: src/zebtrack/core/project/asset_manager.py:453 #, python-brace-format msgid "ROI template '{name}' not found in the project." msgstr "" -#: src/zebtrack/core/project/asset_manager.py:474 +#: src/zebtrack/core/project/asset_manager.py:468 #, python-brace-format msgid "ROI template '{name}' not found for the requested context." msgstr "" -#: src/zebtrack/core/project/asset_manager.py:726 +#: src/zebtrack/core/project/asset_manager.py:718 msgid "" "Remove the reports and summaries before deleting the arena, the ROIs or " "the trajectory." msgstr "" -#: src/zebtrack/core/project/asset_manager.py:736 +#: src/zebtrack/core/project/asset_manager.py:728 msgid "There is no arena recorded for this video." msgstr "" -#: src/zebtrack/core/project/asset_manager.py:737 +#: src/zebtrack/core/project/asset_manager.py:729 msgid "There are no ROIs recorded for this video." msgstr "" -#: src/zebtrack/core/project/asset_manager.py:738 +#: src/zebtrack/core/project/asset_manager.py:730 msgid "There is no trajectory recorded for this video." msgstr "" -#: src/zebtrack/core/project/asset_manager.py:741 +#: src/zebtrack/core/project/asset_manager.py:733 #, python-brace-format msgid "There is no {asset} recorded for this video." msgstr "" -#: src/zebtrack/core/project/asset_manager.py:745 +#: src/zebtrack/core/project/asset_manager.py:737 msgid "There are no reports or summaries to remove." msgstr "" -#: src/zebtrack/core/project/asset_manager.py:750 +#: src/zebtrack/core/project/asset_manager.py:741 msgid "Remove the reports and summaries before deleting the video." msgstr "" -#: src/zebtrack/core/project/asset_manager.py:759 +#: src/zebtrack/core/project/asset_manager.py:749 msgid "" "Remove the arena, the ROIs and the trajectory before deleting the video " "from the project." @@ -1761,11 +1761,11 @@ msgstr "" msgid " • Use the search box to locate videos quickly" msgstr "" -#: src/zebtrack/core/project/project_workflow_service.py:1375 +#: src/zebtrack/core/project/project_workflow_service.py:1374 msgid " • The status symbols show which arenas, ROIs and trajectories exist" msgstr "" -#: src/zebtrack/core/project/project_workflow_service.py:1377 +#: src/zebtrack/core/project/project_workflow_service.py:1375 msgid " • Adjust the zones before processing if necessary" msgstr "" @@ -1785,26 +1785,26 @@ msgstr "" msgid "A project path is required to save a template into the project." msgstr "" -#: src/zebtrack/core/project/roi_template_manager.py:129 +#: src/zebtrack/core/project/roi_template_manager.py:128 msgid "A custom path is required for save_location='custom'." msgstr "" -#: src/zebtrack/core/project/roi_template_manager.py:155 +#: src/zebtrack/core/project/roi_template_manager.py:153 #, python-brace-format msgid "Template '{name}' already exists in {directory}." msgstr "" -#: src/zebtrack/core/project/roi_template_manager.py:233 +#: src/zebtrack/core/project/roi_template_manager.py:230 #, python-brace-format msgid "Template not found: {path}" msgstr "" -#: src/zebtrack/core/project/roi_template_manager.py:253 +#: src/zebtrack/core/project/roi_template_manager.py:249 #, python-brace-format msgid "Invalid JSON in {path}: {error}" msgstr "" -#: src/zebtrack/core/project/roi_template_manager.py:260 +#: src/zebtrack/core/project/roi_template_manager.py:256 #, python-brace-format msgid "Invalid template in {path}: {error}" msgstr "" @@ -2050,6 +2050,277 @@ msgstr "" msgid "Could not start the recording." msgstr "" +#: src/zebtrack/core/services/arduino_ack_semantics.py:84 +#, python-brace-format +msgid "" +"{roi} enter → token {token} → the firmware replied \"{ack}\", but an " +"enter should switch it ON" +msgstr "" + +#: src/zebtrack/core/services/arduino_ack_semantics.py:88 +#, python-brace-format +msgid "" +"{roi} exit → token {token} → the firmware replied \"{ack}\", but an exit " +"should switch it OFF" +msgstr "" + +#: src/zebtrack/core/services/arduino_bindings.py:54 +#, python-brace-format +msgid "token {token}: enter for {enter_rois} and exit for {exit_rois}" +msgstr "" + +#: src/zebtrack/core/services/detector_service.py:132 +#, python-brace-format +msgid "No {method} model is available for animal detection." +msgstr "" + +#: src/zebtrack/core/services/detector_service.py:141 +#, python-brace-format +msgid "Could not find the weight matching the path: {path}" +msgstr "" + +#: src/zebtrack/core/services/detector_service.py:156 +msgid "OpenVINO model path not found or invalid. Please convert the model first." +msgstr "" + +#: src/zebtrack/core/services/detector_service.py:165 +msgid "YOLO .pt model path not found or invalid." +msgstr "" + +#: src/zebtrack/core/services/model_override_service.py:158 +#: src/zebtrack/core/services/model_override_service.py:354 +#: src/zebtrack/ui/components/dialog_manager.py:444 src/zebtrack/ui/gui.py:576 +msgid "No Project" +msgstr "" + +#: src/zebtrack/core/services/model_override_service.py:159 +msgid "Open a project before copying the global settings." +msgstr "" + +#: src/zebtrack/core/services/model_override_service.py:195 +msgid "Global settings applied to the project." +msgstr "" + +#: src/zebtrack/core/services/model_override_service.py:312 +#, python-brace-format +msgid "Global settings applied to the project at {path}." +msgstr "" + +#: src/zebtrack/core/services/model_override_service.py:355 +msgid "Open a project before saving calibration overrides." +msgstr "" + +#: src/zebtrack/core/services/model_override_service.py:372 +msgid "Project overrides updated from this calibration." +msgstr "" + +#: src/zebtrack/core/services/model_service.py:115 +msgid "No weight selected." +msgstr "" + +#: src/zebtrack/core/services/model_service.py:119 +msgid "Weight details not found." +msgstr "" + +#: src/zebtrack/core/services/model_service.py:124 +msgid "The OpenVINO model is ready." +msgstr "" + +#: src/zebtrack/core/services/model_service.py:126 +msgid "Conversion to OpenVINO is required." +msgstr "" + +#: src/zebtrack/core/services/model_service.py:128 +msgid "OpenVINO is disabled." +msgstr "" + +#: src/zebtrack/core/services/model_service.py:491 +#, python-brace-format +msgid "Weight '{name}' not found in the configuration." +msgstr "" + +#: src/zebtrack/core/services/weight_manager.py:672 +#, python-brace-format +msgid "Could not save the weights configuration file: {error}" +msgstr "" + +#: src/zebtrack/core/services/weight_manager.py:875 +#, python-brace-format +msgid "" +"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." +msgstr "" + +#: src/zebtrack/core/services/weight_manager.py:897 +#, python-brace-format +msgid "Failed to copy the external weight file: {error}" +msgstr "" + +#: src/zebtrack/core/services/weight_manager.py:902 +#, python-brace-format +msgid "The model file was not found: {path}" +msgstr "" + +#: src/zebtrack/core/services/weight_manager.py:907 +#, python-brace-format +msgid "The model path is invalid or inaccessible: {error}" +msgstr "" + +#: src/zebtrack/core/services/weight_manager.py:914 +#, python-brace-format +msgid "A weight named '{name}' already exists." +msgstr "" + +#: src/zebtrack/core/services/weight_manager.py:979 +#, python-brace-format +msgid "Weight '{name}' not found." +msgstr "" + +#: src/zebtrack/core/services/weight_manager.py:984 +msgid "You cannot delete the last available weight." +msgstr "" + +#: src/zebtrack/core/services/weight_manager.py:1480 +msgid "OpenVINO model .xml file not found after the conversion." +msgstr "" + +#: src/zebtrack/core/services/weight_manager.py:1621 +msgid "INT8 model .xml file not found after the conversion." +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:69 +msgid "Invalid camera index" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:72 +msgid "The camera index must be between 0 and 10" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:80 +msgid "The Arduino port must be specified when Arduino is enabled" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:88 +msgid "External trigger mode requires Arduino to be enabled" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:95 +msgid "The recording duration must be greater than zero" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:99 +msgid "The recording duration cannot exceed 2 hours (7200 seconds)" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:106 +msgid "The countdown must be between 1 and 60 seconds" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:376 +#, python-brace-format +msgid "{name} [index {index}] - {resolution}" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:383 +#, python-brace-format +msgid "Camera #{number} [index {index}] - {resolution}" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:680 +msgid "The number of days must be between 1 and 365" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:685 +msgid "The number of groups must be between 1 and 6" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:731 +msgid "The number of aquariums must be between 1 and 100" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:738 +msgid "The number of animals per aquarium must be between 1 and 100" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:744 +#: src/zebtrack/core/services/wizard_service.py:824 +msgid "The aquarium width must be greater than zero" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:748 +#: src/zebtrack/core/services/wizard_service.py:828 +msgid "The aquarium height must be greater than zero" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:757 +msgid "The analysis interval must be between 1 and 30 frames" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:761 +msgid "The display interval must be between 1 and 30 frames" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:774 +#, python-brace-format +msgid "Invalid ROI inclusion rule. It must be one of: {rules}" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:802 +msgid "The number of aquariums must be at least 1" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:805 +msgid "The number of aquariums cannot exceed 100" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:812 +msgid "The number of animals per aquarium must be at least 1" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:818 +msgid "The number of animals per aquarium cannot exceed 100" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:874 +msgid "Invalid multi-aquarium configuration" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:896 +msgid "Exactly 2 aquariums must be configured" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:920 +#, python-brace-format +msgid "" +"Aquarium {index} has a very small area ({area} px²). It may affect " +"detection accuracy." +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:949 +msgid "" +"The aquarium polygons appear to overlap. This may cause duplicate " +"detections." +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:965 +#, python-brace-format +msgid "The regex does not capture the expected fields: {fields}" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:980 +#, python-brace-format +msgid "The regex does not match these files: {files}" +msgstr "" + +#: src/zebtrack/core/services/wizard_service.py:987 +#, python-brace-format +msgid "Invalid regex pattern: {error}" +msgstr "" + #: src/zebtrack/io/arduino_manager.py:140 #, python-brace-format msgid "Could not connect to the Arduino on port {port}." @@ -2102,10 +2373,6 @@ msgstr "" msgid "Analysis configuration: {value}" msgstr "" -#: src/zebtrack/ui/components/dialog_manager.py:444 src/zebtrack/ui/gui.py:576 -msgid "No Project" -msgstr "" - #: src/zebtrack/ui/components/dialog_manager.py:445 src/zebtrack/ui/gui.py:577 msgid "Open a project before adjusting the specific calibration." msgstr "" diff --git a/tests/core/services/test_wizard_service_camera_friendly_names.py b/tests/core/services/test_wizard_service_camera_friendly_names.py index e0e4c4d3..203e4d46 100644 --- a/tests/core/services/test_wizard_service_camera_friendly_names.py +++ b/tests/core/services/test_wizard_service_camera_friendly_names.py @@ -71,8 +71,8 @@ def test_fallback_when_pygrabber_unavailable(self, mock_names, mock_vc): assert len(cameras) == 2 assert cameras[0]["friendly_name"] == "" - assert "Câmera #1" in cameras[0]["description"] - assert "Câmera #2" in cameras[1]["description"] + assert "Camera #1" in cameras[0]["description"] + assert "Camera #2" in cameras[1]["description"] def test_dedupe_identical_dshow_names(self): """Two devices with identical DirectShow names get (#2), (#3) suffixes.""" diff --git a/tests/core/test_arduino_ack_semantics.py b/tests/core/test_arduino_ack_semantics.py index 180b13c6..2b58601e 100644 --- a/tests/core/test_arduino_ack_semantics.py +++ b/tests/core/test_arduino_ack_semantics.py @@ -111,4 +111,4 @@ def test_describe_inversion_names_roi_edge_and_token(): assert "Z4" in text assert "4" in text assert "Blue LED OFF" in text - assert "ligar" in text + assert "switch it ON" in text diff --git a/tests/core/test_detector_service.py b/tests/core/test_detector_service.py index 7f6418ef..4e28ab91 100644 --- a/tests/core/test_detector_service.py +++ b/tests/core/test_detector_service.py @@ -146,7 +146,7 @@ def test_initialize_detector_no_model_path(self): self.assertFalse(success) self.assertIsNotNone(error) assert error is not None - self.assertIn("Nenhum modelo", error) + self.assertIn("No det model is available", error) def test_initialize_detector_weight_not_found(self): """Test detector initialization fails when weight not found.""" @@ -165,7 +165,7 @@ def test_initialize_detector_weight_not_found(self): self.assertFalse(success) self.assertIsNotNone(error) assert error is not None - self.assertIn("peso correspondente", error) + self.assertIn("weight matching the path", error) @patch("zebtrack.core.services.detector_service.Detector") def test_initialize_detector_with_openvino(self, mock_detector_class): diff --git a/tests/core/test_weight_manager.py b/tests/core/test_weight_manager.py index 721effea..cfad3697 100644 --- a/tests/core/test_weight_manager.py +++ b/tests/core/test_weight_manager.py @@ -127,7 +127,7 @@ def test_wm_cannot_delete_last_weight(wm_setup): # Attempt to delete the last weight - should raise ValueError import pytest - with pytest.raises(ValueError, match="não pode excluir o último peso"): + with pytest.raises(ValueError, match="cannot delete the last available weight"): manager.delete_weight("default_weight.pt") # Check that it was not deleted diff --git a/tests/core/test_wizard_service_multi_aquarium.py b/tests/core/test_wizard_service_multi_aquarium.py index 19fae838..80447575 100644 --- a/tests/core/test_wizard_service_multi_aquarium.py +++ b/tests/core/test_wizard_service_multi_aquarium.py @@ -56,7 +56,7 @@ def test_validate_missing_aquariums(self): is_valid, errors, _ = WizardService.validate_multi_aquarium_config(config) assert is_valid is False - assert any("2 aquários" in e for e in errors) + assert any("2 aquariums" in e for e in errors) def test_validate_only_one_aquarium(self): """Test validation fails with only one aquarium.""" @@ -72,7 +72,7 @@ def test_validate_only_one_aquarium(self): is_valid, errors, _ = WizardService.validate_multi_aquarium_config(config) assert is_valid is False - assert any("2 aquários" in e for e in errors) + assert any("2 aquariums" in e for e in errors) def test_validate_invalid_regex_pattern(self): """Test validation fails for invalid regex.""" @@ -91,7 +91,7 @@ def test_validate_invalid_regex_pattern(self): is_valid, errors, _ = WizardService.validate_multi_aquarium_config(config) assert is_valid is False - assert any("regex inválido" in e.lower() for e in errors) + assert any("invalid regex pattern" in e.lower() for e in errors) def test_validate_regex_missing_groups(self): """Test validation fails when regex doesn't capture expected fields.""" @@ -110,7 +110,7 @@ def test_validate_regex_missing_groups(self): is_valid, errors, _ = WizardService.validate_multi_aquarium_config(config) assert is_valid is False - assert any("não captura" in e for e in errors) + assert any("does not capture" in e for e in errors) def test_validate_regex_with_correct_groups(self): """Test validation passes when regex has correct groups.""" @@ -170,7 +170,7 @@ def test_validate_regex_against_non_matching_filenames(self): is_valid, errors, _ = WizardService.validate_multi_aquarium_config(config, sample_filenames) assert is_valid is False - assert any("não corresponde" in e for e in errors) + assert any("does not match" in e for e in errors) def test_validate_with_empty_regex(self): """Test validation passes with empty regex pattern.""" @@ -209,7 +209,7 @@ def test_validate_partial_regex_match(self): # Should fail because Treatment doesn't match assert is_valid is False - assert any("não corresponde" in e for e in errors) + assert any("does not match" in e for e in errors) class TestValidateMultiAquariumConfigEdgeCases: @@ -240,7 +240,7 @@ def test_validate_dict_enabled_missing_aquariums(self): is_valid, errors, _ = WizardService.validate_multi_aquarium_config(config) assert is_valid is False - assert any("2 aquários" in e for e in errors) + assert any("2 aquariums" in e for e in errors) def test_validate_with_none_sample_filenames(self): """Test validation works with None sample filenames.""" diff --git a/tests/i18n/test_no_untranslated_literals.py b/tests/i18n/test_no_untranslated_literals.py index ce485d2e..0b7029bb 100644 --- a/tests/i18n/test_no_untranslated_literals.py +++ b/tests/i18n/test_no_untranslated_literals.py @@ -34,6 +34,7 @@ "src/zebtrack/core/application_bootstrapper.py", "src/zebtrack/core/project", "src/zebtrack/core/recording", + "src/zebtrack/core/services", "src/zebtrack/i18n.py", "src/zebtrack/io", "src/zebtrack/plugins", diff --git a/tests/test_class_names_validation.py b/tests/test_class_names_validation.py index 44ae973c..279ccfc0 100644 --- a/tests/test_class_names_validation.py +++ b/tests/test_class_names_validation.py @@ -286,7 +286,7 @@ def test_inspect_model_not_found(self): model_service = ModelService(weight_manager) # Try to inspect non-existent model - with pytest.raises(ValueError, match="não encontrado"): + with pytest.raises(ValueError, match="not found in the configuration"): model_service.inspect_model("nonexistent.pt") diff --git a/tests/ui/wizard/test_calibration_step.py b/tests/ui/wizard/test_calibration_step.py index b0c82f26..cea1aefe 100644 --- a/tests/ui/wizard/test_calibration_step.py +++ b/tests/ui/wizard/test_calibration_step.py @@ -141,7 +141,7 @@ def test_validation_fails_with_zero_aquariums(self): is_valid, error_message = step.validate() assert not is_valid - assert "aquários" in error_message.lower() or "pelo menos 1" in error_message.lower() + assert "aquariums" in error_message.lower() or "at least 1" in error_message.lower() def test_validation_fails_with_zero_animals(self): """Validation should fail when animals_per_aquarium is 0.""" @@ -157,7 +157,7 @@ def test_validation_fails_with_zero_animals(self): is_valid, error_message = step.validate() assert not is_valid - assert "animais" in error_message.lower() or "pelo menos 1" in error_message.lower() + assert "animals" in error_message.lower() or "at least 1" in error_message.lower() def test_validation_fails_with_negative_dimensions(self): """Validation should fail with negative dimensions.""" @@ -173,7 +173,7 @@ def test_validation_fails_with_negative_dimensions(self): is_valid, error_message = step.validate() assert not is_valid - assert "largura" in error_message.lower() or "positiv" in error_message.lower() + assert "width" in error_message.lower() or "greater than zero" in error_message.lower() def test_validation_fails_with_zero_height(self): """Validation should fail when height is zero.""" @@ -189,7 +189,7 @@ def test_validation_fails_with_zero_height(self): is_valid, error_message = step.validate() assert not is_valid - assert "altura" in error_message.lower() or "maior que zero" in error_message.lower() + assert "height" in error_message.lower() or "greater than zero" in error_message.lower() def test_on_show_restores_from_wizard_data(self): """on_show should restore values from wizard_data.""" From 28ca1bb9b707a00ce6818ca8def6346d5aee9a1a Mon Sep 17 00:00:00 2001 From: MarkSant Date: Thu, 13 Aug 2026 22:27:58 -0300 Subject: [PATCH 07/21] feat(i18n): fechar core/** (fase 3, lote 3d) `core/detection`, `core/video` e `core/viewmodels` a zero. Com isso o pacote `core` inteiro esta migrado e as CINCO entradas que o ratchet acumulava (app_runner.py, application_bootstrapper.py, project, recording, services) colapsam em `src/zebtrack/core` -- qualquer arquivo novo sob `core/` passa a nascer coberto. DUAS REDACOES DA MESMA VALIDACAO viraram uma. `analysis_control_view_model` tinha a sua propria versao da checagem det/seg que `project_workflow_service` ja fazia, com texto diferente: pws: "O modo de deteccao (det) para animais so e compativel com 1 animal por aquario. Configuracao atual: N animais... altere o metodo ... para 'seg' (segmentacao) nas configuracoes." acvm: "O modo de deteccao (det) suporta apenas 1 animal por aquario. Voce configurou N animais... use o modo de segmentacao (seg)." Mesma regra, mesma correcao a sugerir, conselho redigido de dois jeitos conforme o caminho que disparasse. Agora as duas compartilham UM msgid (o do lote 3b) -- e o assert que eu tinha deixado propositalmente em portugues no `test_detection_enforcement` pode finalmente casar com o ingles. `_SLOT_LABELS` deixou de ser `ClassVar` e virou `_slot_labels()`. Era um dos sitios de traducao em tempo de import listados no handoff: um dict de chamadas a `_()` no corpo da classe e avaliado no import, ANTES de `i18n.install()`, e congelaria os rotulos no idioma-fonte independentemente de `ui.language`. `progress_notifier`: o plano era migrar o produtor e depois simplificar o consumidor (`state_synchronizer`) para so `"step:"`. A apuracao mostrou que o plano estava errado. NENHUM produtor poe prefixo no campo `step` -- os valores vem do `live_camera_session_coordinator` ja traduzidos e nus, e o `"Etapa:"` do `progress_notifier` vai para a MENSAGEM do SET_STATUS, outro campo. A tira- prefixo e defensiva contra um produtor que nao existe, e o unico teste que a cobre (`test_analysis_metadata_display`) REIMPLEMENTA a funcao em vez de chama-la, entao passaria igual se o codigo real sumisse. Simplificar para so `"step:"` seria pior ainda: em pt_BR nada tem prefixo nenhum. Deixei o comportamento tolerante e corrigi o comentario, que afirmava o contrario. `"Erro de Processamento"` (4 sitios) foi unificado com o `"Erro no Processamento"` ja existente: um titulo de dialogo, um msgid. A variacao de/no era ruido do fonte. Guardas de `aquarium_detector` e `multi_aquarium_detector` ("Only 2 aquariums are supported", "ByteTracker not initialised for aquarium N") ficaram em ingles sem `_()`: nomeiam estado interno e contrato de API. O limite de 2 aquarios que o OPERADOR ve continua vindo do wizard, esse sim traduzido. Verde: ruff limpo, mypy limpo em 698 arquivos, 4210 fast, 1079 gui. Co-Authored-By: Claude Opus 5 --- docs/tasks/active/ROLLING_TASK_LOG.md | 10 +- .../core/detection/aquarium_detector.py | 15 +- .../core/detection/multi_aquarium_detector.py | 12 +- .../core/video/analysis_pipeline_runner.py | 9 +- src/zebtrack/core/video/processing_mode.py | 6 +- src/zebtrack/core/video/progress_notifier.py | 7 +- .../core/video/tracking_session_runner.py | 7 +- .../core/video/video_context_factory.py | 15 +- .../viewmodels/analysis_control_view_model.py | 29 ++-- .../viewmodels/hardware_status_view_model.py | 32 ++-- .../viewmodels/main_view_model_runtime.py | 9 +- .../locales/_pairs/pr3-core-tail.json | 28 ++++ .../locales/pt_BR/LC_MESSAGES/zebtrack.mo | Bin 159177 -> 160687 bytes .../locales/pt_BR/LC_MESSAGES/zebtrack.po | 146 ++++++++++++++++-- src/zebtrack/locales/zebtrack.pot | 120 ++++++++++++-- .../ui/components/state_synchronizer.py | 8 +- .../core/test_analysis_control_view_model.py | 4 +- tests/core/test_processing_mode.py | 2 +- tests/i18n/test_no_untranslated_literals.py | 6 +- tests/test_detection_enforcement.py | 6 +- 20 files changed, 374 insertions(+), 97 deletions(-) create mode 100644 src/zebtrack/locales/_pairs/pr3-core-tail.json diff --git a/docs/tasks/active/ROLLING_TASK_LOG.md b/docs/tasks/active/ROLLING_TASK_LOG.md index 2a703748..bd8eca41 100644 --- a/docs/tasks/active/ROLLING_TASK_LOG.md +++ b/docs/tasks/active/ROLLING_TASK_LOG.md @@ -48,9 +48,13 @@ no topo do ratchet; depois, atualizar a seção "A migração" em - [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. -- [ ] Lote 3d: `core/video/**`, `core/detection/**`, `core/viewmodels/**` (30 em - 10 arquivos), incluindo simplificar o consumidor de `progress_notifier` - para só "step:" depois de migrar o produtor. +- [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. - [ ] Lote 5: `ui/dialogs/**` (26 arquivos). 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/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-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/pt_BR/LC_MESSAGES/zebtrack.mo b/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.mo index 42cf5e0de16ebff6b58028f035494d31fdcaaa73..56977a46669d45b53e5b00f42be6c577eacbf724 100644 GIT binary patch delta 24030 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\n" "Language-Team: pt_BR \n" @@ -1248,6 +1248,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" @@ -1350,6 +1354,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" @@ -2044,6 +2049,7 @@ msgid "Video not found in the project." msgstr "Vídeo não encontrado no projeto." #: src/zebtrack/core/project/project_workflow_service.py:129 +#: src/zebtrack/core/viewmodels/analysis_control_view_model.py:92 #, python-brace-format msgid "" "The detection mode (det) for animals is only compatible with 1 animal per aquarium.\n" @@ -2625,14 +2631,14 @@ msgstr "" msgid "token {token}: enter for {enter_rois} and exit for {exit_rois}" msgstr "token {token}: entrada de {enter_rois} e saída de {exit_rois}" -#: src/zebtrack/core/services/detector_service.py:132 +#: src/zebtrack/core/services/detector_service.py:131 #, python-brace-format msgid "No {method} model is available for animal detection." msgstr "Nenhum modelo {method} está disponível para detecção de animais." # | msgid "Could not connect to the Arduino on port {port}." # | msgid "Could not open the video: {path}" -#: src/zebtrack/core/services/detector_service.py:141 +#: src/zebtrack/core/services/detector_service.py:140 #, python-brace-format msgid "Could not find the weight matching the path: {path}" msgstr "Não foi possível encontrar o peso correspondente ao caminho: {path}" @@ -2646,14 +2652,14 @@ msgstr "" "Caminho do modelo OpenVINO não encontrado ou inválido. Por favor, converta o" " modelo primeiro." -#: src/zebtrack/core/services/detector_service.py:165 +#: src/zebtrack/core/services/detector_service.py:164 msgid "YOLO .pt model path not found or invalid." msgstr "Caminho do modelo YOLO .pt não encontrado ou inválido." # | msgid "Error Saving to Project" # | msgid "Save to Project" #: src/zebtrack/core/services/model_override_service.py:158 -#: src/zebtrack/core/services/model_override_service.py:354 +#: src/zebtrack/core/services/model_override_service.py:352 #: src/zebtrack/ui/components/dialog_manager.py:444 src/zebtrack/ui/gui.py:576 msgid "No Project" msgstr "Nenhum Projeto" @@ -2677,11 +2683,11 @@ msgid "Global settings applied to the project at {path}." msgstr "Configurações globais aplicadas ao projeto em {path}." # | msgid "Open a project before adjusting the specific calibration." -#: src/zebtrack/core/services/model_override_service.py:355 +#: src/zebtrack/core/services/model_override_service.py:353 msgid "Open a project before saving calibration overrides." msgstr "Abra um projeto antes de salvar overrides de calibração." -#: src/zebtrack/core/services/model_override_service.py:372 +#: src/zebtrack/core/services/model_override_service.py:370 msgid "Project overrides updated from this calibration." msgstr "Overrides do projeto atualizados a partir desta calibração." @@ -2767,27 +2773,27 @@ msgstr "O caminho do modelo é inválido ou inacessível: {error}" # | msgid "Template '{name}' applied to the video being edited." # | msgid "Template '{name}' already exists." -#: src/zebtrack/core/services/weight_manager.py:914 +#: src/zebtrack/core/services/weight_manager.py:913 #, python-brace-format msgid "A weight named '{name}' already exists." msgstr "Um peso com o nome '{name}' já existe." # | msgid "Video not found" # | msgid "ROI '{name}' not found." -#: src/zebtrack/core/services/weight_manager.py:979 +#: src/zebtrack/core/services/weight_manager.py:976 #, python-brace-format msgid "Weight '{name}' not found." msgstr "Peso '{name}' não encontrado." -#: src/zebtrack/core/services/weight_manager.py:984 +#: src/zebtrack/core/services/weight_manager.py:980 msgid "You cannot delete the last available weight." msgstr "Você não pode excluir o último peso disponível." -#: src/zebtrack/core/services/weight_manager.py:1480 +#: src/zebtrack/core/services/weight_manager.py:1476 msgid "OpenVINO model .xml file not found after the conversion." msgstr "Arquivo .xml do modelo OpenVINO não encontrado após a conversão." -#: src/zebtrack/core/services/weight_manager.py:1621 +#: src/zebtrack/core/services/weight_manager.py:1616 msgid "INT8 model .xml file not found after the conversion." msgstr "Arquivo .xml do modelo INT8 não encontrado após conversão." @@ -2949,11 +2955,121 @@ msgstr "Regex não corresponde a arquivos: {files}" # | msgid "Invalid target" # | msgid "Invalid template" # | msgid "Invalid template in {path}: {error}" -#: src/zebtrack/core/services/wizard_service.py:987 +#: src/zebtrack/core/services/wizard_service.py:986 #, python-brace-format msgid "Invalid regex pattern: {error}" msgstr "Padrão regex inválido: {error}" +# | msgid "Invalid Configuration" +#: src/zebtrack/core/video/analysis_pipeline_runner.py:697 +#| msgid "Incomplete Configuration" +msgid "Incomplete calibration data." +msgstr "Dados de calibração incompletos." + +#: src/zebtrack/core/video/analysis_pipeline_runner.py:731 +#| msgid "Could not read the aquarium polygon data." +msgid "Failed to prepare the calibration data." +msgstr "Falha ao preparar dados de calibração." + +#: src/zebtrack/core/video/processing_mode.py:29 +#| msgid "Multi-individual" +msgid "Individual" +msgstr "Individual" + +#: src/zebtrack/core/video/processing_mode.py:30 +#: src/zebtrack/ui/components/analysis_controls.py:65 +msgid "Multi-individual" +msgstr "Multi-indivíduos" + +# | msgid "Analysing frame {current}/{total}..." +#: src/zebtrack/core/video/progress_notifier.py:81 +#, python-brace-format +#| msgid "Processing aquarium {current}/{total}..." +msgid "Processing {current}/{total}: {experiment_id}" +msgstr "Processando {current}/{total}: {experiment_id}" + +#: src/zebtrack/core/video/progress_notifier.py:84 +#, python-brace-format +#| msgid "OpenVINO: {status}" +msgid "Step: {status}" +msgstr "Etapa: {status}" + +#: src/zebtrack/core/video/tracking_session_runner.py:251 +#, python-brace-format +msgid "Trajectory for {experiment_id} generated." +msgstr "Trajetória para {experiment_id} gerada." + +#: src/zebtrack/core/video/video_context_factory.py:299 +#, python-brace-format +msgid "Failed to generate the trajectory file for {experiment_id}." +msgstr "Falha ao gerar arquivo de trajetória para {experiment_id}." + +# | 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/video/video_context_factory.py:301 +#, python-brace-format +#| msgid "Template not found: {path}" +msgid "File not found: {path}" +msgstr "Arquivo não encontrado: {path}" + +#: src/zebtrack/core/video/video_context_factory.py:319 +#, python-brace-format +msgid "Failed to read the trajectory file for {experiment_id}." +msgstr "Falha ao ler arquivo de trajetória para {experiment_id}." + +# | msgid "Analysis in Progress" +#: src/zebtrack/core/viewmodels/analysis_control_view_model.py:216 +#| msgid "Live analysis in progress." +msgid "Cancelling the analysis in progress..." +msgstr "Cancelando análise em andamento..." + +#: src/zebtrack/core/viewmodels/analysis_control_view_model.py:301 +#, python-brace-format +#| msgid "Analysing frame {current}/{total}..." +msgid "Generating report {current}/{total}..." +msgstr "Gerando relatório {current}/{total}..." + +# | msgid "Reports removed" +#: src/zebtrack/core/viewmodels/analysis_control_view_model.py:533 +#| msgid "Reports generated." +msgid "Report generation finished." +msgstr "Geração de relatórios concluída." + +#: src/zebtrack/core/viewmodels/hardware_status_view_model.py:247 +#| msgid "🐠 Aquarium (Detection)" +msgid "🐠 Aquarium (det)" +msgstr "🐠 Aquário (det)" + +#: src/zebtrack/core/viewmodels/hardware_status_view_model.py:248 +#| msgid "🐠 Aquarium (Segmentation)" +msgid "🐠 Aquarium (seg)" +msgstr "🐠 Aquário (seg)" + +#: src/zebtrack/core/viewmodels/hardware_status_view_model.py:249 +#| msgid "🐟 Animal (Detection)" +msgid "🐟 Animal (det)" +msgstr "🐟 Animal (det)" + +#: src/zebtrack/core/viewmodels/hardware_status_view_model.py:250 +#| msgid "🐟 Animal (Segmentation)" +msgid "🐟 Animal (seg)" +msgstr "🐟 Animal (seg)" + +# | msgid "Analysis in Progress" +# | msgid "Analysis Error" +#: src/zebtrack/core/viewmodels/main_view_model_runtime.py:525 +#| msgid "Fatal Error" +msgid "Critical Error" +msgstr "Erro Crítico" + +#: src/zebtrack/core/viewmodels/main_view_model_runtime.py:527 +msgid "" +"The camera thread did not shut down correctly. The application will close." +msgstr "" +"A thread da câmera não foi finalizada corretamente. O aplicativo será " +"encerrado." + #: src/zebtrack/io/arduino_manager.py:140 #, python-brace-format msgid "Could not connect to the Arduino on port {port}." @@ -3550,10 +3666,6 @@ msgstr "Tarefa: {value}" msgid "Tracking mode: {value}" msgstr "Modo de rastreamento: {value}" -#: src/zebtrack/ui/components/analysis_controls.py:65 -msgid "Multi-individual" -msgstr "Multi-indivíduos" - #: src/zebtrack/ui/components/analysis_controls.py:74 #: src/zebtrack/ui/components/analysis_display.py:442 #: src/zebtrack/ui/components/state_synchronizer.py:589 diff --git a/src/zebtrack/locales/zebtrack.pot b/src/zebtrack/locales/zebtrack.pot index a1e3ddb0..e147ff27 100644 --- a/src/zebtrack/locales/zebtrack.pot +++ b/src/zebtrack/locales/zebtrack.pot @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: DRerio LogAI VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-08-13 21:55-0300\n" +"POT-Creation-Date: 2026-08-13 22:18-0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -990,6 +990,10 @@ msgid "Unknown error" msgstr "" #: 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 "" @@ -1065,6 +1069,7 @@ msgid "Processing {name}: {done}/{total} ({pct}%) - {detected} detections{eta}" msgstr "" #: 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 "" @@ -1619,6 +1624,7 @@ msgid "Video not found in the project." msgstr "" #: src/zebtrack/core/project/project_workflow_service.py:129 +#: src/zebtrack/core/viewmodels/analysis_control_view_model.py:92 #, python-brace-format msgid "" "The detection mode (det) for animals is only compatible with 1 animal per" @@ -2069,12 +2075,12 @@ msgstr "" msgid "token {token}: enter for {enter_rois} and exit for {exit_rois}" msgstr "" -#: src/zebtrack/core/services/detector_service.py:132 +#: src/zebtrack/core/services/detector_service.py:131 #, python-brace-format msgid "No {method} model is available for animal detection." msgstr "" -#: src/zebtrack/core/services/detector_service.py:141 +#: src/zebtrack/core/services/detector_service.py:140 #, python-brace-format msgid "Could not find the weight matching the path: {path}" msgstr "" @@ -2083,12 +2089,12 @@ msgstr "" msgid "OpenVINO model path not found or invalid. Please convert the model first." msgstr "" -#: src/zebtrack/core/services/detector_service.py:165 +#: src/zebtrack/core/services/detector_service.py:164 msgid "YOLO .pt model path not found or invalid." msgstr "" #: src/zebtrack/core/services/model_override_service.py:158 -#: src/zebtrack/core/services/model_override_service.py:354 +#: src/zebtrack/core/services/model_override_service.py:352 #: src/zebtrack/ui/components/dialog_manager.py:444 src/zebtrack/ui/gui.py:576 msgid "No Project" msgstr "" @@ -2106,11 +2112,11 @@ msgstr "" msgid "Global settings applied to the project at {path}." msgstr "" -#: src/zebtrack/core/services/model_override_service.py:355 +#: src/zebtrack/core/services/model_override_service.py:353 msgid "Open a project before saving calibration overrides." msgstr "" -#: src/zebtrack/core/services/model_override_service.py:372 +#: src/zebtrack/core/services/model_override_service.py:370 msgid "Project overrides updated from this calibration." msgstr "" @@ -2170,25 +2176,25 @@ msgstr "" msgid "The model path is invalid or inaccessible: {error}" msgstr "" -#: src/zebtrack/core/services/weight_manager.py:914 +#: src/zebtrack/core/services/weight_manager.py:913 #, python-brace-format msgid "A weight named '{name}' already exists." msgstr "" -#: src/zebtrack/core/services/weight_manager.py:979 +#: src/zebtrack/core/services/weight_manager.py:976 #, python-brace-format msgid "Weight '{name}' not found." msgstr "" -#: src/zebtrack/core/services/weight_manager.py:984 +#: src/zebtrack/core/services/weight_manager.py:980 msgid "You cannot delete the last available weight." msgstr "" -#: src/zebtrack/core/services/weight_manager.py:1480 +#: src/zebtrack/core/services/weight_manager.py:1476 msgid "OpenVINO model .xml file not found after the conversion." msgstr "" -#: src/zebtrack/core/services/weight_manager.py:1621 +#: src/zebtrack/core/services/weight_manager.py:1616 msgid "INT8 model .xml file not found after the conversion." msgstr "" @@ -2316,11 +2322,95 @@ msgstr "" msgid "The regex does not match these files: {files}" msgstr "" -#: src/zebtrack/core/services/wizard_service.py:987 +#: src/zebtrack/core/services/wizard_service.py:986 #, python-brace-format msgid "Invalid regex pattern: {error}" msgstr "" +#: src/zebtrack/core/video/analysis_pipeline_runner.py:697 +msgid "Incomplete calibration data." +msgstr "" + +#: src/zebtrack/core/video/analysis_pipeline_runner.py:731 +msgid "Failed to prepare the calibration data." +msgstr "" + +#: src/zebtrack/core/video/processing_mode.py:29 +msgid "Individual" +msgstr "" + +#: src/zebtrack/core/video/processing_mode.py:30 +#: src/zebtrack/ui/components/analysis_controls.py:65 +msgid "Multi-individual" +msgstr "" + +#: src/zebtrack/core/video/progress_notifier.py:81 +#, python-brace-format +msgid "Processing {current}/{total}: {experiment_id}" +msgstr "" + +#: src/zebtrack/core/video/progress_notifier.py:84 +#, python-brace-format +msgid "Step: {status}" +msgstr "" + +#: src/zebtrack/core/video/tracking_session_runner.py:251 +#, python-brace-format +msgid "Trajectory for {experiment_id} generated." +msgstr "" + +#: src/zebtrack/core/video/video_context_factory.py:299 +#, python-brace-format +msgid "Failed to generate the trajectory file for {experiment_id}." +msgstr "" + +#: src/zebtrack/core/video/video_context_factory.py:301 +#, python-brace-format +msgid "File not found: {path}" +msgstr "" + +#: src/zebtrack/core/video/video_context_factory.py:319 +#, python-brace-format +msgid "Failed to read the trajectory file for {experiment_id}." +msgstr "" + +#: src/zebtrack/core/viewmodels/analysis_control_view_model.py:216 +msgid "Cancelling the analysis in progress..." +msgstr "" + +#: src/zebtrack/core/viewmodels/analysis_control_view_model.py:301 +#, python-brace-format +msgid "Generating report {current}/{total}..." +msgstr "" + +#: src/zebtrack/core/viewmodels/analysis_control_view_model.py:533 +msgid "Report generation finished." +msgstr "" + +#: src/zebtrack/core/viewmodels/hardware_status_view_model.py:247 +msgid "🐠 Aquarium (det)" +msgstr "" + +#: src/zebtrack/core/viewmodels/hardware_status_view_model.py:248 +msgid "🐠 Aquarium (seg)" +msgstr "" + +#: src/zebtrack/core/viewmodels/hardware_status_view_model.py:249 +msgid "🐟 Animal (det)" +msgstr "" + +#: src/zebtrack/core/viewmodels/hardware_status_view_model.py:250 +msgid "🐟 Animal (seg)" +msgstr "" + +#: src/zebtrack/core/viewmodels/main_view_model_runtime.py:525 +msgid "Critical Error" +msgstr "" + +#: src/zebtrack/core/viewmodels/main_view_model_runtime.py:527 +msgid "The camera thread did not shut down correctly. The application will close." +msgstr "" + #: src/zebtrack/io/arduino_manager.py:140 #, python-brace-format msgid "Could not connect to the Arduino on port {port}." @@ -2858,10 +2948,6 @@ msgstr "" msgid "Tracking mode: {value}" msgstr "" -#: src/zebtrack/ui/components/analysis_controls.py:65 -msgid "Multi-individual" -msgstr "" - #: src/zebtrack/ui/components/analysis_controls.py:74 #: src/zebtrack/ui/components/analysis_display.py:442 #: src/zebtrack/ui/components/state_synchronizer.py:589 diff --git a/src/zebtrack/ui/components/state_synchronizer.py b/src/zebtrack/ui/components/state_synchronizer.py index 398da880..6ea95427 100644 --- a/src/zebtrack/ui/components/state_synchronizer.py +++ b/src/zebtrack/ui/components/state_synchronizer.py @@ -820,8 +820,12 @@ def update_analysis_task_status( if step: step_text = str(step).strip() if step_text: - # Both spellings: the producer (progress_notifier) prefixes the - # Portuguese "Etapa:" today and the English "Step:" once migrated. + # Defensive only: no producer puts a prefix on `step`. The values + # come from live_camera_session_coordinator already translated and + # bare; progress_notifier's "Step:" goes into the SET_STATUS + # message, a different field. Kept tolerant rather than matched + # exactly, because a prefix check on displayed text is precisely + # what translation breaks. for prefix in ("etapa:", "step:"): if step_text.lower().startswith(prefix): step_text = step_text[len(prefix) :].strip() diff --git a/tests/core/test_analysis_control_view_model.py b/tests/core/test_analysis_control_view_model.py index d7c88011..be655daa 100644 --- a/tests/core/test_analysis_control_view_model.py +++ b/tests/core/test_analysis_control_view_model.py @@ -72,7 +72,7 @@ def test_start_single_video_workflow_invalid_det_config(view_model): view_model.ui_event_bus.publish.assert_called_once() event = view_model.ui_event_bus.publish.call_args[0][0] assert event.type == UIEvents.SHOW_ERROR - assert event.data.title == "Configuração Inválida" + assert event.data.title == "Invalid Configuration" def test_start_single_video_workflow_detector_setup_failure(view_model): @@ -172,7 +172,7 @@ def test_cancel_current_analysis_stops_live_session(view_model): view_model.ui_event_bus.publish.assert_called_once_with( Event( type=UIEvents.SET_STATUS, - data=payloads.StatusPayload(message="Cancelando análise em andamento..."), + data=payloads.StatusPayload(message="Cancelling the analysis in progress..."), ) ) view_model.ui_state_controller._show_cancel_feedback.assert_called_once() diff --git a/tests/core/test_processing_mode.py b/tests/core/test_processing_mode.py index 6bab16a8..1c5ca1d4 100644 --- a/tests/core/test_processing_mode.py +++ b/tests/core/test_processing_mode.py @@ -4,7 +4,7 @@ def test_processing_mode_display_names(): - assert ProcessingMode.MULTI_TRACK.display_name == "Multi-indivíduos" + assert ProcessingMode.MULTI_TRACK.display_name == "Multi-individual" assert ProcessingMode.SINGLE_SUBJECT.display_name == "Individual" diff --git a/tests/i18n/test_no_untranslated_literals.py b/tests/i18n/test_no_untranslated_literals.py index 0b7029bb..9093b671 100644 --- a/tests/i18n/test_no_untranslated_literals.py +++ b/tests/i18n/test_no_untranslated_literals.py @@ -30,11 +30,7 @@ MIGRATED_PATHS: tuple[str, ...] = ( "src/zebtrack/analysis", "src/zebtrack/coordinators", - "src/zebtrack/core/app_runner.py", - "src/zebtrack/core/application_bootstrapper.py", - "src/zebtrack/core/project", - "src/zebtrack/core/recording", - "src/zebtrack/core/services", + "src/zebtrack/core", "src/zebtrack/i18n.py", "src/zebtrack/io", "src/zebtrack/plugins", diff --git a/tests/test_detection_enforcement.py b/tests/test_detection_enforcement.py index 30ba3af9..ac70ffa9 100644 --- a/tests/test_detection_enforcement.py +++ b/tests/test_detection_enforcement.py @@ -136,7 +136,5 @@ def test_single_video_detection_mode_enforcement(): mock_event_bus.publish.assert_called_once() call_ev = mock_event_bus.publish.call_args[0][0] assert call_ev.type == UIEvents.SHOW_ERROR - assert "Configuração Inválida" in call_ev.data.title - # Producer is core/viewmodels/analysis_control_view_model.py, not yet - # migrated: still Portuguese on purpose. - assert "modo de detecção (det)" in call_ev.data.message + assert "Invalid Configuration" in call_ev.data.title + assert "detection mode (det)" in call_ev.data.message From 960054c4d10793a871947e92159e5ac116603fa7 Mon Sep 17 00:00:00 2001 From: MarkSant Date: Fri, 14 Aug 2026 10:55:24 -0300 Subject: [PATCH 08/21] feat(i18n): wizard -- models, dialogo e design experimental (fase 3, lote 4a) Primeiro lote do `ui/wizard`. Tres arquivos a zero; os outros dez seguem. `models.py` NAO foi traduzido: passou a INGLES FIXO, sem `_()`. E a camada de esquema Pydantic, e a decisao segue a mesma de `core/project/schemas.py`: - `Field(description=...)` e avaliado no CORPO DA CLASSE, onde `_()` e proibido (congelaria no idioma-fonte no import); - o arquivo JA era misto -- os campos mais novos (`roi_inclusion_rule`, `behavioral_analysis`) ja tinham descricao em ingles. Traduzir metade e deixar metade era o estado anterior, nao um objetivo; - as mensagens dos validators nomeiam campos e so aparecem como `ValidationError` cru. O texto que o operador realmente le vem do `validate()` de cada step -- esse sim traduzido -- e da `wizard_service.validate_*`, migrada no lote 3c. Cuidado deliberado com os valores de enum (`experimental`, `live`, `skip`, `import_zones`, `partial`, `full`, `replace`, `merge`, `manual`): sao persistidos no projeto e ficaram intocados. So rotulo exibido foi tocado. `wizard_dialog.py`: os quatro botoes de navegacao e o titulo do messagebox de validacao. `"Cancelar"` reusou o msgid `"Cancel"` da fase 1. `experimental_design_step.py`: rotulos, os dois textos longos (tooltip de duracao e caixa "Como isso sera usado?") e o resumo com contagem. Sete literais acentuados mais oito sem acento que o scanner nao ve ("dias", "grupos", "animais/grupo", "Animais por Grupo:", "Nomes dos Grupos", ...). Verde: ruff limpo, mypy limpo em 698 arquivos, 4210 fast, 1079 gui. Co-Authored-By: Claude Opus 5 --- docs/tasks/active/ROLLING_TASK_LOG.md | 7 + .../locales/_pairs/pr3-wizard-shell.json | 25 +++ .../locales/pt_BR/LC_MESSAGES/zebtrack.mo | Bin 160687 -> 162846 bytes .../locales/pt_BR/LC_MESSAGES/zebtrack.po | 166 ++++++++++++++++-- src/zebtrack/locales/zebtrack.pot | 101 ++++++++++- .../ui/wizard/experimental_design_step.py | 59 ++++--- src/zebtrack/ui/wizard/models.py | 68 +++---- src/zebtrack/ui/wizard/wizard_dialog.py | 13 +- 8 files changed, 355 insertions(+), 84 deletions(-) create mode 100644 src/zebtrack/locales/_pairs/pr3-wizard-shell.json diff --git a/docs/tasks/active/ROLLING_TASK_LOG.md b/docs/tasks/active/ROLLING_TASK_LOG.md index bd8eca41..f70378b0 100644 --- a/docs/tasks/active/ROLLING_TASK_LOG.md +++ b/docs/tasks/active/ROLLING_TASK_LOG.md @@ -57,6 +57,13 @@ no topo do ratchet; depois, atualizar a seção "A migração" em 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`. + - [ ] 4b: `confirmation_step.py` (55) + `live_config_step.py` (49). + - [ ] 4c: `model_selection_step.py` (41) + `detection_step.py` (28). + - [ ] 4d: `discovery_step` (20), `custom_regex_dialog` (19), + `file_selection_step` (19), `import_config_step` (16), + `calibration_step` (14), `design_editor_dialog` (9). - [ ] Lote 5: `ui/dialogs/**` (26 arquivos). - [ ] Fechamento: colapsar MIGRATED_PATHS, atualizar guia e CHANGELOG. 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 56977a46669d45b53e5b00f42be6c577eacbf724..8c2f933372e9005a8a3848dcf12b509dbd636c20 100644 GIT binary patch delta 25236 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 ZB>r!%;Aj64lxm^H|IJKoS-EgR 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 CFw-Rf diff --git a/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.po b/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.po index e0de9f85..da418d3c 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-13 22:18-0300\n" +"POT-Creation-Date: 2026-08-14 10:49-0300\n" "PO-Revision-Date: 2026-08-08 21:41-0300\n" "Last-Translator: FULL NAME \n" "Language-Team: pt_BR \n" @@ -2961,18 +2961,18 @@ msgid "Invalid regex pattern: {error}" msgstr "Padrão regex inválido: {error}" # | msgid "Invalid Configuration" +# | msgid "Incomplete Configuration" #: src/zebtrack/core/video/analysis_pipeline_runner.py:697 -#| msgid "Incomplete Configuration" msgid "Incomplete calibration data." msgstr "Dados de calibração incompletos." +# | msgid "Could not read the aquarium polygon data." #: src/zebtrack/core/video/analysis_pipeline_runner.py:731 -#| msgid "Could not read the aquarium polygon data." msgid "Failed to prepare the calibration data." msgstr "Falha ao preparar dados de calibração." +# | msgid "Multi-individual" #: src/zebtrack/core/video/processing_mode.py:29 -#| msgid "Multi-individual" msgid "Individual" msgstr "Individual" @@ -2982,15 +2982,15 @@ msgid "Multi-individual" msgstr "Multi-indivíduos" # | msgid "Analysing frame {current}/{total}..." +# | msgid "Processing aquarium {current}/{total}..." #: src/zebtrack/core/video/progress_notifier.py:81 #, python-brace-format -#| msgid "Processing aquarium {current}/{total}..." msgid "Processing {current}/{total}: {experiment_id}" msgstr "Processando {current}/{total}: {experiment_id}" +# | msgid "OpenVINO: {status}" #: src/zebtrack/core/video/progress_notifier.py:84 #, python-brace-format -#| msgid "OpenVINO: {status}" msgid "Step: {status}" msgstr "Etapa: {status}" @@ -3007,59 +3007,59 @@ msgstr "Falha ao gerar arquivo de trajetória para {experiment_id}." # | msgid "Could not connect to the Arduino on port {port}." # | msgid "Could not open the video: {path}" # | msgid "Could not open: {path}" +# | msgid "Template not found: {path}" #: src/zebtrack/core/video/video_context_factory.py:301 #, python-brace-format -#| msgid "Template not found: {path}" msgid "File not found: {path}" msgstr "Arquivo não encontrado: {path}" -#: src/zebtrack/core/video/video_context_factory.py:319 +#: src/zebtrack/core/video/video_context_factory.py:318 #, python-brace-format msgid "Failed to read the trajectory file for {experiment_id}." msgstr "Falha ao ler arquivo de trajetória para {experiment_id}." # | msgid "Analysis in Progress" +# | msgid "Live analysis in progress." #: src/zebtrack/core/viewmodels/analysis_control_view_model.py:216 -#| msgid "Live analysis in progress." msgid "Cancelling the analysis in progress..." msgstr "Cancelando análise em andamento..." +# | msgid "Analysing frame {current}/{total}..." #: src/zebtrack/core/viewmodels/analysis_control_view_model.py:301 #, python-brace-format -#| msgid "Analysing frame {current}/{total}..." msgid "Generating report {current}/{total}..." msgstr "Gerando relatório {current}/{total}..." # | msgid "Reports removed" +# | msgid "Reports generated." #: src/zebtrack/core/viewmodels/analysis_control_view_model.py:533 -#| msgid "Reports generated." msgid "Report generation finished." msgstr "Geração de relatórios concluída." +# | msgid "🐠 Aquarium (Detection)" #: src/zebtrack/core/viewmodels/hardware_status_view_model.py:247 -#| msgid "🐠 Aquarium (Detection)" msgid "🐠 Aquarium (det)" msgstr "🐠 Aquário (det)" +# | msgid "🐠 Aquarium (Segmentation)" #: src/zebtrack/core/viewmodels/hardware_status_view_model.py:248 -#| msgid "🐠 Aquarium (Segmentation)" msgid "🐠 Aquarium (seg)" msgstr "🐠 Aquário (seg)" +# | msgid "🐟 Animal (Detection)" #: src/zebtrack/core/viewmodels/hardware_status_view_model.py:249 -#| msgid "🐟 Animal (Detection)" msgid "🐟 Animal (det)" msgstr "🐟 Animal (det)" +# | msgid "🐟 Animal (Segmentation)" #: src/zebtrack/core/viewmodels/hardware_status_view_model.py:250 -#| msgid "🐟 Animal (Segmentation)" msgid "🐟 Animal (seg)" msgstr "🐟 Animal (seg)" # | msgid "Analysis in Progress" # | msgid "Analysis Error" +# | msgid "Fatal Error" #: src/zebtrack/core/viewmodels/main_view_model_runtime.py:525 -#| msgid "Fatal Error" msgid "Critical Error" msgstr "Erro Crítico" @@ -3444,6 +3444,7 @@ msgid "Detection (for fast bounding boxes)" msgstr "Detecção (para caixas delimitadoras rápidas)" #: src/zebtrack/ui/builders/common_widgets.py:223 +#: src/zebtrack/ui/wizard/wizard_dialog.py:207 msgid "Cancel" msgstr "Cancelar" @@ -8860,6 +8861,139 @@ msgstr "Frame carregado: {name}" msgid "Buffer radius must be >= 0" msgstr "Raio de buffer deve ser >= 0" +#: src/zebtrack/ui/wizard/experimental_design_step.py:154 +#| msgid "Invalid Configuration" +msgid "Experimental Design Configuration" +msgstr "Configuração do Design Experimental" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:161 +msgid "Set up the structure of your live experiment" +msgstr "Configure a estrutura do seu experimento ao vivo" + +# | msgid "Global Model Configuration..." +# | msgid "Model Configuration" +#: src/zebtrack/ui/wizard/experimental_design_step.py:174 +#| msgid "Zone Configuration" +msgid "Basic Configuration" +msgstr "Configuração Básica" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:183 +msgid "Experiment Duration (days):" +msgstr "Duração do Experimento (dias):" + +# | msgid "Day" +#: src/zebtrack/ui/wizard/experimental_design_step.py:199 +#| msgid "day" +msgid "days" +msgstr "dias" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:204 +msgid "" +"Experiment Duration\n" +"\n" +"How many days your full experiment will last.\n" +"\n" +"Examples:\n" +"• 1 day: acute test\n" +"• 7 days: 1-week treatment\n" +"• 21 days: chronic treatment\n" +"\n" +"You can type directly or use the +/- buttons.\n" +"This affects how the output files are organised." +msgstr "" +"Duração do Experimento\n" +"\n" +"Quantos dias durará seu experimento completo.\n" +"\n" +"Exemplos:\n" +"• 1 dia: Teste agudo\n" +"• 7 dias: Tratamento de 1 semana\n" +"• 21 dias: Tratamento crônico\n" +"\n" +"Você pode digitar diretamente ou usar os botões +/-.\n" +"Isso afeta a organização dos arquivos de saída." + +# | msgid "Analysis in Progress" +#: src/zebtrack/ui/wizard/experimental_design_step.py:218 +#| msgid "Analysis Error" +msgid "Animals per Group:" +msgstr "Animais por Grupo:" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:234 +msgid "animals/group" +msgstr "animais/grupo" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:252 +#| msgid "No Group" +msgid "Number of Groups:" +msgstr "Número de Grupos:" + +# | msgid "No Group" +# | msgid "Group" +#: src/zebtrack/ui/wizard/experimental_design_step.py:271 +#| msgid "group" +msgid "groups" +msgstr "grupos" + +# | msgid "Weight Type" +# | msgid "Weight Name" +#: src/zebtrack/ui/wizard/experimental_design_step.py:308 +#| msgid "ROI Name" +msgid "Group Names" +msgstr "Nomes dos Grupos" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:316 +msgid "Give each group a descriptive name:" +msgstr "Defina nomes descritivos para cada grupo:" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:330 +msgid "ℹ️ How will this be used?" +msgstr "ℹ️ Como isso será usado?" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:339 +msgid "" +"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" +"\n" +"Example: 2 groups x 5 days x 3 animals = 30 organised recordings" +msgstr "" +"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" +"\n" +"Exemplo: 2 grupos x 5 dias x 3 animais = 30 gravações organizadas" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:415 +#, python-brace-format +msgid "📊 Total: {sessions} recordings ({animals} animals x {days} days)" +msgstr "📊 Total: {sessions} gravações ({animals} animais x {days} dias)" + +#: src/zebtrack/ui/wizard/wizard_dialog.py:199 +msgid "< Back" +msgstr "< Voltar" + +# | msgid "Exit" +#: src/zebtrack/ui/wizard/wizard_dialog.py:203 +#: src/zebtrack/ui/wizard/wizard_dialog.py:308 +#| msgid "On Exit" +msgid "Next >" +msgstr "Próximo >" + +#: src/zebtrack/ui/wizard/wizard_dialog.py:306 +#| msgid "Create New Project" +msgid "Create Project" +msgstr "Criar Projeto" + +#: src/zebtrack/ui/wizard/wizard_dialog.py:464 +#| msgid "Validation Error" +msgid "Validation" +msgstr "Validação" + #: src/zebtrack/utils/hardware_benchmark.py:811 msgid "Step 2/6 — Testing video decoding..." msgstr "Passo 2/6 — Testando decodificação de vídeo..." diff --git a/src/zebtrack/locales/zebtrack.pot b/src/zebtrack/locales/zebtrack.pot index e147ff27..a9bc44e8 100644 --- a/src/zebtrack/locales/zebtrack.pot +++ b/src/zebtrack/locales/zebtrack.pot @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: DRerio LogAI VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-08-13 22:18-0300\n" +"POT-Creation-Date: 2026-08-14 10:49-0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -2369,7 +2369,7 @@ msgstr "" msgid "File not found: {path}" msgstr "" -#: src/zebtrack/core/video/video_context_factory.py:319 +#: src/zebtrack/core/video/video_context_factory.py:318 #, python-brace-format msgid "Failed to read the trajectory file for {experiment_id}." msgstr "" @@ -2750,6 +2750,7 @@ msgid "Detection (for fast bounding boxes)" msgstr "" #: src/zebtrack/ui/builders/common_widgets.py:223 +#: src/zebtrack/ui/wizard/wizard_dialog.py:207 msgid "Cancel" msgstr "" @@ -7109,6 +7110,102 @@ msgstr "" msgid "Buffer radius must be >= 0" msgstr "" +#: src/zebtrack/ui/wizard/experimental_design_step.py:154 +msgid "Experimental Design Configuration" +msgstr "" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:161 +msgid "Set up the structure of your live experiment" +msgstr "" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:174 +msgid "Basic Configuration" +msgstr "" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:183 +msgid "Experiment Duration (days):" +msgstr "" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:199 +msgid "days" +msgstr "" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:204 +msgid "" +"Experiment Duration\n" +"\n" +"How many days your full experiment will last.\n" +"\n" +"Examples:\n" +"• 1 day: acute test\n" +"• 7 days: 1-week treatment\n" +"• 21 days: chronic treatment\n" +"\n" +"You can type directly or use the +/- buttons.\n" +"This affects how the output files are organised." +msgstr "" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:218 +msgid "Animals per Group:" +msgstr "" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:234 +msgid "animals/group" +msgstr "" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:252 +msgid "Number of Groups:" +msgstr "" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:271 +msgid "groups" +msgstr "" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:308 +msgid "Group Names" +msgstr "" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:316 +msgid "Give each group a descriptive name:" +msgstr "" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:330 +msgid "ℹ️ How will this be used?" +msgstr "" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:339 +msgid "" +"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" +"\n" +"Example: 2 groups x 5 days x 3 animals = 30 organised recordings" +msgstr "" + +#: src/zebtrack/ui/wizard/experimental_design_step.py:415 +#, python-brace-format +msgid "📊 Total: {sessions} recordings ({animals} animals x {days} days)" +msgstr "" + +#: src/zebtrack/ui/wizard/wizard_dialog.py:199 +msgid "< Back" +msgstr "" + +#: src/zebtrack/ui/wizard/wizard_dialog.py:203 +#: src/zebtrack/ui/wizard/wizard_dialog.py:308 +msgid "Next >" +msgstr "" + +#: src/zebtrack/ui/wizard/wizard_dialog.py:306 +msgid "Create Project" +msgstr "" + +#: src/zebtrack/ui/wizard/wizard_dialog.py:464 +msgid "Validation" +msgstr "" + #: src/zebtrack/utils/hardware_benchmark.py:811 msgid "Step 2/6 — Testing video decoding..." msgstr "" diff --git a/src/zebtrack/ui/wizard/experimental_design_step.py b/src/zebtrack/ui/wizard/experimental_design_step.py index 2669b5b5..a7826535 100644 --- a/src/zebtrack/ui/wizard/experimental_design_step.py +++ b/src/zebtrack/ui/wizard/experimental_design_step.py @@ -15,6 +15,7 @@ import structlog from zebtrack.core.services.wizard_service import WizardService +from zebtrack.i18n import _ from zebtrack.ui.wizard.base import WizardStep from zebtrack.ui.wizard.enums import WizardStepID from zebtrack.ui.wizard.tooltip import ToolTip @@ -150,14 +151,14 @@ def build_ui(self): # Header title = Label( self, - text="Configuração do Design Experimental", + text=_("Experimental Design Configuration"), font=("TkDefaultFont", 13, "bold"), ) title.pack(pady=(0, 10)) subtitle = Label( self, - text="Configure a estrutura do seu experimento ao vivo", + text=_("Set up the structure of your live experiment"), fg="gray", font=("TkDefaultFont", 10), ) @@ -170,7 +171,7 @@ def build_ui(self): # Left column: Basic config left_col = LabelFrame( config_container, - text="Configuração Básica", + text=_("Basic Configuration"), padx=15, pady=10, ) @@ -179,7 +180,7 @@ def build_ui(self): # Days Label( left_col, - text="Duração do Experimento (dias):", + text=_("Experiment Duration (days):"), font=("TkDefaultFont", 10, "bold"), ).pack(anchor="w", pady=(0, 2)) @@ -195,26 +196,26 @@ def build_ui(self): ) days_input.pack(side="left") - Label(days_frame, text="dias", fg="gray").pack(side="left", padx=5) + Label(days_frame, text=_("days"), fg="gray").pack(side="left", padx=5) ToolTip( days_frame, - ( - "Duração do Experimento\n\n" - "Quantos dias durará seu experimento completo.\n\n" - "Exemplos:\n" - "• 1 dia: Teste agudo\n" - "• 7 dias: Tratamento de 1 semana\n" - "• 21 dias: Tratamento crônico\n\n" - "Você pode digitar diretamente ou usar os botões +/-.\n" - "Isso afeta a organização dos arquivos de saída." + _( + "Experiment Duration\n\n" + "How many days your full experiment will last.\n\n" + "Examples:\n" + "• 1 day: acute test\n" + "• 7 days: 1-week treatment\n" + "• 21 days: chronic treatment\n\n" + "You can type directly or use the +/- buttons.\n" + "This affects how the output files are organised." ), ) # Subjects per group Label( left_col, - text="Animais por Grupo:", + text=_("Animals per Group:"), font=("TkDefaultFont", 10, "bold"), ).pack(anchor="w", pady=(0, 2)) @@ -230,7 +231,7 @@ def build_ui(self): ) subjects_input.pack(side="left") - Label(subjects_frame, text="animais/grupo", fg="gray").pack(side="left", padx=5) + Label(subjects_frame, text=_("animals/group"), fg="gray").pack(side="left", padx=5) ToolTip( subjects_frame, @@ -248,7 +249,7 @@ def build_ui(self): # Number of groups Label( left_col, - text="Número de Grupos:", + text=_("Number of Groups:"), font=("TkDefaultFont", 10, "bold"), ).pack(anchor="w", pady=(0, 2)) @@ -267,7 +268,7 @@ def build_ui(self): # Register callback for groups change self.num_groups_var.trace_add("write", lambda *args: self._on_num_groups_change()) - Label(groups_frame, text="grupos", fg="gray").pack(side="left", padx=5) + Label(groups_frame, text=_("groups"), fg="gray").pack(side="left", padx=5) ToolTip( groups_frame, @@ -304,7 +305,7 @@ def build_ui(self): # Right column: Group names right_col = LabelFrame( config_container, - text="Nomes dos Grupos", + text=_("Group Names"), padx=15, pady=10, ) @@ -312,7 +313,7 @@ def build_ui(self): Label( right_col, - text="Defina nomes descritivos para cada grupo:", + text=_("Give each group a descriptive name:"), fg="gray", font=("TkDefaultFont", 9), ).pack(anchor="w", pady=(0, 10)) @@ -326,7 +327,7 @@ def build_ui(self): # Info box info_frame = LabelFrame( self, - text="ℹ️ Como isso será usado?", + text=_("ℹ️ How will this be used?"), padx=15, pady=10, ) @@ -334,12 +335,12 @@ def build_ui(self): info_text = Label( info_frame, - text=( - "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\n" - "Exemplo: 2 grupos x 5 dias x 3 animais = 30 gravações organizadas" + text=_( + "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\n" + "Example: 2 groups x 5 days x 3 animals = 30 organised recordings" ), justify="left", fg="#555", @@ -411,7 +412,9 @@ def _update_summary(self): total_animals = num_groups * subjects self.summary_var.set( - f"📊 Total: {total_sessions} gravações ({total_animals} animais x {num_days} dias)" + _("📊 Total: {sessions} recordings ({animals} animals x {days} days)").format( + sessions=total_sessions, animals=total_animals, days=num_days + ) ) def validate(self) -> tuple[bool, str]: diff --git a/src/zebtrack/ui/wizard/models.py b/src/zebtrack/ui/wizard/models.py index 82e99bef..1d428878 100644 --- a/src/zebtrack/ui/wizard/models.py +++ b/src/zebtrack/ui/wizard/models.py @@ -159,7 +159,7 @@ def validate_external_trigger(cls, v, info): def validate_arduino_port(cls, v, info): """Arduino port is required when Arduino is enabled.""" if info.data.get("use_arduino") and not v: - raise ValueError("Porta Arduino é obrigatória quando Arduino está ativado") + raise ValueError("The Arduino port is required when Arduino is enabled") return v @field_validator("countdown_duration_s") @@ -167,7 +167,9 @@ def validate_arduino_port(cls, v, info): def validate_countdown(cls, v, info): """Countdown duration must be >= 1 when countdown is enabled.""" if info.data.get("use_countdown") and v < 1: - raise ValueError("Duração de contagem regressiva deve ser >= 1 segundo quando ativada") + raise ValueError( + "The countdown duration must be >= 1 second when the countdown is enabled" + ) return v @field_validator("recording_duration_s") @@ -175,7 +177,7 @@ def validate_countdown(cls, v, info): def validate_recording_duration(cls, v, info): """Validate that recording duration is > 0 when timed recording is enabled.""" if info.data.get("use_timed_recording") and v <= 0: - raise ValueError("Duração de gravação deve ser > 0 quando gravação temporizada ativada") + raise ValueError("The recording duration must be > 0 when timed recording is enabled") return v @@ -217,17 +219,17 @@ class AquariumConfig(BaseModel): within a video containing multiple aquariums. """ - aquarium_id: int = Field(ge=0, le=1, description="ID do aquário (0 ou 1)") - group: str = Field(min_length=1, description="Grupo experimental (ex: Controle, Tratamento)") - subject_id: str = Field(default="", description="Identificador único do sujeito") - day: int = Field(default=1, ge=1, description="Dia do experimento") + aquarium_id: int = Field(ge=0, le=1, description="Aquarium ID (0 or 1)") + group: str = Field(min_length=1, description="Experimental group (e.g. Control, Treatment)") + subject_id: str = Field(default="", description="Unique subject identifier") + day: int = Field(default=1, ge=1, description="Experiment day") @field_validator("group") @classmethod def validate_group_not_empty(cls, v: str) -> str: """Group must not be empty or contain only spaces.""" if not v.strip(): - raise ValueError("Nome do grupo não pode estar vazio") + raise ValueError("The group name cannot be empty") return v.strip() @@ -238,27 +240,27 @@ class MultiAquariumData(BaseModel): metadata extraction configurations via regex. """ - enabled: bool = Field(default=False, description="Se modo multi-aquário está habilitado") + enabled: bool = Field(default=False, description="Whether multi-aquarium mode is enabled") aquarium_configs: list[AquariumConfig] = Field( default_factory=list, max_length=2, - description="Configurações dos aquários (máximo 2)", + description="Aquarium configurations (maximum 2)", ) regex_pattern: str = Field( default="", - description="Padrão regex para extração de metadados do nome do arquivo", + description="Regex pattern for extracting metadata from the file name", ) regex_group_field: str = Field( default="group", - description="Nome do grupo de captura para o grupo experimental", + description="Capture group name for the experimental group", ) regex_subject_field: str = Field( default="subject", - description="Nome do grupo de captura para o identificador do sujeito", + description="Capture group name for the subject identifier", ) regex_day_field: str = Field( default="day", - description="Nome do grupo de captura para o dia do experimento", + description="Capture group name for the experiment day", ) @staticmethod @@ -421,7 +423,7 @@ def _is_full_context(pattern: str | None) -> bool: def validate_configs_count(cls, v: list[AquariumConfig]) -> list[AquariumConfig]: """Validate that there are at most 2 configurations.""" if len(v) > 2: - raise ValueError("Máximo de 2 aquários suportados") + raise ValueError("A maximum of 2 aquariums is supported") return v @field_validator("regex_pattern") @@ -432,16 +434,14 @@ def validate_regex_pattern(cls, v: str) -> str: try: re.compile(v) except re.error as e: - raise ValueError(f"Padrão regex inválido: {e}") from e + raise ValueError(f"Invalid regex pattern: {e}") from e return v @model_validator(mode="after") def validate_configs_when_enabled(self) -> "MultiAquariumData": """When enabled, must have exactly 2 configurations.""" if self.enabled and len(self.aquarium_configs) != 2: - raise ValueError( - "Modo multi-aquário habilitado requer exatamente 2 configurações de aquário" - ) + raise ValueError("Multi-aquarium mode requires exactly 2 aquarium configurations") return self def extract_metadata(self, filename: str) -> list[dict[str, str]]: @@ -505,7 +505,7 @@ class CalibrationData(BaseModel): ] = Field(default="bbox_intersects", description="Rule for determining ROI inclusion") multi_aquarium: MultiAquariumData = Field( default_factory=MultiAquariumData, - description="Configurações de modo multi-aquário (2 aquários por vídeo)", + description="Multi-aquarium mode settings (2 aquariums per video)", ) behavioral_analysis: BehavioralAnalysisData = Field( default_factory=BehavioralAnalysisData, @@ -541,8 +541,8 @@ def validate_detector_name(cls, v: str) -> str: if v not in available_detectors: raise ValueError( - f"Detector '{v}' não encontrado.\n" - f"Detectores disponíveis: {', '.join(sorted(available_detectors))}" + f"Detector '{v}' not found.\n" + f"Available detectors: {', '.join(sorted(available_detectors))}" ) except ImportError: # If plugins module not available, skip validation (e.g., during testing) @@ -581,7 +581,7 @@ def validate_video_files(cls, v): # Check for empty strings if any(not vf.strip() for vf in v): - raise ValueError("Caminhos de vídeo não podem estar vazios") + raise ValueError("Video paths cannot be empty") missing_files = [] invalid_formats = [] @@ -629,26 +629,30 @@ def validate_video_files(cls, v): # Task 2.4: Report forbidden paths FIRST (security priority) if forbidden_paths: errors.append( - "⚠️ SEGURANÇA: Caminhos bloqueados (diretórios do sistema):\n - " + "⚠️ SECURITY: blocked paths (system directories):\n - " + "\n - ".join(forbidden_paths[:5]) - + (f"\n ... e mais {len(forbidden_paths) - 5}" if len(forbidden_paths) > 5 else "") + + ( + f"\n ... and {len(forbidden_paths) - 5} more" + if len(forbidden_paths) > 5 + else "" + ) ) if missing_files: errors.append( - "Arquivos não encontrados:\n - " + "Files not found:\n - " + "\n - ".join(missing_files[:5]) - + (f"\n ... e mais {len(missing_files) - 5}" if len(missing_files) > 5 else "") + + (f"\n ... and {len(missing_files) - 5} more" if len(missing_files) > 5 else "") ) if not_files: - errors.append("Caminhos não são arquivos:\n - " + "\n - ".join(not_files[:5])) + errors.append("Paths are not files:\n - " + "\n - ".join(not_files[:5])) if invalid_formats: errors.append( - "Formatos de vídeo inválidos:\n - " + "Invalid video formats:\n - " + "\n - ".join(invalid_formats[:5]) - + f"\n\nFormatos aceitos: {', '.join(sorted(VIDEO_EXTENSIONS))}" + + f"\n\nAccepted formats: {', '.join(sorted(VIDEO_EXTENSIONS))}" ) if errors: @@ -691,7 +695,7 @@ class WizardData(BaseModel): def validate_live_config_required(cls, v, info): """Live config is required for live projects.""" if info.data.get("project_type") == "live" and v is None: - raise ValueError("Configuração ao vivo é obrigatória para projetos live") + raise ValueError("Live configuration is required for live projects") return v @field_validator("file_selection") @@ -699,7 +703,7 @@ def validate_live_config_required(cls, v, info): def validate_file_selection_required(cls, v, info): """File selection is required for pre-recorded projects.""" if info.data.get("project_type") == "pre-recorded" and v is None: - raise ValueError("Seleção de arquivos é obrigatória para projetos pré-gravados") + raise ValueError("A file selection is required for pre-recorded projects") return v diff --git a/src/zebtrack/ui/wizard/wizard_dialog.py b/src/zebtrack/ui/wizard/wizard_dialog.py index bf6dbebe..c881cfae 100644 --- a/src/zebtrack/ui/wizard/wizard_dialog.py +++ b/src/zebtrack/ui/wizard/wizard_dialog.py @@ -22,6 +22,7 @@ if TYPE_CHECKING: from zebtrack.settings import Settings +from zebtrack.i18n import _ from zebtrack.ui.wizard.calibration_step import CalibrationStep from zebtrack.ui.wizard.confirmation_step import ConfirmationStep from zebtrack.ui.wizard.detection_step import DetectionStep @@ -195,15 +196,15 @@ def buttonbox(self): box.grid(row=1, column=0, sticky="ew", padx=12, pady=(0, 12)) # Back button (disabled on first step) - self.back_button = Button(box, text="< Voltar", width=10, command=self._on_back) + self.back_button = Button(box, text=_("< Back"), width=10, command=self._on_back) self.back_button.pack(side="left", padx=5) # Next / Create Project button (text changes on last step) - self.next_button = Button(box, text="Próximo >", width=15, command=self._on_next) + self.next_button = Button(box, text=_("Next >"), width=15, command=self._on_next) self.next_button.pack(side="left", padx=5) # Cancel button - cancel_button = Button(box, text="Cancelar", width=10, command=self._on_cancel) + cancel_button = Button(box, text=_("Cancel"), width=10, command=self._on_cancel) cancel_button.pack(side="right", padx=5) self._update_navigation_buttons() @@ -302,9 +303,9 @@ def _update_navigation_buttons(self): # Next button: changes to "Criar Projeto" on last step if self.current_step_index == len(self.active_steps) - 1: - self.next_button.config(text="Criar Projeto") + self.next_button.config(text=_("Create Project")) else: - self.next_button.config(text="Próximo >") + self.next_button.config(text=_("Next >")) def _initialize_geometry(self): """ @@ -460,7 +461,7 @@ def _on_next(self): # Validate current step is_valid, error_message = current_step.validate() if not is_valid: - messagebox.showerror("Validação", error_message, parent=self) + messagebox.showerror(_("Validation"), error_message, parent=self) log.warning( "wizard.validation_failed", step=self.current_step_index + 1, From 228c126ec8512a493111d87f10aa4bbf5738c653 Mon Sep 17 00:00:00 2001 From: MarkSant Date: Fri, 14 Aug 2026 11:05:12 -0300 Subject: [PATCH 09/21] docs(i18n): registrar no guia as regras aprendidas na migracao O guia descrevia o mecanismo (`_()`, dominios, `.format()`) mas nenhuma das regras que a fase 3 descobriu na pratica. Elas existiam so em mensagens de commit, o que nao ajuda quem abre o arquivo. Acrescentadas: - **`_` e o alias do gettext, nunca descarte.** Ligar `_` em qualquer ponto sombreia o alias na FUNCAO INTEIRA, inclusive acima da atribuicao. Sete ocorrencias na migracao, todas pegas por ruff/mypy e nenhuma por leitura. - **Nao ramifique por texto exibido nem por texto de excecao** -- agora com o caso real do `detector_service`, onde a distincao "fora da faixa" vs "nao e numero" saia de um `in str(e)`, e com o conserto certo (tirar o range check de dentro do `try` em vez de traduzir com cuidado). - **Rotulo citado em texto de ajuda apodrece**: interpole o msgid do proprio widget. O guia pos-criacao tinha seis rotulos retipados, dois ja errados. - **Reuso de msgid**: par conflitante e DESCARTADO com um aviso facil de perder; e reuso alto e sinal de duplicacao (10 de 16 msgids de `gui.py` ja existiam). - **Regra geral do que nao traduzir**: gravado ou comparado => ingles fixo; renderizado => `_()`. Com as categorias novas (cabecalho de coluna de xlsx, metadata.json, `Field(description=...)` do Pydantic, tags de proveniencia) e o caso em que a MESMA frase tem os dois papeis. - **Plural sem ngettext** e **um msgid por frase completa**, com o motivo da concordancia de genero que substituicao nenhuma garante. A secao "The migration" deixou de descrever o plano e passou a descrever o processo: o checklist por lote, o colapso das entradas do ratchet quando um pacote fecha, e a ressalva de que a contagem do scanner e PISO -- ele so acha acento, e cada lote rendeu de um terco a metade a mais de portugues sem acento. Nenhuma mudanca de codigo. Co-Authored-By: Claude Opus 5 --- docs/guides/developer/i18n.md | 159 +++++++++++++++++++++++++++++++++- 1 file changed, 158 insertions(+), 1 deletion(-) diff --git a/docs/guides/developer/i18n.md b/docs/guides/developer/i18n.md index e881bede..942aa700 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 @@ -152,4 +282,31 @@ it: `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. +list in the same PR that migrates a package. When a whole package is done, its +per-file entries collapse into the package path, so a new file under it is +covered the moment it is added. The migration ends when the list is just +`("src/zebtrack",)`. + +**The scanner's count is a floor, not a ceiling.** It only detects accented +characters, so unaccented Portuguese — `Salvar projeto`, `Nenhum video`, +`Remover`, `dias`, `grupos` — is invisible to it. In practice each batch turned +up between a third and half again as many literals as the scan reported. 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. From 46fbc9b6ef3f56eb8d6a6280f927278b821ae398 Mon Sep 17 00:00:00 2001 From: MarkSant Date: Fri, 14 Aug 2026 11:46:39 -0300 Subject: [PATCH 10/21] feat(i18n): traduzir confirmation_step e live_config_step (lote 4b) 104 literais acentuados saem de ui/wizard (665 -> 561). Quatro defeitos apareceram no caminho, nenhum deles visivel no scan. 1. ConfirmationStep obtinha a identidade do template chamando .replace('Template carregado: ', '') sobre o texto JA RENDERIZADO do banner. Traduzir o prefixo faria o replace parar de casar em silencio e o resumo passaria a exibir "Template carregado: X" inteiro dentro de uma linha que ja diz o que e. templates.py ganhou format_template_banner_details(), que devolve so a identidade; format_template_banner() passa a ser o prefixo traduzido em volta dela. 2. coordinators/ ja esta em MIGRATED_PATHS e mesmo assim publicava "Aguardando sinal externo... (porta N)" como UI_SET_STATUS em dois arquivos. A frase nao tem acento, e o scanner so detecta acento -- logo o ratchet construido sobre ele nunca a viu. E a confirmacao pratica de que a contagem do scanner e piso, nao teto. O tooltip do gatilho externo retipava essa mesma frase como prosa; agora interpola o msgid, entao as duas nao podem mais divergir. 3. O `--previous` do Babel enrolava o msgid anterior no meio de uma string ao escrever o comentario `#|`, gerando um .po que o polib recusa ("unknown keyword Recommended:") e travando todo o pipeline. Corrigido com --no-fuzzy-matching: o i18n_pairs.py ja descartava explicitamente todo palpite fuzzy do Babel, entao a adivinhacao so produzia msgstr errado, flag fuzzy (que o CI reprova) e, aqui, corrupcao. 4. Quatro assercoes de teste eram vacuas e passariam com o codigo de producao quebrado: `or "Template" in banner_text`, `or "vazio" in ...`, e dois `"indispon" not in ...` que, uma vez traduzido o aviso, passariam ate se o aviso fosse exibido. Todas reancoradas no texto real. Tambem unificados dois pares de grafias divergentes da mesma mensagem: "pyserial nao instalado"/"nao disponivel" e "Total de Videos"/"Total de videos" (este ultimo reusando o msgid ja existente em core/project). Verificacao: ruff limpo, mypy limpo em 698 arquivos, 4210 fast, 1079 gui. Co-Authored-By: Claude Opus 5 --- docs/tasks/active/ROLLING_TASK_LOG.md | 21 +- scripts/update_translations.py | 10 +- .../live_camera_session_coordinator.py | 2 +- .../recording_session_coordinator.py | 4 +- .../locales/_pairs/pr3-wizard-4b.json | 215 ++++ .../locales/pt_BR/LC_MESSAGES/zebtrack.mo | Bin 162846 -> 180629 bytes .../locales/pt_BR/LC_MESSAGES/zebtrack.po | 968 +++++++++++++++++- src/zebtrack/locales/zebtrack.pot | 883 +++++++++++++++- src/zebtrack/ui/wizard/confirmation_step.py | 313 +++--- src/zebtrack/ui/wizard/live_config_step.py | 270 ++--- src/zebtrack/ui/wizard/templates.py | 29 +- tests/ui/wizard/test_calibration_step.py | 2 +- tests/ui/wizard/test_live_config_step.py | 8 +- tests/ui/wizard/test_wizard_confirmation.py | 36 +- tests/ui/wizard/test_wizard_integration.py | 4 +- 15 files changed, 2469 insertions(+), 296 deletions(-) create mode 100644 src/zebtrack/locales/_pairs/pr3-wizard-4b.json diff --git a/docs/tasks/active/ROLLING_TASK_LOG.md b/docs/tasks/active/ROLLING_TASK_LOG.md index f70378b0..592cd9f5 100644 --- a/docs/tasks/active/ROLLING_TASK_LOG.md +++ b/docs/tasks/active/ROLLING_TASK_LOG.md @@ -59,7 +59,26 @@ no topo do ratchet; depois, atualizar a seção "A migração" em 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`. - - [ ] 4b: `confirmation_step.py` (55) + `live_config_step.py` (49). + - [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`. - [ ] 4c: `model_selection_step.py` (41) + `detection_step.py` (28). - [ ] 4d: `discovery_step` (20), `custom_regex_dialog` (19), `file_selection_step` (19), `import_config_step` (16), 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/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/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/pt_BR/LC_MESSAGES/zebtrack.mo b/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.mo index 8c2f933372e9005a8a3848dcf12b509dbd636c20..ef85fa08f377700b2b45aa4598c8e6c806afb677 100644 GIT binary patch delta 40778 zcmbun2Ygh;-p9RXLkS(}ofD)al+Y0ZNUzd+6IhZZSxB-Wn*gD>2m%T!CFojE0mY6A z0apQQM7=61iilpZi@jjQ%KQDDIhzEp_df6Yd7t}y{LajoGv(iA=FEm$&sMr_O_lJw zwaQ)R;s3^0@OYZQJGv<6@wE4svKPq?jXTWRGBVRLws%Y#rIzYW#S4^WbrhiW}5^697N@R1OX0${V z;1)N(2i7M49Mr)64b@>>w#SnM+d$2HJhWj3RL57r1h@<8{b!&Ca1w?!(yvJjgVll_ zPcobg)$tWjD%b%vpf_P>cm~?=!W`4^V5kYqhf={tcrm;is-sV#+WP})01Xy-JdI%c zMaW+b52L__K3E;Dh8oBg*cLtsFMwadO7Ksp8CA^nc&fl8sF`<%n$Rey4l-d4xE^W% z5hxWD!3ywSxyV1B#H((>TTnWFA2L`^y^zP#6|RRZ;2}5)z6<-oc6mlZ3*kiayWlwZ zGrS89%Qvd}79Jp9f3e4dqj-+Ony_nli4oCYSc`(`ur|ze^Q)j{RtT%Yd!a-fh1K9Y zurB-p)`#*%wbKAfV(nlY>yO2K(d%}jx zjQdT1675nb<12(iVH9d$e?bl4!sSL{J)l%N9ZCfmP&2;*QZMYej)Zh}w<+*E58ILd zH>?9I;;fos6DY}afhr#g)zM5?9%e%|oCnq6TBv&4pd@z>l&T+wmEjZc0{Q<7BqZW@ z+y_5EHT;)Tfl4b)gBL>8>jH6M&j=`yKMbeBm!U-7`ZAAa66^_Wcnwtf15he^63QrF zRyqB9-X+lpeh06IRVeHXcR_-b=QzxR{Z<*{+YdEk>vA)*ick`&1LZYsq1<^oREIlV zJ_uFsF&Krf!>|fAt~LYM2_>?_P;Pe&O5}gQ%Pqva#)x?96&_C_<+nqr>Lu6>o`v0E zhqXrcsjw;ebx;zx1D1miyF9cO`Rm0eDUfyk1lPlJP%2oz&hRE^livd+sh442_&JpS zx8Wsq&=pFzL!fj$4$g$rVKaCDs@`$f0G?itXjO2I0?qV-E6w843Q7`_p^Rq%lou?4 zGO~wYB76$U*uH=j;IB}+^;~5pR1>x)+XzY(1E5qo77l?2!|nsm2GekLh?G6ep(@-C z)xhU)9;~_1<5>hl@OAhJEDs;O+I08?R7Cw3WGtSK;Bc6Ejj4A4Y9NPTRT%yk361;& zYzI%lO0d$kMwGQ-C3N2m%2pCL885gPN_CT=B((t!g||U@#c3!>{RL~l8k^0)n!{G) zd%=t3|8q#Rryv5=@iS1me;L+39!RV0y_d{|-t; zwYC}+C%{hRyF*+4Kc7Tn3f9B@@OHPtv_gEG{8dnrc^&4!bFe?m*~XyZUg(D-t}_F9 z5OyYi40eTa_;eR|5!65zzzOhbm_+}c_ee+rm1wOS>;YTAg>W#u7CHlf((O5D!-h8) zQK!Iq}LpRc$P`PZl5P7=M~E zumju+_53|3k+-?obT|~s-*ceKuZ7a}9=OFqg7A0p{#)<>=(`p9k0!DAR%1NpAa>>% zdYi{X9nTxE77Xn$I$Q^vlfTZ*AAo4X^9-B>r|mQWW)HlZ4)?kFnYSBbo(mgNZy}6> zYi~#X675wK$Q=&2d>SfJy#g!3<51TB4xA3ZhZ6ayJIszJ6-q*@U_87L_Jg~iCh#HD z`#-^Iu*z;@ybZ%7E})chTnC|e}q-YSGm)? zUmwc)+d%cx0}g`2q1p{!O+pRd2r*>OLr^Lha+k+53m%7>S?9ZragBuXibZfHJOtHn z{d-JDNl+8$>2j!BJ_)MBRH*)PU`_e|dJ?jM8=&0fG;9SM-D_Ux4=a%$4JDDO@IsjG z<~Kqb^4tv7@cnMS7}h3#95#WU!3wbaeMY5KVGH?x9TIx+BB+XEphUJ1#=~V$BVPk0 z>do*AcqeQGx7=^)-w$Q=hv0DdHq?MG+~e^qhh5u{VQu&>tP8(~^Naaaew4;#Uspd?iDF(b*A@D&UFr;upEgUU}BUAA#K3#!2_ zP$J#~rJ6%f9Xt&?!H;1_SocYj9|C)mUksPReeeuy_mt_l>C@&!)D>PN|DQ=hqAG;a z@m|;mJ_pr7xndI!x;SXiNq94q)#g2G8d?F>;YOD?!pq6u1tt2V=Zq~}3?<1i za0pxgWjhbS*7Warg@g@%h8Mv4&zne?1iO>J6n2J%@IrV5R)C*F8QXWT7WDkfOr$oH zM3Z0xI1K9jxv&k~3dh5PFl>{k`hqdC4lcVwiLe*!3Wq?6t^lgzE1^2v1ZTiqP%8Kn zN+s1_G@|bdE0G@vAB3ZzAN~OqH|D*B{PkkwB{P$|VN3FlK#A}a)J(pIRbkDeMt4o2 zX5I~IhJ&F7bSacE=fKMF8Yrv30k(zr!z%DisP{fPiu`R7e^BrPZ2Gc!aKS4k^0kFh z$w(+0nFXuCT=)4ZsDbT(J>VWF349DSf!ePc_e+GD*dUkyGoWl>Lzsj*xCctahhR<2ZoEpRHl70!h}!*X!yantZDD3NDD1)2>|9qopiz+SkHfxQUjb`wsR z@&H6dVb2;8YT#N}AKnKqfW@#jd;@CcpFq8pwG>6h=Re8%iZvyL6-X11WKKO0ZU>0me{%SY| z?t_|u^=~85+OQM(CQxx;G}Ocb&>N<~l_b>Bs&~wTEwCc_-7o>}fqMQ5oDAQG>Y&Fd zW2D2O8q9$8;R+}fY=e@}4)^(eQ13kfo5Eu-+?vFSFK^bM2_l#r) zK;_3m*}!b5_fnxmp9d8mHbO~qEA+x0P_}hD)Wq(Gs<;0=*c|D)K@D&!Yy#85 zB$|`B9BQOHVGmdY?|@&xIdIb_1ReM-tOMtrHVrI+>S!C3PWQl$@I|P4zeBZK^HXD_ zEnpk+!(nR}&LL5YL~fRflJP#ygYd%~*Un3K~G*p~bzSd0EW2T8PpM_v93za!u3TQlN% zWU7--go*GXI2O)@lEgz$qArHg`6;OS-$SXwI%6c#09GY$Lp|>R!!1elA+ZGdU={cl zRK-tV4fs9m553=;{rn&}lKfgIoxcjzj+W3Q&T zwt;==pf_wlzTi*O@m8pM55ZdSsj!3_# z`xOp@ZO)lZY!GfEzYj`7^%Ybg zDi>#YY}f{l@uDUu2}R0VjtCDx`Tg@y13v~ekq@DC{vAw)l`EL?UQjkN3^tJePa<(K z1zB)1ydNqMc`I6uZS;j|FbgV>EO)sIs-d+|9c_gg!2NJ3d<@osJ>o4#m6yN@!twbbKk4 z3NxXM?lLG_*aE{6Wf2L9{uq>Q-hgW88<*ZHmJ`Y9xoi(LlYvm~Hv&o$lc5%sd2abt zPy@IQYJzvb>F`_F2ac#}g`F6^uBzo68sCL-$41pG=f-0s)X1NJ67eyp2ET+p_#3PY zXH_=?OotlCN+_e=;FjMB)y`q4MeSuMN&Hxy{g*^lv4)v(eW)3=gEGpCpk_J;DwfZO z8o&yu4mLvR{4S{2{|J=vz7AFYODKt2H7zF+HiGJS7}Nx2gh^;-hhcem0&a%yK>2?b zJ5e=!1(Xrq2G!vnxDs!8+%4}@#~9m4C|j5VHL-xp0;mCB1vP=)Py-F`Cn1r&j zqdN^X(hKSu$+U!eu_si+!(cL;>XxsA^84#xZ}=P>0^{nLnU8=n+QqN~d=R3#u;&yB zS>tC=9aLtIZNr9eAWVVkXeHDlazB)c-hgu351=GyH!$y|KP9*XSfxURJ2&&`zphW%+ltli3a8n?paPP& ziJ4FpC|ha-_5MJ&JOo?G|5qyk?}iGYFT;-TYxoIl+SKSYu9=B^)u2??&dv9P3N%xo z0@Ok%_sxOCAkP(0)_-+#Gm%@NCipKHmaeMWM#RIRmenOtetZ~eKp(l!y$P1H&Uc2g z>M2mR5QH+WFqEiohU4I?Q1K<9h2`uE`a#tTK&fnP3;bX1bsGih@J=Y*9EI)S?@*#o zywHepqRV-(BIS#q22cRy6<0xt{wDYN?Jz+8J}9rK+tR$(9F8X6rX~I_(d1L$gX>`i z{2UH~6IvOoz8-3z&%=K36)4-N(b|Z-DOATTpw^IfP`bVxCcu4Ak~t1#3zgbfp0%)Z zn1n_ig|hzV-Ta$S1N#ih|9^8?H__DV1T~|6Fb$4?vi1j{Y~cvRy*zJ2dCwPn24#w|u;BNBwKuLIPvZaGf*z*X9BnldIw49I21K|nsr{GAqzmw_s9F)HYIvdg6 z4K=g3pi~my#n{Sl_&fO!lnR1fE$2wK14;$2Lk%pxo1Wu;-ASmS`B1vL4%#s4=1)So z=WlQfw7VMyq1@*OPw|s-vkFS)PsX3Mf_9>}dwn4z{F!&uq6~4U~u< zhC`v<%ZxZ3YG1G&YT0}ZN@6uq%#3?Mi9QXg{CcR4pM;XY*KWR9Z_Cr4{3TG&uZCeI z9wng}oQ7(kZXe6pO!k8cMC+k+9);?t4qMwqI1~YW5cfEcw{;wD6_p_Yu|HGj=Tm_}8hoCzC1m?nl{mt_OP^$U_Rs^ts?n<%(_u;tkSyAH9O?}5jmM*RFxOQ&g?HX!s?W7 zfeK*Z9qxk%+=Bg3x_la*g?~YX=3mB}7n)5lY!9o_Q3|Zd^I;QB0Gb9Bc!E$8Sq5cO zcR?+`$KY<>`xus!|8JOVta}rzN`)g(>-%d^4ZjQ1;kQsDpE|{ed_I)z+z&OAQ&6kk zmr(7TgUw;psYd7Rp%$$%Q03{+`Tn<_#8L`wg=(PwG~*`ipgPQin$dEo0j+};)ZT0*tO2is8u4x@ z5gu`$pM(l1-$4za%1rZKAE@$4P>WU&N+OG)Ca}hRz72*|@E{4T%TGe->>Vh#sWi(h zmwlnEc?eX)`A`Gh3?+f>ZvG*t_6|cy;3(8YK6T5_K~1RUr6$7Km*W5GcmM@5l1pJF zxE^Z6Tc8@e)qVaTl-s-oHGt}~%?uks9bgimCeRA1oe5A9n*}xSAe0SkaC!S|{9hwF zOhGUB61)gjm}C6?VyFSmhskg?)QtDK&yPS2^lR7-{stA&ThBEe-U*wKe+tS---VLQ zSMKu)VV_yA6QE|$1L78*6ewfq2g}1TE+@IqXSn$}P&)Rz<(W|LErKiHDyRwm3ERSI z^Na*~LQO0@o`kkiSx^=8p+vUUiI;dfzN~* zkk8HMK((_NB9X9XGYNHkD^#F)04{-VK#6X2nlZ)&FhTX8MEN9C{g13g^OYaE^@SClcyta=;8E8_J5eKy4%*hl&rA7Z`U6 zK?S6ZP|ND|a2-4V*TOyvO@MmZ<#8xUoQCqIpP<~bLMFy4w`)d1GaL?O{i$#$ycz1n z521|d4Aelgvkccm&3p%xsvd^gguV{d!58pC=*>3ITSJu(f(=*$W@h96{V2!_nvv~? zoyZ@7772B<8mi-)pc*{j@;Rs(z6JH- zIVg9kv(iW?8A=70xSRt=kPkuG(qk|HzkyTWw9AZy9)y+T{|8BE0M9^;{2iznf9tY5 zHlj$^7)kArp%|LC~hI~h;2~36RFCT_&5?7Lt zvE2jJ@w0A)SD|$J5o`^=bD!5=Z5l{`l1w+Kf%S(P*d&+?7eE`{2-VI(sE(h5n$X*; z@qdl{M+!9J8f#1}Zv$0fB9vETK}EW2pkvKYB7X`x4MC~oN2vDVt}qj72GwD2sDX`f z^J!4+E|IR(V1$AjE!+h5B0qGUd0`clJ6#V)!ksV+eg%8O8S9N(UJup5A*hCrL9K#k z;B~b7D^%>?bd?$KeXu9_BViI9Nc;sag6%h$3TaSlz-3V7_rbyNBPdBFZZr+{cR35n z9hbt<@MbsvIb1=$>ea?ruY%pk?|?}#{00e)_#BjxG{44F90pZ!HkA7nLe+Z|%2wWj zitRr`8#cPu7*}s7)l7$yNG_CwuY^+JEl~X&hSUptJ|-a@{t4Al{Y|EUzEDOr1?q(b zF1JFB{2-M3eGI$6-=R8A-fW%^gL;1v)IiokN$6%M2|W(G%m1rxF*6$oCF*%lMsyRD z1P;Ii_=U@=Tg^a|q1!%DNlhVszXOYFWM%UIMR#GS=fz zM)sSVuYRjhK{qJX41t=6A8LThq1wF`YQp=%Bs!CL4yxj>P!g$eo3ZBBQ2BmP4NQU3 zVE{VT4JC=SP?EU?D%3s#rQ;(|0qkX{_TPf?l5gGeaQqH4lG;!mBtli_4ds@TAWq`R zhO**s;4D~qr%_2doJzg`YJ+mjE&thN{4OJrMo=nj2Q|>XaJT$_8VRk_f5Bz2;%^yIm0qu7i_J*3sNO&Bky7``Wn)k;;=lmZe z(VYtGoB|>fWV_~h9m@Si-EC}R2JAw915})N49abPf(l5<_ZZzygN|{+a+Gg_GRhla z3cL-Byhth3-mzTh~Z8teoWUNvNYWP_g@Z zD3RU;XTV3HI;iuoanlY^5p5uB0dt|YX4gT5-osGue+)IypJ6vxHEL8d0Lpv(@HrS> zLqapVsZA8Lk;A2-i0hFZ-spba-bh2;BSZ+OBjul0n9 z3ti!Ll>4A0@C#IX?VmIQ9t+zmv~43XnFmk7VX*#FrlUDfBfcE^;6bS6xyjQe4h(@B z&{Eg|-UQXr^N{VH=TG<-`L)lOL+cesj2FBP$5Ni~tmWAu|6fGnR#@vfV+(tsto0cv zUA^Y!zk}P!{{_duo1Zs<7+XZ2qd{{E0;#RKw3fh2pnee(v(D z%ZkTLhxMS`_d=-m`oJl0ER>Po873kB{Qyc5@voW4Ru3v3BtW@i7pMm2LHTz+ly29% z`8%N^;UiE3e-%nHKS9mhdfn*0I#j(BXv6RX5)xSmYB5;@^}@AKD!3W`1`j~#?4jdk zV6Q6QGPR4N6t3p}e3FN;Pl6OW|24ubT3n@q*cK z5c$hsMfv~JB;gv*L>e})D}vIy`gMoB9!QTQ1x=42DSle#&<&* z;W4Q8zi{)`2l&54+lYkRXBBJ&w?Q@dAk_26p=Q+bL(}mfxQYBsDBJlG+VFSS6*l?E zEMDVafAaUi4)7~D8aDdas3_xO{9i_O83mHS4p<)UhML)Zu%ShufHwKpKQROS5o$9U zf7!16y853R-FAjrp9euj zuFW>;`3hf|0rZDb;p{L8xzA#l z44;Ow+B5KCSm$dKA0|Q>&L8kb1@0cY&?{Ru7us;(@Z>!jX;U`MW~M7hU(}8s5Rm&RQ*Xm8I}2= z8r%k@ikskmxErRz89$r7-{Vm2`~fAw+P`2M^8a=uR4@rj_nA-~Y=Uyf9Z)kq1ZAYJ zKpEHPQ1$Nw%7sn;ILmij}5@*q_EE18?A2e5m3|J@QLW#MC|g4< zHvOP9xg%>h|VyHOr>7Rx_K@BkOFXO(op(NQ9N+QWnUUI4X-2WH; zKZ$}M1#0LSI0e22vtZIW^Fjnl8bN`ejIypCwwL7RL( zDBG9|uGQgYR zk$ywMhG(I4*RY1yIirn(3FJ3Jx!XQCnf!^GUMH}$zrgF9n0i4O+vP6rfNjY?2W4d6 zLv3P*ZEg2-h>J)BkCBVn-66pS3|}8{ZKPK z2{rSIbqy0>d-A7LpTF=YGFjS5z4xsg|dM&Za(os zlb;AR@C|ScJPh~1{w>XbzJVIxpD-!x#cEr5onJh3Zi6i#iF=?V@j;?-%b($R^7WI< zKm)KJ`NeMjVJPb_hVu7QP?GrrDpc2Qi;8Kt7nCZB+IgKl=V=($3tie9;}{R+R@XvR z+y@omK7raudtf35 zLD@v_F34XMMp2-Fq(O;(Bh(8!pp0uDycZsUl2A@p5QhDj(UzXKIQe}m0o)9zkR4z|%3N)iuTWORHSN(J9Qso*!LnbhoQ zW|jauksk`>&P$-4Z-i3Kb|{HG3bne1&yi51O7G=$wqEx`z3>4n2fugu3zR$k0c}_% z#Yn0>RQwnKrNTo{#`q@Gs&~fauTT=TdV8JE^|c|A3wvgh=*WZXpdFBn%QFfs;@o{{}i8^ffcD4JFw&P;15zDAi>_Mc6IS`TL)nNT{JAsFDB6 z^{u zSPk3uHx2B88pu;nd%l0e1@KEKH=HuSEHbH3GrtVV+P6Tx_b}8zUWGEYkD-jZ`am<$ z3!xUNUNEegOd`<%&V$nNW~hd4gKFqem;jGLb@(k*-0%)E4JAMsS0AYNrbD$Ca`QW3 z4*5e+9k(BBR>5I|@&C;fjG;gsyywyyVsv>S)X2NTTkwXFP!joUnCa*&RKC`5Q@;h& z0J=fxehk#}Oei;80VTofpeDX+c-R=#LlkJHAGj5Na`V+km;tna()C49^|IhVcsU#Z zpMgVRg^{Mc(Xbc!%b?bXN1(jwTd3W%=Mp2yq%aBjcOR$*=0S<>9+(W@gtF=iqfCHl z043@vP&yAmdCd(_?so*L!w;ZD-C(q7rz_M1=0T@EyqJ9W2nktfxiMx$qo5j|1Lba4 zz+2z}_#B)#*3@r2&OA?r67e>uSbqmpM~7fvc*ZU7INn6cp-|5+gG@Z^*-s*cg16xx zusyCiMZRkp9{v6VSeZnqc$EUQd6R4m-oU zVLz?^Zp6!DRR&>;p&o zyq*wT57puCup3OBXZC(Kz~1DG;S_idYJH!QYTjQB?qa;bQs!gCrFBTIZMmk>zqX)QC?(4XoZG!$D9w&Vfnr zRw%D|9?Iy>Kn1P~bB*6mf_lE)oeR0q_KrXdC340S$-J?P8ZZp;Yn`l>5amHlm*e6&bg{PV)a(Nc4lAC0^&RSo*`I zWcNUI(0Zv6-OL+gF$# z3rdx%;oa~M%!g^08kvn*m(0+8EunYm66s3NPXL-@Wa zL)W1?%HM`pQr`AT(@_zuLjEgQ1D=HqVEk3)besScLEBu#Nc)rMNx@lI042KI4QA2V z?($yPnvNcUdhxxDCQy9`)lv1UjpQ0Z8QU1BHDd{Ul=rTNTC9d%YrNzVsDW(=lc+}G zCb!^j7$E-$R0AzG8Kb)h${1HeMYhMF7L`}r{HHL1{I5_UxAA7P*Bb;iq3N(TTmvPk z2-M;fwzn9+?gKTm)lf4kgnHpNsL)&lwd_6%6(LVS4W!XlBZ>C#KJp`B5dIAH-uyx{ z&;qClZGke%J0Z`*o)<{i6r6(U;4i4iRCk+su_x5?aj+3w1UWc(u6Fa!K+WVmSP}jX zHSoCWjLz#rNvac6`Ea*<7Hlj3&nKaPu>+Qe$K3pzuo?OI>&>RI4XjJPo0}gAHN&Y; z16}~t(KS#r-3=w7gV2UAL2XXIhVqh%5sY5`-<^cE-Tk0OI0njH=DEBXRwaKNYK9-c zIQTj23crSxV8RV%0PUc*aD$*Gkl~iEhZ@jr@M5?ZhNa8zNXSS!+-P(;6?P_{52cd3 zpsf0Fs2P6>C9&V&X4vi~Gqb}`DmnpW)Mua~;yEZ4wY}L$W+>DIX5Nhd%Pr=25 zZ%`7fd%O7#*agldzZI&VA0Y4X`;R+}J9LJ(@L&#<$o_K zk^cxQ!$t>;@wPgE|H}=AQlJJyuqiBrGOoQ)#&!~_!LOkLPu#;M!gYgca5~g`X;2-m zhHCd#sJQZ?`}{rk`JYhk+%Ot8x@{9R*4iKHg-f9tTm+@dP0)r#Q2u)ys)3K726hH& z4XF5tsoxgr`534P%z|on5tL1>g&OFsVG^>|gDzi!^2>LjUaat_F|Ni?1L^_Q&?J`` za5VW9P{#KHTnhh$^Wfrx#(Um!`3ckj&Oi+yT=|f(=GIUJ{h>xa3p%SBRN%M<%IbH( z1b7h2Cfi0*$uaXlHg=0FIogO)61X+xD6_RJ>ce#!uIn2 zPf19pb)GaMOoSTYB~T4yK{c=jN)(;pJR7PD$2aHztttNv%NTw<`3F=K|3cmnCcIOl03HUG@G5b(mV{6eOh$SY`--v zv7O!7_NAo-@&dtZU#1=MXZll>*4rPh+GSHgLsJ|7RL$u{_1ykb?B&_MEdR>FigWwU z8$gQ%p+G3b_AmD57L+tJKbI;Yw}S!Mfh;=C^5rrZoM_R^w!Uv3~j>%TS{d;Z^=b)PfvzcrkjmLJFtQk)aa&5Mm9JRsMf?fZM( zLCi6i(m-~af2qCPLF|p#Gb1ZI@w_@Q$qx!<rbi}{Vs@n=sNF=nhLp6AQU53MZhJ2z>-gs~$^ z`*nvrF*TSY(S^iV50m@|8~NB6s-_*2QQ_>li0=B5R*(|ME?$j;C6hgQNe zf-byk?uC&Lrdu6r#ilbU*S7$73g#9R9-O;7>^M}+n;du5;2eK($F|&^w{0kqr=gvd zw*ISJD1V+>q{Qd=O{k+&Q?GAdYrBT%`r`S8%TvR~jZMeyLnEN`UpP-_Zsc&Qmrf`p z=JEgV_mBsy}WR~@3wM@M3}-qecNkvw&w>j{T>j^O!Ma!-Y{=(XO-AX0(lvB zUWPBvmSk*SCWf0qI zi~YoKr-M*Y=KNc{?fkj9yc$eR&CkunoR(y0t{71sCgpU3yJnl%Tl9OzC*Ja-)M?R) zg=toUL2~)@{9I?!cH-C^6Rnd9J7+a6KMvuh6b;Hssu-UeToUppCnpyz$eZo0cHUcs zPcB|lRS_u`ii-L#d9XqPLtuWXPOQ?RqMYNLnwnOa!i1OVR&+&m`zI@{uscS3i00#= zk66JZ=2AA#N@T*M%>&=O%s?oEd57{-wK!=pD@dqZd6N2*bp(*4-Hw03C~5pu5T3|hjSM-*_>CQd3HM5 z^9&~HJBx5>%1&hEPb%7ay{}5j0J92d8li&h)QsFu ze!0^Qm*!0sAC9;b&6JETF_ag~!PD~df>~Hxst;A!zWKE7$RL#CPYuiu@J1eiF)7(I zBwI>LgR0^)ZYw{LF~#JW;yzwnRA<*JZw(fpKpKr>`@mOp#T^UcTFx=yZ>49*QnaNN zD355Lh&^^N(bu1rRQSh&a@fEOtA-aSza9+F3@+Cg@V%g~{+c;i8I|Tkf8cu-~1v1NygU(ps z?Nk;=f^m?Q%Xu0?bcii8knK;l2kBua+o^co#2E7AkRLa+^TOxbk*YbvkC_J{ig=eF zYR~-AwML>^A0o)0!$3Au$(N>RaDsnPejt~b`WTcG7ci2TwaTZ}wBx#Fo5@q(yx`)# zeip`UPH&2xRLXKEVr1!rhWUhnG$VOuaJ)-M$xcfyDu4Ho_{KxEImS|)ov%GVH<)FI zQqfInMpDs;hnmNQhdCPx&v0LE+7chWTk2yaMR*u)=TA$)<9#{4d4bG89vhqGsU?|} zl?RQRtP;GRCD=jb2?It`#FloYJ#1j*VNQkgocxuQE0^?^Vi)BWowBOU2xcRlcI@hX zsSAsS9muJem`I#M2l6D3vuaF;Ehp%%Ow=#jdwAG1Hx4UQYBN%5M$UvK-O@;2(k_Zo zAe>bCG%G)phhdj0NkKlgj5?KLN<;KPcd>EIQA+qf;*RO;{4Em${XZj0xFn>Q5iq(~ z+)QUR4cP@ma>+^k6B2TyMNQdX5-ciL{_eJ|JMBq$5Xyf6W&K&p3${~7ct<1v1lZ#pvXL$=Z9a&dVYw1LPW>J-Auc%nK;%KEt z_OPHm4|6l9>GfQ45aeX#wK+yEY3W^p7CN~*Dmm7QZz$CtasS4~9l**-g%omr4n zG~>i+D{LI6YzMyV62Eu+eSRP}l$Y!om+AL~(AYmM6s|H#DwWJNHWFvu)zJQ4v#9^c zt`)+BJyv-=cPu})kt449a{O+5X53n-+S`(UEYjnCIl;_=bfR|R814UA^8BGBt?o`Z zu;;N2q1owUM~y`T>CUnwePT4Bg-!%ys}k1skd0k_YRtMyMxgD6-K_&2n@z;Tb=f+m zXZoFh$>KtLX?T?u%+x?`YJR3Kx4oU=U+QB-jE&*|JH~XCvkp7cA#TR@smZp9x?v~m zo;N(U+UZG-ePlWTRy~+WY4Fa5!aS8EN<3Z0A4t!}S<=u2*89KYYO4=NAmt^Iq@v)v zlgc-A0#~l~4)mkA@0>l!6dwF&c=<7GHd6|J{b)$ViTPPszTASM?jKL7RBkNxRJiKv zrtuT~d0GHNgZ!> zJae1X&i#;r219Pk&X`g$6D*&ol3QX?MW25EdtB4lSAycP=k}XDZ(e~e(#dUX| z$Ck;G$8G>N+fK(b^|tE+MSK2iW>s|pf*UL&Ki9LGS;X&i4=Dx7w@)?9Y@LpkTrAmJ*5B~jb)B>~oI-g373I7epCr>y~Y z7uzh_I*PNK3EHlfIB^BA|JbxcHXDu9g_#ABtaz(Q9D0hZjJG;opq5y_IW(KkWGf@P z;;o}`?5U&am8|z_hn*88p_z|p+4_j)Y(s5M9W2Q77)rL;6gi>I=F^Kx+S_S?^gtdA z-IN z-#L~gMQio8uD5Fb&3!_VyZTtjArx)c*J>A6E-NRmAUd!=zcH*UDTO?-gSh!B5Q<(l z&8(V3HF3W~8;tnsTEW8j!YF zi)@-s=sJulUc@STKD^1PQ|8eKEmv4dDCHj>j?46Ar$nzAW!+|lojq2}hTE_+caPN} z_U^vLe7AKhUVB}$xG{n@%ja{v()zrzO|tE-kEuFuCezuDzqjZtrhGor!8b{fx@^PA zr7aWT%>JAWr&f@(K*)F-*63PGax`&_^?h7;;&~r4N(4vS&Q}jfJIHsBSPTfUY~jQu zuE^m!6#GhATFKe|=!*r5Naf1rdqpUvvi6}w?R)}ttkJOZrPleNEmv8l0c-D6^0~u# zNnRP^bivL+XB+K#N_YDuL!%12LM()xiSwGPM3!duVl-?Hv88Uu5xIJX&KxDnVzkx- z%WH*i+95k}<|)7BKvHso3z>N71i}u^ew0}3T2V4bAa_M~ye~x`FVL~Z%Yn`e+Z_xC zXMM6W%Le)~pURm#(K_0GqBYr?+&N}Jv8@Pi6mryatl*!@WB~b&ITHaFvyEBlyuo@L z`$#kPlE{il*0N~{)D!t?vemR9ZRZrYhhA;X zbtJ{7XuskVYjLxyi)^zB+Y>pdrsipjuTKiPUP(%cESq9Y>vrvfrRTx(k6kIA?`}Kq zX)I2pM0VtP>qk#dv2L*HOfY*4bIalGGIXZuj%nx{W3uxZik-b3(w2e~22SHEUG8v( zT)#}kS@$Bj)2y3oZ++Z$_8-)Y?LDHEr(1ojAsb^~7VSv|e92~nh1W2C-4!_79#fzV zkW!oMaT#2~@D&HGWCrHpAMC_ZBA3pvx`ksc+oR0NsTXdsr?Tl{5j7vCDcNn0$a0i{ z4oe-=>3Fw2&WC^4&dFA9-^piscMCSomy5^d5jH7*)Q+9A-K=_yJTk-TUt^U;i~A7@xMj^LebS2J_YKk_KHMVievO;p?RvT4_c&U=oD4EAf)N{L)Q(+bt!c0c1P z{cIF_7_BhNy2Pq}k3EQk7T*je_|yGMBcm?0UMkl)rE9lnliAk&<+`(b){R{8J&OZA z6BT`FK4$~%%gkWk?e4IGA+`WEJC7V6_j1u^=2&J*OLAx} z5yd-m1G+rM9BW&f z(sULYj(QxrLV?Aq^PiV^0UuiMXO+C~zCJkM`;Xqt9bPfUK<)o_hZknrc!f3O0kC#fig3Kbj!Y~OLovsbW%F(x;J}CwtKb5 z;QsSwFLKClbslowEtT8;5Z&p?w9dQRo5D6;j)UN0o3ohsA|Ly$Dapff*_|2ZQiWhH zN4_+P{~z!9B3qP4&F1KBx8bOT#e@nb6~00rQR#Su7yAubd}GH&(@7C^IkTzOQUo z=RNydf06I!TL&uXs?Uj8kvr0@Js0ph1J;AU;vjc^IV>8!$hZvaP?wR#JI$$`FWgzN zDhjDw8?hp0^Yz`>d+a;V%1Gsa^<4P;_nd{6W%Pf&$JY#2W4BUl#Ibl3Vh|h&*e$#J zUN%H*d!3-2UTPoa>aTcLw%6~Az7nvGdh1OL&SOjAwoTj(+EbWhlz*?eZd`qGy5r;M z>UHSo3R*Se8aVfSuKCgDfLI`lK9Or(W(_Y{bB%$jhI{|RJz#7h z80-sW%nSPXPT@ZEj3e0jWb(227afylJyo#^1Y&CSSO*4A_w*WlXZTCB3Va=t+T3IVVzd> zl*u?E@MvMz1zKrI?2)6wvKNQ?WXWBTQ_k%nRuSa00{6Gh3~2`g(#bqjyk{|cPUqVQ zvDBz8KNRHhFgj(uwZUp$yu*FTG|$OI#1o&UBvRB#Rs10p+zdD|sB=_$d~ z;(HZ2VqZxWOB}m!dUaImj3sjU7OPIA##L6!=J83FFUvD3a@9E&ofPN!S?6=wsbA5eI>C1c@A>&*?Y87Bf z%VE1%@>(qN=neOlG7$7cP7U?euk#P9pLxQy4M2_ys_SljKx|U4E z^)!LfIWgGcn{?9|{twI39E$f4^Bgl`wUG1g;ck+NkdqTS6VJCf(-UxX9ht+~GCMCw ztNKokm|}LFN+{w>ymnI(uMxSqcc33L8 zLgd7va}VQnuO}4~^{c4=zt@w6Sw)G_flpa)Sr?d79QTvCEYE!C&5hP6wz|i6F)Ny# z=xmOh4`9W7a8!PH7Q(nOvfY!1fl%++Su)wo0mF%NfFUvFM8 z^3=uN+O^CYIiFvW%C@xvM7F(XJ=INDmYM8zO)z$XhI4-+t18)TITxEHwNfIJUb3EP zKEb)vH1BaX&u8n7(V`c=v*iUt(fUWNYvSsJQuWO~kqYb*Y+u$qR&v2f%CG;-Rz0%s zUF#idVr1jzR+EO3MCts?NG5vCdsd%H^~NbOBLMeal-tH^AMvhG)F&n3FNX>DKqszHsABv{1Vy9E^Rqx6C1hsORK&U%g-C1*_f1UznogpAHJ~u zsDL**cX?<;?ea?*$CDL}VtZgMI5H2u={i4tz--%d&67=l&my36OIo~#OG)=dU6vN# zbN+3q<5`OKS;ad;srul7dd(GKIM1~+-J`bGHf6DYnI7|vIDc6{D@JS_#91ayj9{P5 zy{EHt>?Q-jw46ZBj)P0n5;u;$N8EKUP<2leyF%qlHg=QNoK?%6cgcOKxje--=`J$1 zs<(c9=R%X?Cm&KBcho1_%wQ<8shzj(C`BJ4i7|VEigQgWi)DTPzhql_Q|i2)6#eFm z^W=F2| zdh11wdc9Q}nNM`IqR&7GxHO1G-tc;pE?bkM4>%zQSt!CWJ|3l=C z#ofsGtycZ=ql2uO{4+(hqh0EG$69rryI0w0neT&1(Qtk5&hix&_!T81O`Ced6HCr} z{QAU+3cR6J-TAR@=>@Fw(M(=r)(YnuHb>VWPT1D|PnWQ66uh~q_ce118#(ihRfmPp zIe62odkt&OG*sH5sb}-tP9prOfkQ6I1Zl#oNZ`sZ2?)L8;;L%z6 zD8{+G4K|L2QvQGekvNge{eTgQZs_5Ct4{w?N6K^VbJ@$3F*|e>JBV?YyENtet6Y}C zyQv$A}yo?;X*Yi4-^-uf}Acw*mFoUc2f=-0EoKgN%CE@agbAN-sPTq*tUSGVkc-K$%+NdG;4 zr9|IN_eQN|&JFF~YYyO2moKYnzQ~#7-r61Ad)(ONEpCXj5J#QQ@6MT^^g5S2+{l?9 ztvb=dfcFshxNgW%jFHToKMdfvhWxEVkOPRmGL&4kx~GNn?{S@><+3j53YR6_gsYOK zN-uE#vCS6Rsj%na48wB6(&Z2F#gyG|$q6qxI&z`6cR7v@B_A$JK1}(`Tv2cjQL?CN z<(ya=n>Ddmlqs#_Y~h@%SNW7~ZcTVBy}Dgy&V5e9mCUDnu00|N^}Kb$T;BRZ&PG@F zwa(_uT-tK(Ed6yRai(;^HaE6z^f>R*mg&ZBZJqA`WjssgF=u1tEEN*6ZfhsrwJzwj zBF%EV73yoEF~4;VwJVyC2jPI9Pb8eI#;po zw=c7!!_1tEQga!r;9yQ?=a-t>QD;l__X}C)GnhU0l4zw|Zw;&LeXQw6=ISDJ8`2_=guHg6^X_PMem0l1EDwvSi-StMt1AX{&7TJ*5xUlam6A_&7%)KoiEA$(&$e6|6{q>z| zTW6Q!R94_q_=uic>`k|ZZ?ySwsxxXLk-m?*x3>dwd|H3?EuJ}Wof}JSie1_|6SRjG z@6lCmN@V3yZ~xNU+d*alC$j48f4ac^PqCRRUDpNl+HPd4^BPyVK4%o%c`CGXgR2`N z7C~+N%3S7_*AJ;vB5yDCPN=ua(VspArE^D$R-9{vNMeEap~y=G-WEM`>7wNJmR{Tt zTXJcuyV}xk9Ck<2=KA)70&lK&+x?Ma`>i@wN_5;Z?_8_yJ?5s?9>>)V!-^t>?$i;uw1h`yz zFi$Dfu>JqHGR3^qvtk=ekNvS4j>3_+A9Y=s4lY*&_Q4!D3&U|e2IC1#vRtkoFwEt0 zyJ8buuIzm9DVD?mSOJ$|HoSz9_z25jNGF%8JjP;99EuwF9BhKykd9riaV%!)Yz8t1 z%TeEmn&XdHp69!QyOz!+jXm!{y?CuJimO6PD{q{I#ZaDP+O+jss95oq$?v4{F96Fa*z{ z26zLt#($vh59noP9)^C@i=!e?8Vh439EhD!Idi@j@vlvxes2@{Sy-0(71Wvs_8~-A z5)0}&tcU$j1KNXn@C{T%o}!i{q_2r&9@I=Lp{{F)3VlbXKDICMFG0gx8Wf2`sF_{F zQ2fnle~0QIV?Snz1yCIqMLn=G>i!0(h$W!1y)UN4p{Se~kBaO(r+u@Vf;!&gTyWC4 z;0E@m{W&Vc9jLUyp{S0|VQc)^sh1sKa;OgK^VX=y^~A+E5u-7T_%jz*BTPp3J_=1J z6i+fU8-eMmk3&UbCYHuESRXH72<8}Mj6_`*gI;Xt)UCm0069>RDT~FiJ}R<9aH7Tf zPot2DhTx%u6LVq-tcB%pAeP7Vs5QQYdGTM&jJcSGKbCZiM%`Bdr(-`yrl2}rjas|isI@$d99piEm=7a{o9k+#4xCS%dOr-MJ`T&^d{jh_ zVkJC}nsCqvCd~6)VH5%}4l`l{OoMGv+omfj_^Ino-{KGIDg1BGR%HQS0&cm%`oIp)TU6HI#v zY)HK$YQ~#z5MIWbSbHMNgHx~v=9y$3Gzp`rZ@?J5j%6{en+4NIV(}B~jKy#dYKFH_ zp-MBwWPf$6Lp>3F13)d=Wemq>s7QuPHKC3~_Ssi)3xxtimynJ!mT zp6~jPLOmLq&T_dv$A!2O3(hucdK9%pS1~_6aO!?@*s9dCU~Alj{`elZ@IZB@`qsH7 z8NWcy{2=<_6)d6se}h7L8vN!NGhq<*yr^V~#CBK}v*RMngF8@>IEy+T?qd!79hHnF z=9~MgV+i%usHE(QnQZPcp9_gO{e}R zhEms_(~@MxK#WE$VI?epHBt9t9FU~}Yfbah7Urk{|6cXeK7-T{wM=SbLcm#a6H!`-;jO`!vY<||CnY)7rx z*Vqa#Vl^zpmiPn{Q4!gTmGA_%#t*1Se6q^ylAhR)`ZTBhH*TSxWwp8g5SFIyK1U%x zg?CsSbFJYt#JZS*cd;BMuQf}s6}2>9q1OBkhT$_*$OG1yBRT}N6m>BGo1kxis0jB& zmc;EEOF=B8c^wN!1L`cUjbeJ*Cj|6v{s z*kJlAfSt7e%TQ1UpW_tVh6-_+jpmC+4b+L2fLem-SOtGX?T&1lT&~7g4V9#yp(eBx z{c#Tlpcm8P5&RF&U~Zo8IUqU|yVyTBDt)r%86!yWJ z=s_jhpk1b;kr+aKy5l08NPRtO?IXW5OIii%Qg4L$aNd{1Uo+T6LpWYXZNJwTgK5~L zGR+>73q>%RdQDWshhR%w=B5x%;TbAPLQ;*P zsL7T4g}SdYhT|vr0*5;7y}Z72$L*Rx zAu}JW!ECq(wU%eh1+MF;fxW^C=y$-(yb5Y<`=Ivyc+`xRVg!DTTI0K@{{KToF!MnZ ziEzxR{U1#sh7TH}*4B+mw)LnPT|zCzznIn{A-^&M3_WB97J&+FY1DN!P!nj3y1yMN zS*M_qc{Xb87h!ts|3?(s;7ja+O%9ti-HDpn9#qFaU{1V++3|%_PkY2HMIp>Xdl}4u zO;9uMj#{GOsDaExE!_fihf!EbK^^W#h584_A5ly42o=f?sO$24ZE_<9HPhOdnaH$6 zMWWj`=JRCCLHiUeO8bg$O+P0wlKR7MiGMQ+X^)!Fw8JRsT`@PNphmt6ZHt2l^?*~y zO#2PgjDE)me1ZBr+i^4V!kC|WB~(9cQIYA7is+Q%ZWD@m&IQX+A=`y{@hIwi_yyH* z;0e<~4*Z*X8B|ggIB6mgjjGo`e{6=juN5kC-BIVp7*ymYxhW_q=AmY?)VW|S#!=tm zeEt-*G%rvcx=xvaXGRS;59-0CQ8^Ne>#z;#zIV>&L8s02*|9L~?xGZwWX(~z&=)oH z$*3e*i$S;p^Wk39T3$izntxGC6?KMxutZHzOWO3T3GoUnOMMHf|DRCTzr+GO-<9b* zGr|~D_SQtLSpsTitb(^N3bS4?A*_ob)LUYD?2PKLKWZj!?1A%9x%3)?&~?$A zE5WG#yP&S`fg1P-bXTS@heBCAjat)pI37!1GBe$Yn#oc0ts!or{t`8l<(K&mfT^fl zYH-CwsvGJ-$yf60CcR(#cUsNO}q9Qi~_4#5fgd49D{}B|vqd^BktsCaTW|)zB2ds(xP}^|_ zHp1tqEUtXh{Fok(8rUT4kK1u6hTSs#?LiIX8@$FYIgNv;uXO)JcGHmIwwY<&pH0@b z!8%+p3^lN?FbYqhX7UM)1g8DPyyV;^DWsvH zJ*LI(j!Bq_`e@XHW??J?U5#PXf4*xT^a^!d)_Z0tA{{HCmaZ{sKna)~$Dks$03)>j z4^e1E!_Qa(qwkxMC18E(v#=Ik!G)OpfmxdEsDT|qt?32SlH5TJ=pAaH=^mPa6hd8B z0cT=e%%lB(kwQKi{=jHV|EmdgbyN-{VSaR@I$Vt!=m9K(577_P|7LbWCM-a`D28KW zY-W=es7M@oY$A0FgL%H|83m2_AJje%d}7uzE0(5S8r7bFTKj&efsDlJI3MHjBI^7o z{M4LyiKzSLqd%^8+<@w5E4r05`zdGu7jY!s!>ri;ce7?m7)X7Z;~Z2d7h)q^kLB?> zYQTk_nf=`gW2lcuCEp&bgikOE3;aR+(@<#jhgr)6RLBRTl4l~4B(4-xi1(ppeh{_x zmmPn@Y}DU5W_oT0To}{QUJBD=Wh{*Koc00w{aywdM$w>|e1`4t3Rc5Xf0`q-FIJ^~ z49nn0OvcjxGXq_ZisU|2e-|(j?_gT2`Ii|$V^os1MGZLFO(8RdnHY&HP$$|k%!t=e zGyN4cleeg(3;5g2uoSAJa;OJ4LOq}zX2M~pBY7I;z_qCB527OJzCa-hg~zB7zemj^ z;f47@WCYHmJ`S~iGrTk%Mq&u{ny3dg!*OJJXVjAZ{K`z=k>hLBz=B?zffhstC1ogmwu>uxN%$|MEgJOzh=f^7)8A{&c$ThhW;PSjCNr#_1mZj{EEtjm#F*ld^GJ%QMuI7 zaT4aEz6Cj}T&Gb>wDUjWpO=D{f|lSKs(v4JVCV~-mLLFwEaipeOV$X>@-0yeY9^hq zDy~IE?q{rn>G{sC?bZ@CfRU)rJy-}&*_PXvg}>9FHTL(jd`XfO71Ahdfjv+I+mFTY zD(1$Im=kmPTfPIT9O{8pQA^Mjl?(GwIdudTxyO#L{oTxrhQI(bfDEW?&WCC*gMF|f zDr9S&_U)LNdMYO3N$ib90xjRKTIZpX^D64(3?zItFa))0nxZ1y!A(JH+6CjW4{9yH z!3g{xDiT?OE#F$#M-4E^aRO=}^D!5$N6qkS)PR4$?sx;0tkwAQn5@_ewG{5&6m&v) z@C>fQZrG0>b*tb7?22jAS-!951F$9anOGffqau_$z2*De@l-5N{XS|5!a__W>S0sr zBT-9o3fZ1+*9!_8X(*Y&gzz$Urd~CpS?lGvjQUBuj$JZYzN5M#KXK~8lTZVBhKgK? zEM{gMQ6Zm&TKjXzj&r?2UH5mW#g|?;2S-*D;!jZ{n}xdI04hXJP)iY(&2oif6;!{ zjP+<=kIIRss8Hw6W%*uK@mP%d0@RETqmuBE)1EW8d2nsi1P7s8BUwwKCZ2aL$e71m zSPd0{KB$hDV0}D>WiV}C%XdOn!8q#OQ0Kr#)C2CJj^Hf$OfED=odcs#{VvJJ{;y5p zG!42TU4F~=i^yuI2PC6rx&b@lEgXUs!p-MfQA=?P*=er#*q^#xz(gzwTj_I*!BPb+ z-&eCl)O}M5x=r$IqG2};zoOR4Q^<0y!6K2C@3&TGP$NEE*lfe!QA-qB#DuyM)}`JJ z716Cu`*qYrtfHnp8jDizgt~van}TMz1J~j`9DtLHS+2wQ7xEr*eNo(;gj-6OUGf%{ z^<7JnGt7J~CQ!dx%5qJ_vZXl(Xy1#|sUI(6BG5R>^8KFCjTvZnPozp@!Y`B~J1uREVV za_V+fv-Y8=ms)*PQZ`4o9^j^+ku5{bc#~8A3f18$)QoSSM*f%6o~D|4Yv#gm+KZ!J z%Z*WM-V4*p>C{?ibzY;K)a*X)a|$gGg9A; zmGLN6#P?VPW2&10bwF+35$KyB>hrIvv;Q@+`!tlmKT%7PyM}q-a?C@0KPuU-pdxY4 z`TRX*rJlW}<@*7p1d`RRGN`02hXGjIX>aJ%o1vDjwcGii1L}q@I2MyoGkbNvrFor&Xta+{(IpxoQ6{| zq!H(Xk}X9A+=vRf7k%3fwcT!@vieV~heaBj>jt87WgKc??;Jy$n3u-1HE7t})I9JJ>Y#apzhSXv=9^8X=H@F`XRJp1HjKqbs4pOqEzCEb4)_!G zRk#F4w=_QognnZAeyVPZU1;Bfq>U@I75iTy`n0u4mPE%S)V3OjF}MJg-KS79y@g7$ zzftEz)_8M*=0jzDb=38JP`NM`b^SWj^G=|a>O#ERB+VZ*Xj=q-YU){0p(=w)n);|4 z5>QDv2DN0XQ0K>&s0SWFW&K6e1nxM#K=l{g#`GVKy1%O1DKtfmv@`0)si>@7hFZhD zsFUlGq5{ZD@HJgGiT!lK5 zccBJ!1H=h<*FADRe~EgR1a;H|*nb5m z=wzyb8evb&j}uV)b`5GtzClIgD(bpFP!S7CFdb$`4Kx}R;%2CUc1QI$0@aTPms)(~ z!^%A0Rj;$TVFGIH7Gon^iv#c(>T7lTF6QJ~jC#N!RL7T4XZv5ch>qW)4zRgh%|N$f zDe5P&BEE3iOLTL-|JS9U4~AeJ+=g1)KT#b8bvHk|6+~tKU>u7JQMr=2hk2hD#bVSG zoceU9ei)n5{u^q*<$9XyTlZxDN6;{X2Du%z3w}T)+Y79Oe!a|DUJbQvdZ4!12-JO3 zQER#x^?+ljrMQpEt^d$>#P>GWmqGp1-Lkja{N&P}hJiG!K%MF7`k0qcC9FmLbL@nd z@DxV%HOcxKPf*X_&+`5KpNFUccIj^-GX#|bGf->4!f`ih2~W8xD0^?CZuA>qW|9rH z1W~B=2B?nOV*n0t+J|Bp>QiwB?!)<5W1!{xdqtN}6DXBrt{a2}sjorpGWQ7z#VOp! z#uzflEI~WeK6N|w6jUe=qC)%~Y6eeH19J^F9fzW3S`nkM3F^A>s9afy+C|$<-R(L- zK^^>r%Hrp!2fso^AngznkpifF9E)1JMyTD=3iW_asE(7J_Bp75EJ5|Z6SZx>MK}J4 zxm9XE)ckNc92M#-*am-f>aodY=FL%`cR?lF5Zs7!FbZRaS+3F89Vg>2*b2K1H%sP4 zP2dPB^xt7AjpQ)}?f2{>%z={VI0!W(H(tdhPJO^g)4?=UNH<`4Jml2>LjLCp9%XjP z#L;H$=VDo|`vxoG-{{u%Dn7>S+is}jn1c%aZuG~qsH8iORq!g##EfIjQY~}*0=0dQ zp|;;+tcd~R%*5)UmNp)>bbZD-@Bd^Py3pW8?bExcnf-_ADEoL5`WRG1>Y`@S33X1) zM`ih1)W9F24x)cixl)*IuB9u3dKtx`Cfe9dK^-PxGJb|SsXn0As__K#fVMcBdT(ri z|Dawn)hAlMzmh!whf%+YKVY*-=KFn~$!4YnQ3ETD)3F{V;CVNNKnf+Nm=MLFPPSU; zhb>Sqr`D)1C=(oKV>#A%2`amPm}-*pE^2Mxqjp8iG&8}5sL*$H9Pa3zNkO4ng|T=B zwQmEbn^09kJ)jmUQt_y5H4t_1OvVD3iaN-yqh8}5P$7@}%nYO=mZRPj75YiYE^xax zQP7A_ptj4usE%9Auw0XH0_tG$n`ycBV{_Ct3!G&JIuZ5KO2xYP25VyV*=C8{m_+>x zRI=xrV}38##wYvFLqQR^fo;(G++=w>)XWy5KKG(tHuo?b{pOmJvk>a_+#J2D)yz-O?e_J7EH^J}##s0U0!jdTMh;?GzVYb-EX-y7B8awN*Gi};ax z+lA)qdK=b9{+wu+C!3wF-YlE$I{w`(MfCp`a6KBdUHJSKuXVh69(F{eJ|H z@WA6ved02+#?w*Tb}1@p51^9vFlrY)cKio*#M{eF4y9er{%=7;ej0Q(55jPqiQ50$ zQTz88YHiPr<1W~P`cSN;_x~*l2{h!{YAzU!an$GF0K9-oy4Y=I|F*(j)Q6+CJKTT`@pP-G}hB2J_>b>R*$+xJ9{f4E{f1i2h z#O!mMnfIeXOE4An1;K+lP}X8uJc`;jFHvvDLi^1ce~S7hv;aS1Uawh#JO@k!qp>pW z?NI|*h>FM_RFZ$^rl1jJKWLJxE>@@B8?|pYV+A~q3T?nwW*cTjZLe@tZj`{&7=wDi zd(;F%51GitpmM1pD!F^3?sspaP?5que1U(UzL4BIY<9zA)SA6Vb)4>qdHohd)mNcD z-;X*8PhkXR{MvjEh(-;h9qPatg__W8Br@87%B;8 zppNP_sN_6>n!wMfoXPgB`OQdeEJA$_Do6G?9zykd6l1jiFH=xx1CKh}1hpiEQ8&h; zLe&BHVo&UbxsRD|Om5V`j-VoP6}5Ywp}s$4J8t@|gxVzyQ4{KpIeEToBn9o;1*jV~ zqaOGj>cVTN0X;z-rC}$`gCbGij$<(&S>GNNkx?hj02ZL`KZI((fJ*Yms3m%bZmnIO zQ|4t+5|!<}P#rIG^rAv}6?NwSi{V)Gw0S^7R0I=HYuq38xf{#j8jQqi7=!OI3d@{f z|EHtSJy`?Uj0n|m!s2vu;L{t*ZK+R+w>Ok4& zv|mNN?fyXJLY6DW2-J+D9P6Ma+7`(rw`(K?W#tCc8NVO>7|{2q+_-qv?1tN@jvt}U z{#U47^By&@OxMhH#Zdi~L(RONV++)T+M^DtuD*8m-*yU`(LPiJ&Y&K27uE5fs0RgK zH#5wOYpK^lJ?J6ox>u;TXvP~RIg6np)YfqXDpwYxmU1tq(f&V4AsjED*64p&1B=`= zNz@CqJ7!=!?K^LobHMLM^K(NcR1URu9D~KFuR`U}S=77ahSUBLHQ-`DvHxpQs6|0v znI@yQNzL1q@Bb+L1?n6K|Jfu}EGieep^o5LsBO3lHS;@;Z?GiwEO*S2TmyA(3_vB_ zQY?de@38-)DLkY>YoGlWvjinkBdp_?;It1%CE4ewtWQOCcpK~BYv=Q7cTLhIqF!dh zunLYvy(_$^$ozTNZGOP`K!esi@Sa(lY^a3Eo%mA0TDd@o2feQ5*T!X)&A~5Zt<@@V*t5Kgv{c0Z66jkqr z`c60jJL3r~i-mqO6KI19eP7gDb}UmJv8-v={ai|>Xjm2;ZYM@_WN4$y} zQ1mOaUAtpL>M7{1OW|hks=Zo_G) zNJPIi+cX|4QXh}8xF2=>%eU-*om@fhOtu$CWovEJOp?$Km!gi&m8k0vp_1|r4r0we zqGp`*!7SY(RH%2M&Xq%`Bli+&BKI*0gFdqV^>(WC(Ol33wFC*M$c)Bb_$}&yDf*xJ zMl%4_;X3rkRL29Tq&$M*_&q8@f1vig&40C2V^KM_#7#j5${xqBP$55tjqoZebonjY zcVxD~lGLZ8_VZrU=TA`wOaa^W%`5?xe4nEtuoM;YU8o5kLmlPr>lA`1d_)~ML4M|j zaMTDZIJQ82-WN6C$*8QJg-XUHr~z$q+Ha#C^aPvX2ds}x{B7TXwGe4{yKYjb%m+mR zOb3ZLiMj{XL9ReEkkY8HRt>N(wnYu#IBI4WQEU4EHSpjdbAJI;E>%D+MHf^=hN17@ z|1pb#X0QoM;a=1l-bZ!x7iu8sf^Am>=0`m+4s|}XM!kfFqH^bR)P36>PvRizzo4#b zk;eADl=|U3?f)SZ)UjV$VZ3;74p)-p15pu3A7VbwgL+VD)b412`n)G zG#53&6{xJ==6wDW`u_btFDYp4(qu3*t%0?ux57AFjP>v)s)PI)&62f19nmvUyWuG6 zwR{;Bq4%h5nIV(8zZ@#UW3V)C&1AcMGrdWJ4u+?wH7uIhY?G#_2lPROZVl?jL#P2i zMeT<0EVl2b=BB75n~fU4RaAeEPyH7--g+~ zADM2VW)hOqcGbjqjK;-S1J7U)49;cyPQoaRq22=Zped-_*@lY5H>e!Dj2ie$Y=!RP zxfuY3@u=@)>rf}uWz>M4qrO4~<}nWlag0NCGz_)o+fm7P6_pDQF$n*0+CMn$A$d)E zKJ2RfUzkD~E}Vfnh*D4^UV#H}mt)?1CMjp3B5@uSN>_ff_5r8?)IqhkM)lVY^%m@k z%9U?WyX6^1>;0cL+=Q|kYTI>p9F7{uXBdvlQRl*8)PT=nFT93I&e#aE?b@QUd@!oN z)p!$kV^5q}z%1!s*jxL*Y(cYz%Q2q%A*_a(3)!xr*bMccV^|K0N7}w`NS(1N^_lo7 zoRkKT!L-T#T_lYQ!5*$@8;g+Hz)%D`GL)d!Tl~TvT!%Kpi+wu|DQ5Z$9tl zI2*N1_qi!3S$@Lin5Kg5`>xgw)$wf9_kjHvhgL;1gW9P3hN9MTt>gEoC3uZm%9u(f z)YDN1*TOUDTH&;Rj|%l0R3wVmGVg}AsL)PFE!|Gsg1_M~oLk#=ZN-2(Y-7HEx^_`ek{zsT zvOY^a+xPu`0@h{b-(x2}&(gs5{TopRV{_UA8rr@;kce+&9&`uwr87rka}X88FzPWF zfK5;*<0tqAyJ0cy|Bg+}OJbViLM+S!)}d}V+ti#`KcXJ+2DKgio0+7lggWaJ@gVmN zN1cFCEzE8xj~S^aV+c;cKDYpV@Be=(D9JLnG+!b+qE0Rk>K(AvsUO86)URMRe2<}+ z`4ba~NYwWH0yXfLr~wDGa;`(2sFA3*U!_*;f1PyAY0v{_qt514xE&8;683IwZoG*a z(C?@j`^B4N3qy5W4a2cH>H&jL=gV}|ecPPRkD-q22l4EG?e~vPLxoSx+pi@C(LMk* z!(phkoPp|SozwmmYCD}rt?_?OJ)n*GZdd`+(;knCSXZY$5;fqdZVDRV5>&^BP&2!N zipXOO$9Jgjdb!)0Y%h;#sCPrXlm=iW9P79b_2B2IiM&VUj9)u*Uq#gO+;J2NQD}$y zHap(=U?pk*`>;BmMlDrfd$Z4Lqn2s_M&m40$d8~-!mFqmen3SiQwMV-$Dt;28Ce3i z>lp=QWl%?Rl4V1E0jY_KL`U?)!KkbrhFX%*s4pg;JMD{5OR&soU*oiIMumO{j>coC z-4L6gfwBLZQcyCCM@3*MDr5&x5%?CB&6iO#dVnSH73RSLoy_$$P!p+-ida1AK?_k! zu>m!}-A=m~1GWE8Q^=3!Q7@V2sFNypXOrFYQK8@G)c;1UdG;=5O(Rj?irZm0eug@F zx1t8{2M)mx*ainB+OCOs2Hl!bqpl_i6Hxnm0xH|Lpw{XrDndUwpT9y4G+#H<-Wau1 zJ+Uy3LA}n`Ii5n@{}dImfbO>MmsJJ2v;Vu$Fo^~|=mF}+52!OeqKEmu-W(O7KTsWo z^fU)cQPh%^!@1ZB7vKZbnh)<~1~wbDlxrPRQO`Ts%Wb|0{6a$m8glkFA?u3MsSiO# z;+!Eii-TJtBUwaz}&?CTP!^P?l``iZFPW~0uBgQ#6`0~N_% zQ91At*~V^H?qqXe84Txxrl{;5h`MncYJjtx`dZWgcA?IJ6R3`Fq9*VcYOOa6?|0^iyfZ2<>@FZ%4H&Hq90`;IuBh8FjqOR|TdIuz@1tg%Wwc4M z(x@e>irS7XQ8OKc%7vMz2zyZxIfr`g^U-#5-;J5Zm>UYCW?mDOtsPKF)DJcC8K~^t ziN3W*9mO|M5A+|KTD-@neyORwt68aq`gXNae(hHwWy`<fKSc53|S8+K~daqX>?{o}KwULPMGv}amJ zJ7w@^V^ZILwj(g*`P^5j3G+KxDXUU)r+${QAuu(0S)i5bU7j;I_1gL&fp*fs)SlZC z)1{=@A7*t=U9$f(JGI8a_I@d64(Ccu|4psn)V`?~V>ix%X&8>b}QO!Kv^5_p_Dy z>5G`a)Qs<21T30p)mU`is^-mQSxwV;hNrc%c$TNNa(S<$wLE^_2^p;AmbXDB>#d)s zdsb^#=KejpC5Oii={~H_z@+dNNkfvo^|D#N+Mc>$R)lwUn3cuPdpEb0+44r@wJKQN z{`st^0MEvP)@1MSLe>d8Aa2m0{(U@Gi&$kmgDY8~o~H?RT2IxY)+X<>qSlR2?>Ev%>h-gog)yA6ZE#1~y<>$@T(VAg-b|qLty@fkj#r(YSiB@gf zyT6;&H`v>wzqQ)(CJeMzSl(@etml579mA}5-jEU20LycAr1dn@;_%vu1H1L;AKt3R z(4l>j272N~TWvijM_c)_4;nSJ$B;gW{lf<)C5I;tPwdk_v1|Vxo?>IH;ht~DSn-|~ z2kflgE@Q1FmZ$4@1{CDBDqEgb6Rg~x85681PyHiymUJx=hbH$3Z=W=<$52n{iB==e ztclhlPw`1sHqWq0Rxj_XNmc{P({hS++CQpFxys%=Q>`8TUTdb6V0l;0vU*#d?sKg2 zo~HAx;+_-ptY)4D^Q~~tq50M_Pkf4%>dCs$+UB{q(E7@AWRZ2sFMdFxCu$7i%ImTA z+TO=Yt*n0D5-Y7te%{t=tZ`Q6I*CJjcTGwh(*6II&6{(bbu`HP+g59m?U}U0TI?;l z(<&b1U9i{kx4c>RTeB?B>H}78@1z4(hhWc_-&#T5Do3sAmZv~(o^$({)hgIq?woZi zFyQ#+HtsGcJ^3ry7u-ZJ1CAM?v~iyGdaPI@?1)= zuX*=$vO}|Z6O-*5S-qoX*yRGeM?SaXEKm46ySnGIdG>N|jrn$YfA4`s_F}TfV^8<~ z;jw2ioaJ_J+Z(vb{yo?mx7prndHZd(4_e-{+wF?}-dwwF%koa$V}C~bUOTOo@&EES zafmO2J^$^q`*_yvw@Y~*?6-4!a(V3$o_$_sPqH4cJQKgNH+yRxvb$TJ!-x6&@56RA zOICVM>^F8h@0Z`$36`hGQG4M30|OF@ Apa1{> diff --git a/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.po b/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.po index da418d3c..a4fdfee4 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-14 10:49-0300\n" +"POT-Creation-Date: 2026-08-14 11:34-0300\n" "PO-Revision-Date: 2026-08-08 21:41-0300\n" "Last-Translator: FULL NAME \n" "Language-Team: pt_BR \n" @@ -259,6 +259,7 @@ msgstr "Vídeo Único" #: src/zebtrack/ui/components/validation_manager.py:491 #: src/zebtrack/ui/components/validation_manager.py:1634 #: src/zebtrack/ui/gui.py:144 src/zebtrack/ui/gui.py:808 +#: src/zebtrack/ui/wizard/live_config_step.py:862 msgid "Error" msgstr "Erro" @@ -948,7 +949,7 @@ msgstr "Análise concluída." #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1095 #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1795 -#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2314 +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2316 msgid "Countdown to start the live analysis." msgstr "Contagem regressiva para iniciar a análise ao vivo." @@ -956,14 +957,14 @@ msgstr "Contagem regressiva para iniciar a análise ao vivo." # | msgid "Single Video Analysis Options" #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1097 #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1797 -#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2316 +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2318 msgid "Starting the live analysis." msgstr "Iniciando análise ao vivo." # | msgid "Analysis in Progress" #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1190 #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1866 -#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2385 +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2387 msgid "Live analysis in progress." msgstr "Análise ao vivo em andamento." @@ -1015,14 +1016,20 @@ 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:551 +#, 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/coordinators/live_camera_session_coordinator.py:2170 #: src/zebtrack/ui/components/project_initializer.py:347 msgid "Camera not found" msgstr "Câmera não encontrada" -#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2170 +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2172 #, python-brace-format msgid "" "The camera saved in the project ('{name}') was not detected.\n" @@ -1149,6 +1156,7 @@ msgstr "" # | msgid "Error Saving to Project" #: src/zebtrack/coordinators/model_diagnostics_coordinator.py:742 +#: src/zebtrack/ui/wizard/confirmation_step.py:802 msgid "Error Saving" msgstr "Erro ao Salvar" @@ -2080,6 +2088,7 @@ msgstr "📊 Status dos vídeos:" # | msgid "Failure: {error}" # | msgid "❌ Failures: {count}" #: src/zebtrack/core/project/project_workflow_service.py:1331 +#: src/zebtrack/ui/wizard/confirmation_step.py:558 #, python-brace-format msgid " • Total videos: {count}" msgstr " • Total de vídeos: {count}" @@ -4069,6 +4078,7 @@ msgid "No project is currently loaded." msgstr "Nenhum projeto está carregado no momento." #: src/zebtrack/ui/components/arduino_dashboard.py:310 +#: src/zebtrack/ui/wizard/live_config_step.py:863 msgid "" "The pyserial library is not installed.\n" "\n" @@ -4615,6 +4625,7 @@ msgstr "💡 Abre o diálogo de Calibração e Detecção." #: src/zebtrack/ui/components/project_model_configuration_panel.py:189 #: src/zebtrack/ui/components/project_model_configuration_panel.py:378 #: src/zebtrack/ui/components/weight_hardware_manager.py:145 +#: src/zebtrack/ui/wizard/confirmation_step.py:529 msgid "Enabled" msgstr "Ativado" @@ -4623,6 +4634,7 @@ msgstr "Ativado" #: src/zebtrack/ui/components/project_model_configuration_panel.py:191 #: src/zebtrack/ui/components/project_model_configuration_panel.py:378 #: src/zebtrack/ui/components/weight_hardware_manager.py:145 +#: src/zebtrack/ui/wizard/confirmation_step.py:529 msgid "Disabled" msgstr "Desativado" @@ -8861,8 +8873,516 @@ msgstr "Frame carregado: {name}" msgid "Buffer radius must be >= 0" msgstr "Raio de buffer deve ser >= 0" +#: src/zebtrack/ui/wizard/confirmation_step.py:88 +msgid "Project Confirmation and Creation" +msgstr "Confirmação e Criação do Projeto" + +#: src/zebtrack/ui/wizard/confirmation_step.py:96 +msgid "Review the settings and create your project." +msgstr "Revise as configurações e crie seu projeto." + +#: src/zebtrack/ui/wizard/confirmation_step.py:123 +msgid "Project Name:" +msgstr "Nome do Projeto:" + +#: src/zebtrack/ui/wizard/confirmation_step.py:140 +msgid "Location:" +msgstr "Localização:" + +#: src/zebtrack/ui/wizard/confirmation_step.py:152 +msgid "Browse..." +msgstr "Procurar..." + +#: src/zebtrack/ui/wizard/confirmation_step.py:158 +msgid "Project Summary" +msgstr "Resumo do Projeto" + +#: src/zebtrack/ui/wizard/confirmation_step.py:189 +msgid "💾 Save as Template" +msgstr "💾 Salvar como Template" + +#: src/zebtrack/ui/wizard/confirmation_step.py:198 +msgid "" +"💡 Tip: Review every setting before creating the project. You can save it as " +"a template to reuse later." +msgstr "" +"💡 Dica: Verifique todas as configurações antes de criar o projeto. Você pode" +" salvar como template para reutilizar." + +#: src/zebtrack/ui/wizard/confirmation_step.py:243 +#, python-brace-format +msgid "Experiment_{group}" +msgstr "Experimento_{group}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:245 +msgid "Experimental_Project" +msgstr "Projeto_Experimental" + +#: src/zebtrack/ui/wizard/confirmation_step.py:247 +msgid "Exploratory_Project" +msgstr "Projeto_Exploratorio" + +#: src/zebtrack/ui/wizard/confirmation_step.py:260 +msgid "Select the Project Folder" +msgstr "Selecione a Pasta do Projeto" + +#: src/zebtrack/ui/wizard/confirmation_step.py:318 +msgid "📝 Template Loaded:" +msgstr "📝 Template Carregado:" + +#: src/zebtrack/ui/wizard/confirmation_step.py:323 +#, python-brace-format +msgid " • Created at: {value}" +msgstr " • Criado em: {value}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:326 +#, python-brace-format +msgid " • Template version: {value}" +msgstr " • Versão do template: {value}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:331 +msgid "📋 Project Type:" +msgstr "📋 Tipo de Projeto:" + +#: src/zebtrack/ui/wizard/confirmation_step.py:333 +msgid "Experimental (pre-recorded)" +msgstr "Experimental (pré-gravado)" + +#: src/zebtrack/ui/wizard/confirmation_step.py:334 +msgid "Exploratory (pre-recorded)" +msgstr "Exploratório (pré-gravado)" + +#: src/zebtrack/ui/wizard/confirmation_step.py:335 +msgid "Live (real time)" +msgstr "Ao Vivo (tempo real)" + +#: src/zebtrack/ui/wizard/confirmation_step.py:348 +msgid "🔬 Experimental Design:" +msgstr "🔬 Design Experimental:" + +#: src/zebtrack/ui/wizard/confirmation_step.py:353 +#, python-brace-format +msgid " • {groups} groups x {days} days x {subjects} animals/group" +msgstr " • {groups} grupos x {days} dias x {subjects} animais/grupo" + +#: src/zebtrack/ui/wizard/confirmation_step.py:360 +#, python-brace-format +msgid " • Total: {sessions} recordings ({animals} animals)" +msgstr " • Total: {sessions} gravações ({animals} animais)" + +#: src/zebtrack/ui/wizard/confirmation_step.py:366 +#, python-brace-format +msgid " • Groups: {groups}" +msgstr " • Grupos: {groups}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:370 +msgid "📹 Hardware:" +msgstr "📹 Hardware:" + +#: src/zebtrack/ui/wizard/confirmation_step.py:375 +#, python-brace-format +msgid " • Camera: {name} (index {index})" +msgstr " • Câmera: {name} (índice {index})" + +#: src/zebtrack/ui/wizard/confirmation_step.py:380 +#, python-brace-format +msgid " • Camera: index {index}" +msgstr " • Câmera: Índice {index}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:384 +#, python-brace-format +msgid " • Arduino: {port}" +msgstr " • Arduino: {port}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:386 +msgid " • Mode: External Trigger ✓" +msgstr " • Modo: Gatilho Externo (External Trigger) ✓" + +#: src/zebtrack/ui/wizard/confirmation_step.py:391 +msgid "⏱️ Recording Settings:" +msgstr "⏱️ Configurações de Gravação:" + +#: src/zebtrack/ui/wizard/confirmation_step.py:397 +#, python-brace-format +msgid " • Timed recording: {minutes}min {seconds}s" +msgstr " • Gravação temporizada: {minutes}min {seconds}s" + +#: src/zebtrack/ui/wizard/confirmation_step.py:403 +#, python-brace-format +msgid " • Countdown: {seconds}s" +msgstr " • Contagem regressiva: {seconds}s" + +#: src/zebtrack/ui/wizard/confirmation_step.py:410 +msgid "⚙️ Processing Intervals:" +msgstr "⚙️ Intervalos de Processamento:" + +#: src/zebtrack/ui/wizard/confirmation_step.py:413 +#, python-brace-format +msgid " • Analysis: every {count} frames" +msgstr " • Análise: a cada {count} frames" + +#: src/zebtrack/ui/wizard/confirmation_step.py:417 +#, python-brace-format +msgid " • Display: every {count} frames" +msgstr " • Exibição: a cada {count} frames" + +#: src/zebtrack/ui/wizard/confirmation_step.py:425 +msgid "🎯 Detection Configuration:" +msgstr "🎯 Configuração de Detecção:" + +#: src/zebtrack/ui/wizard/confirmation_step.py:431 +#: src/zebtrack/ui/wizard/confirmation_step.py:524 +#, python-brace-format +msgid " • Aquarium weight: {weight}" +msgstr " • Peso aquário: {weight}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:434 +#: src/zebtrack/ui/wizard/confirmation_step.py:526 +#, python-brace-format +msgid " • Animal weight: {weight}" +msgstr " • Peso animais: {weight}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:452 +msgid "🔍 Detected Design:" +msgstr "🔍 Design Detectado / Design:" + +#: src/zebtrack/ui/wizard/confirmation_step.py:461 +#, python-brace-format +msgid " • Groups: {count} ({preview}{suffix})" +msgstr " • Grupos: {count} ({preview}{suffix})" + +#: src/zebtrack/ui/wizard/confirmation_step.py:467 +#, python-brace-format +msgid " • Days: {count}" +msgstr " • Dias: {count}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:469 +#, python-brace-format +msgid " • Confidence: {value}" +msgstr " • Confiança: {value}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:477 +msgid "Groups" +msgstr "Grupos" + +#: src/zebtrack/ui/wizard/confirmation_step.py:478 +msgid "Days" +msgstr "Dias" + +#: src/zebtrack/ui/wizard/confirmation_step.py:479 +msgid "Subjects" +msgstr "Sujeitos" + +#: src/zebtrack/ui/wizard/confirmation_step.py:483 +msgid "🧩 Custom Regex:" +msgstr "🧩 Regex Personalizada:" + +#: src/zebtrack/ui/wizard/confirmation_step.py:503 +msgid "Segmentation (seg)" +msgstr "Segmentação (seg)" + +#: src/zebtrack/ui/wizard/confirmation_step.py:504 +msgid "Detection (det)" +msgstr "Detecção (det)" + +#: src/zebtrack/ui/wizard/confirmation_step.py:508 +msgid "🎯 Detection Settings:" +msgstr "🎯 Configurações de Detecção:" + +#: src/zebtrack/ui/wizard/confirmation_step.py:515 +#, python-brace-format +msgid " • Aquarium method: {method}" +msgstr " • Método aquário: {method}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:518 +#, python-brace-format +msgid " • Animal method: {method}" +msgstr " • Método animais: {method}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:530 +#, python-brace-format +msgid " • OpenVINO: {status}" +msgstr " • OpenVINO: {status}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:563 +msgid "🌳 Folder Structure (preview):" +msgstr "🌳 Estrutura de Pastas (prévia):" + +#: src/zebtrack/ui/wizard/confirmation_step.py:570 +msgid " • (+ 1 additional selection)" +msgstr " • (+ 1 seleção adicional)" + +#: src/zebtrack/ui/wizard/confirmation_step.py:572 +#, python-brace-format +msgid " • (+ {count} additional selections)" +msgstr " • (+ {count} seleções adicionais)" + +#: src/zebtrack/ui/wizard/confirmation_step.py:577 +msgid "📏 Physical Calibration:" +msgstr "📏 Calibração Física:" + +#: src/zebtrack/ui/wizard/confirmation_step.py:583 +#, python-brace-format +msgid " • Aquariums: {count}" +msgstr " • Aquários: {count}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:585 +#, python-brace-format +msgid " • Animals per aquarium: {count}" +msgstr " • Animais por aquário: {count}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:588 +#, python-brace-format +msgid " • Dimensions: {width} x {height} cm" +msgstr " • Dimensões: {width} x {height} cm" + +#: src/zebtrack/ui/wizard/confirmation_step.py:593 +msgid "⚙️ Processing Plan:" +msgstr "⚙️ Plano de Processamento:" + +#: src/zebtrack/ui/wizard/confirmation_step.py:605 +msgid "Skip (complete data)" +msgstr "Skip (dados completos)" + +#: src/zebtrack/ui/wizard/confirmation_step.py:606 +msgid "Import Zones + track" +msgstr "Import Zones + rastrear" + +#: src/zebtrack/ui/wizard/confirmation_step.py:607 +msgid "Partial (arena only)" +msgstr "Partial (arena apenas)" + +#: src/zebtrack/ui/wizard/confirmation_step.py:608 +msgid "Full (process from scratch)" +msgstr "Full (processar do zero)" + +#: src/zebtrack/ui/wizard/confirmation_step.py:614 +#, python-brace-format +msgid " • 1 video: {name}" +msgstr " • 1 vídeo: {name}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:616 +#, python-brace-format +msgid " • {count} videos: {name}" +msgstr " • {count} vídeos: {name}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:628 +#, python-brace-format +msgid "⏱️ Estimated time: ~{minutes} minutes" +msgstr "⏱️ Tempo Estimado: ~{minutes} minutos" + +#: src/zebtrack/ui/wizard/confirmation_step.py:631 +msgid " (1 video to process)" +msgstr " (1 vídeo para processar)" + +#: src/zebtrack/ui/wizard/confirmation_step.py:633 +#, python-brace-format +msgid " ({count} videos to process)" +msgstr " ({count} vídeos para processar)" + +#: src/zebtrack/ui/wizard/confirmation_step.py:645 +msgid "📦 Existing Parquets:" +msgstr "📦 Parquets Existentes:" + +#: src/zebtrack/ui/wizard/confirmation_step.py:647 +#, python-brace-format +msgid " • Scope: {scope}" +msgstr " • Escopo: {scope}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:652 +#, python-brace-format +msgid " • Arena: {count}" +msgstr " • Arena: {count}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:653 +#, python-brace-format +msgid " • ROIs: {count}" +msgstr " • ROIs: {count}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:654 +#, python-brace-format +msgid " • Trajectory: {count}" +msgstr " • Trajetória: {count}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:655 +#, python-brace-format +msgid " • Complete: {count}" +msgstr " • Completos: {count}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:668 +msgid "📥 Import Configuration:" +msgstr "📥 Configuração de Importação:" + +#: src/zebtrack/ui/wizard/confirmation_step.py:672 +msgid " ✅ Arena: 1 video" +msgstr " ✅ Arena: 1 vídeo" + +#: src/zebtrack/ui/wizard/confirmation_step.py:674 +#, python-brace-format +msgid " ✅ Arena: {count} videos" +msgstr " ✅ Arena: {count} vídeos" + +#: src/zebtrack/ui/wizard/confirmation_step.py:679 +msgid " ✅ ROIs: 1 video" +msgstr " ✅ ROIs: 1 vídeo" + +#: src/zebtrack/ui/wizard/confirmation_step.py:681 +#, python-brace-format +msgid " ✅ ROIs: {count} videos" +msgstr " ✅ ROIs: {count} vídeos" + +#: src/zebtrack/ui/wizard/confirmation_step.py:686 +msgid " ✅ Trajectory: 1 video" +msgstr " ✅ Trajetória: 1 vídeo" + +#: src/zebtrack/ui/wizard/confirmation_step.py:688 +#, python-brace-format +msgid " ✅ Trajectory: {count} videos" +msgstr " ✅ Trajetória: {count} vídeos" + +#: src/zebtrack/ui/wizard/confirmation_step.py:703 +msgid "Replace existing ROIs" +msgstr "Substituir ROIs existentes" + +#: src/zebtrack/ui/wizard/confirmation_step.py:704 +msgid "Merge (keep both)" +msgstr "Mesclar (manter ambos)" + +#: src/zebtrack/ui/wizard/confirmation_step.py:705 +msgid "Manual conflict resolution" +msgstr "Resolução manual de conflitos" + +#: src/zebtrack/ui/wizard/confirmation_step.py:708 +msgid "🔀 ROI Strategy:" +msgstr "🔀 Estratégia de ROIs:" + +#: src/zebtrack/ui/wizard/confirmation_step.py:713 +msgid "(selection)" +msgstr "(seleção)" + +#: src/zebtrack/ui/wizard/confirmation_step.py:721 +msgid "1 folder" +msgstr "1 pasta" + +#: src/zebtrack/ui/wizard/confirmation_step.py:721 +#, python-brace-format +msgid "{count} folders" +msgstr "{count} pastas" + +#: src/zebtrack/ui/wizard/confirmation_step.py:725 +msgid "1 file" +msgstr "1 arquivo" + +#: src/zebtrack/ui/wizard/confirmation_step.py:725 +#, python-brace-format +msgid "{count} files" +msgstr "{count} arquivos" + +#: src/zebtrack/ui/wizard/confirmation_step.py:728 +msgid "empty" +msgstr "vazio" + +#: src/zebtrack/ui/wizard/confirmation_step.py:749 +msgid " … Preview truncated (full details in step 2)" +msgstr " … Prévia limitada (detalhes completos na etapa 2)" + +#: src/zebtrack/ui/wizard/confirmation_step.py:757 +msgid "Save Template" +msgstr "Salvar Template" + +#: src/zebtrack/ui/wizard/confirmation_step.py:758 +msgid "Enter a name for the template:" +msgstr "Digite um nome para o template:" + +#: src/zebtrack/ui/wizard/confirmation_step.py:770 +msgid "Save Wizard Template" +msgstr "Salvar Template do Wizard" + +#: src/zebtrack/ui/wizard/confirmation_step.py:772 +msgid "Wizard Templates" +msgstr "Templates do Wizard" + +#: src/zebtrack/ui/wizard/confirmation_step.py:789 +#, python-brace-format +msgid "" +"Template '{name}' saved successfully!\n" +"\n" +"File: {path}\n" +"\n" +"You will be able to load this template later to create similar projects quickly." +msgstr "" +"Template '{name}' salvo com sucesso!\n" +"\n" +"Arquivo: {path}\n" +"\n" +"Você poderá carregar este template no futuro para criar projetos similares rapidamente." + +#: src/zebtrack/ui/wizard/confirmation_step.py:795 +msgid "Template Saved" +msgstr "Template Salvo" + +#: src/zebtrack/ui/wizard/confirmation_step.py:804 +#, python-brace-format +msgid "" +"Could not save the template '{name}'.\n" +"\n" +"Check the logs for more details." +msgstr "" +"Não foi possível salvar o template '{name}'.\n" +"\n" +"Verifique os logs para mais detalhes." + +#: src/zebtrack/ui/wizard/confirmation_step.py:821 +msgid "Please enter a name for the project." +msgstr "Por favor, informe um nome para o projeto." + +#: src/zebtrack/ui/wizard/confirmation_step.py:826 +msgid "" +"The project name contains invalid characters. Use only letters, digits, " +"spaces, '_' and '-'." +msgstr "" +"Nome do projeto contém caracteres inválidos. Use apenas letras, números, " +"espaços, '_' e '-'." + +#: src/zebtrack/ui/wizard/confirmation_step.py:835 +msgid "Please select a location for the project." +msgstr "Por favor, selecione uma localização para o projeto." + +#: src/zebtrack/ui/wizard/confirmation_step.py:838 +#, python-brace-format +msgid "Location does not exist: {location}" +msgstr "Localização não existe: {location}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:843 +#, python-brace-format +msgid "No write permission at the location: {location}" +msgstr "Sem permissão de escrita na localização: {location}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:851 +#: src/zebtrack/ui/wizard/confirmation_step.py:863 +msgid "The project name is too long for the file system." +msgstr "Nome do projeto é muito longo para o sistema de arquivos." + +#: src/zebtrack/ui/wizard/confirmation_step.py:858 +#, python-brace-format +msgid "A file with that name already exists at: {location}" +msgstr "Já existe um arquivo com esse nome em: {location}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:874 +#, python-brace-format +msgid "A project with that name already exists at: {location}" +msgstr "Já existe um projeto com esse nome em: {location}" + +#: src/zebtrack/ui/wizard/confirmation_step.py:886 +msgid "No video selected. Go back and select videos." +msgstr "Nenhum vídeo selecionado. Volte e selecione vídeos." + +#: src/zebtrack/ui/wizard/confirmation_step.py:891 +msgid "Configure the camera in the previous step before creating the project." +msgstr "Configure a câmera na etapa anterior antes de criar o projeto." + +# | msgid "Invalid Configuration" #: src/zebtrack/ui/wizard/experimental_design_step.py:154 -#| msgid "Invalid Configuration" msgid "Experimental Design Configuration" msgstr "Configuração do Design Experimental" @@ -8872,8 +9392,8 @@ msgstr "Configure a estrutura do seu experimento ao vivo" # | msgid "Global Model Configuration..." # | msgid "Model Configuration" +# | msgid "Zone Configuration" #: src/zebtrack/ui/wizard/experimental_design_step.py:174 -#| msgid "Zone Configuration" msgid "Basic Configuration" msgstr "Configuração Básica" @@ -8882,8 +9402,8 @@ msgid "Experiment Duration (days):" msgstr "Duração do Experimento (dias):" # | msgid "Day" +# | msgid "day" #: src/zebtrack/ui/wizard/experimental_design_step.py:199 -#| msgid "day" msgid "days" msgstr "dias" @@ -8914,8 +9434,8 @@ msgstr "" "Isso afeta a organização dos arquivos de saída." # | msgid "Analysis in Progress" +# | msgid "Analysis Error" #: src/zebtrack/ui/wizard/experimental_design_step.py:218 -#| msgid "Analysis Error" msgid "Animals per Group:" msgstr "Animais por Grupo:" @@ -8923,22 +9443,22 @@ msgstr "Animais por Grupo:" msgid "animals/group" msgstr "animais/grupo" +# | msgid "No Group" #: src/zebtrack/ui/wizard/experimental_design_step.py:252 -#| msgid "No Group" msgid "Number of Groups:" msgstr "Número de Grupos:" # | msgid "No Group" # | msgid "Group" +# | msgid "group" #: src/zebtrack/ui/wizard/experimental_design_step.py:271 -#| msgid "group" msgid "groups" msgstr "grupos" # | msgid "Weight Type" # | msgid "Weight Name" +# | msgid "ROI Name" #: src/zebtrack/ui/wizard/experimental_design_step.py:308 -#| msgid "ROI Name" msgid "Group Names" msgstr "Nomes dos Grupos" @@ -8973,24 +9493,440 @@ msgstr "" msgid "📊 Total: {sessions} recordings ({animals} animals x {days} days)" msgstr "📊 Total: {sessions} gravações ({animals} animais x {days} dias)" +#: src/zebtrack/ui/wizard/live_config_step.py:159 +msgid "Live Recording Configuration" +msgstr "Configuração de Gravação ao Vivo" + +#: src/zebtrack/ui/wizard/live_config_step.py:164 +msgid "Set up the camera and the recording options for the live project." +msgstr "Configure a câmera e as opções de gravação para o projeto ao vivo." + +#: src/zebtrack/ui/wizard/live_config_step.py:181 +msgid "Camera Configuration" +msgstr "Configuração de Câmera" + +#: src/zebtrack/ui/wizard/live_config_step.py:190 +msgid "Select Camera:" +msgstr "Selecionar Câmera:" + +#: src/zebtrack/ui/wizard/live_config_step.py:204 +msgid "Select the camera for live recording." +msgstr "Selecione a câmera para gravação ao vivo." + +#: src/zebtrack/ui/wizard/live_config_step.py:209 +msgid "🔍 Detect Cameras" +msgstr "🔍 Detectar Câmeras" + +#: src/zebtrack/ui/wizard/live_config_step.py:219 +msgid "Arduino Configuration (Optional)" +msgstr "Configuração de Arduino (Opcional)" + +#: src/zebtrack/ui/wizard/live_config_step.py:227 +msgid "Use Arduino for synchronization" +msgstr "Usar Arduino para sincronização" + +#: src/zebtrack/ui/wizard/live_config_step.py:234 +msgid "Enable Arduino to synchronize external events with the recording." +msgstr "Habilitar Arduino para sincronizar eventos externos com a gravação." + +#: src/zebtrack/ui/wizard/live_config_step.py:243 +msgid "Arduino Port:" +msgstr "Porta do Arduino:" + +#: src/zebtrack/ui/wizard/live_config_step.py:259 +msgid "🔍 Detect" +msgstr "🔍 Detectar" + +#: src/zebtrack/ui/wizard/live_config_step.py:268 +msgid "🔌 Test" +msgstr "🔌 Testar" + +#: src/zebtrack/ui/wizard/live_config_step.py:281 +msgid "External Trigger Mode" +msgstr "Modo de Gatilho Externo (External Trigger)" + +#: src/zebtrack/ui/wizard/live_config_step.py:289 +#, python-brace-format +msgid "" +"External Trigger Mode\n" +"\n" +"What starts the recording becomes the Arduino, not the operator.\n" +"\n" +"When 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" +"\n" +"Useful to synchronize the start with a stimulus, a gate or another instrument.\n" +"\n" +"Requires a connected Arduino and the port selected above — without them the session is refused, not started blind." +msgstr "" +"Modo de Gatilho Externo\n" +"\n" +"Quem dá a partida na gravação passa a ser o Arduino, não o operador.\n" +"\n" +"Ao 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" +"\n" +"Requer Arduino conectado e a porta selecionada acima — sem isso a sessão é recusada, não iniciada às cegas." + +#: src/zebtrack/ui/wizard/live_config_step.py:299 +msgid "Waiting for external signal" +msgstr "Aguardando sinal externo" + +#: src/zebtrack/ui/wizard/live_config_step.py:305 +msgid "Recording Settings" +msgstr "Configurações de Gravação" + +#: src/zebtrack/ui/wizard/live_config_step.py:314 +msgid "Use timed recording" +msgstr "Usar gravação temporizada" + +#: src/zebtrack/ui/wizard/live_config_step.py:322 +msgid "" +"Enable recording with a fixed duration (stops automatically after the " +"specified time)." +msgstr "" +"Habilitar gravação com duração fixa (desliga automaticamente após o tempo " +"especificado)." + +#: src/zebtrack/ui/wizard/live_config_step.py:332 +msgid "Recording duration (seconds):" +msgstr "Duração da gravação (segundos):" + +#: src/zebtrack/ui/wizard/live_config_step.py:344 +msgid "Total recording duration in seconds (e.g. 300 = 5 minutes)." +msgstr "Duração total da gravação em segundos (ex: 300 = 5 minutos)." + +#: src/zebtrack/ui/wizard/live_config_step.py:350 +msgid "Use a countdown before starting" +msgstr "Usar contagem regressiva antes de iniciar" + +#: src/zebtrack/ui/wizard/live_config_step.py:357 +msgid "Show a countdown before starting the recording." +msgstr "Mostrar contagem regressiva antes de iniciar a gravação." + +#: src/zebtrack/ui/wizard/live_config_step.py:365 +msgid "Countdown duration (seconds):" +msgstr "Duração da contagem (segundos):" + +#: src/zebtrack/ui/wizard/live_config_step.py:379 +msgid "Countdown duration in seconds." +msgstr "Duração da contagem regressiva em segundos." + +#: src/zebtrack/ui/wizard/live_config_step.py:388 +msgid "⚙️ Advanced Processing Settings" +msgstr "⚙️ Configurações Avançadas de Processamento" + +#: src/zebtrack/ui/wizard/live_config_step.py:397 +msgid "Preserve the real aquarium shape (segmentation)" +msgstr "Preservar formato real do aquário (segmentação)" + +#: src/zebtrack/ui/wizard/live_config_step.py:404 +msgid "" +"Preserve the real aquarium shape\n" +"\n" +"When enabled, keeps the real polygon (N vertices) detected by the YOLO segmentation mask instead of reducing the aquarium to a 4-corner rectangle.\n" +"\n" +"Recommended for circular, hexagonal or irregularly shaped aquariums. Requires a segmentation (seg) model — for detection (det) models this option is ignored." +msgstr "" +"Preservar formato real do aquário\n" +"\n" +"Quando 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" +"\n" +"Recomendado 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." + +#: src/zebtrack/ui/wizard/live_config_step.py:417 +msgid "About Live Projects" +msgstr "Sobre Projetos ao Vivo" + +#: src/zebtrack/ui/wizard/live_config_step.py:426 +msgid "" +"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." +msgstr "" +"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." + +#: src/zebtrack/ui/wizard/live_config_step.py:538 +#, python-brace-format +msgid "" +"Hardware detected:\n" +"\n" +"Capability: {capability}\n" +"CPU: {cores} cores\n" +"RAM: {ram} GB\n" +"GPU: {gpu}\n" +"\n" +msgstr "" +"Hardware Detectado:\n" +"\n" +"Capacidade: {capability}\n" +"CPU: {cores} cores\n" +"RAM: {ram} GB\n" +"GPU: {gpu}\n" +"\n" + +#: src/zebtrack/ui/wizard/live_config_step.py:547 +msgid "Yes" +msgstr "Sim" + +#: src/zebtrack/ui/wizard/live_config_step.py:547 +msgid "No" +msgstr "Não" + +#: src/zebtrack/ui/wizard/live_config_step.py:552 +#, python-brace-format +msgid "" +"⚠️ Your system does NOT support real-time processing.\n" +"Recommendation: use '{mode}' mode (offline)." +msgstr "" +"⚠️ Seu sistema NÃO suporta processamento em tempo real.\n" +"Recomendação: Use modo '{mode}' (offline)." + +#: src/zebtrack/ui/wizard/live_config_step.py:554 +msgid "Recording Only" +msgstr "Apenas Gravação" + +#: src/zebtrack/ui/wizard/live_config_step.py:557 +#, python-brace-format +msgid "" +"Supported aquariums: {count}\n" +"Multi-aquarium in real time may not be possible." +msgstr "" +"Aquários suportados: {count}\n" +"Multi-aquário em tempo real pode não ser possível." + +#: src/zebtrack/ui/wizard/live_config_step.py:561 +msgid "Hardware Detection" +msgstr "Detecção de Hardware" + +#: src/zebtrack/ui/wizard/live_config_step.py:695 +#: src/zebtrack/ui/wizard/live_config_step.py:742 +msgid "Detecting..." +msgstr "Detectando..." + +#: src/zebtrack/ui/wizard/live_config_step.py:709 +#, python-brace-format +msgid "Camera {index}" +msgstr "Câmera {index}" + +#: src/zebtrack/ui/wizard/live_config_step.py:720 +msgid "✓ 1 camera detected" +msgstr "✓ 1 câmera detectada" + +#: src/zebtrack/ui/wizard/live_config_step.py:722 +#, python-brace-format +msgid "✓ {count} cameras detected" +msgstr "✓ {count} câmeras detectadas" + +#: src/zebtrack/ui/wizard/live_config_step.py:735 +msgid "✗ No camera detected" +msgstr "✗ Nenhuma câmera detectada" + +#: src/zebtrack/ui/wizard/live_config_step.py:762 +msgid "✓ 1 port detected" +msgstr "✓ 1 porta detectada" + +#: src/zebtrack/ui/wizard/live_config_step.py:764 +#, python-brace-format +msgid "✓ {count} ports detected" +msgstr "✓ {count} portas detectadas" + +#: src/zebtrack/ui/wizard/live_config_step.py:780 +#, python-brace-format +msgid "✓ 1 port — using {device}" +msgstr "✓ 1 porta — usando {device}" + +#: src/zebtrack/ui/wizard/live_config_step.py:782 +#, python-brace-format +msgid "✓ {count} ports — using {device}" +msgstr "✓ {count} portas — usando {device}" + +#: src/zebtrack/ui/wizard/live_config_step.py:799 +msgid "✗ No port detected" +msgstr "✗ Nenhuma porta detectada" + +#: src/zebtrack/ui/wizard/live_config_step.py:804 +#: src/zebtrack/ui/wizard/live_config_step.py:860 +msgid "✗ pyserial not installed" +msgstr "✗ pyserial não instalado" + +#: src/zebtrack/ui/wizard/live_config_step.py:814 +msgid "No Port Selected" +msgstr "Nenhuma Porta Selecionada" + +#: src/zebtrack/ui/wizard/live_config_step.py:815 +msgid "Please detect and select an Arduino port first." +msgstr "Por favor, detecte e selecione uma porta Arduino primeiro." + +#: src/zebtrack/ui/wizard/live_config_step.py:825 +msgid "Testing..." +msgstr "Testando..." + +#: src/zebtrack/ui/wizard/live_config_step.py:833 +msgid "✓ Connection OK" +msgstr "✓ Conexão OK" + +#: src/zebtrack/ui/wizard/live_config_step.py:835 +msgid "Connection Test" +msgstr "Teste de Conexão" + +#: src/zebtrack/ui/wizard/live_config_step.py:837 +#, python-brace-format +msgid "" +"Connection to {port} established successfully!\n" +"\n" +"The port is reachable and ready to use." +msgstr "" +"Conexão com {port} estabelecida com sucesso!\n" +"\n" +"A porta está acessível e pronta para uso." + +#: src/zebtrack/ui/wizard/live_config_step.py:845 +msgid "✗ Connection failed" +msgstr "✗ Falha na conexão" + +#: src/zebtrack/ui/wizard/live_config_step.py:847 +msgid "Connection Error" +msgstr "Erro de Conexão" + +#: src/zebtrack/ui/wizard/live_config_step.py:849 +#, python-brace-format +msgid "" +"Could not connect to port {port}.\n" +"\n" +"Error: {error}\n" +"\n" +"Check that:\n" +"• The Arduino is connected\n" +"• The port is not in use by another program\n" +"• You have permission to access the port" +msgstr "" +"Não foi possível conectar à porta {port}.\n" +"\n" +"Erro: {error}\n" +"\n" +"Verifique 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" + +#: src/zebtrack/ui/wizard/live_config_step.py:868 +msgid "✗ Unexpected error" +msgstr "✗ Erro inesperado" + +#: src/zebtrack/ui/wizard/live_config_step.py:870 +msgid "Unexpected Error" +msgstr "Erro Inesperado" + +#: src/zebtrack/ui/wizard/live_config_step.py:871 +#, python-brace-format +msgid "" +"An error occurred while testing the connection:\n" +"\n" +"{error}" +msgstr "" +"Ocorreu um erro ao testar a conexão:\n" +"\n" +"{error}" + +#: src/zebtrack/ui/wizard/live_config_step.py:905 +msgid "Aquarium Limit" +msgstr "Limitação de Aquários" + +#: src/zebtrack/ui/wizard/live_config_step.py:907 +#, python-brace-format +msgid "" +"⚠️ Simultaneous recording is limited to 2 aquariums.\n" +"\n" +"Your project has {count} aquariums configured.\n" +"\n" +"Options:\n" +"• Reduce to 2 aquariums in the zone configuration\n" +"• Use sequential mode (process aquariums separately)\n" +"• Process offline after recording without detection" +msgstr "" +"⚠️ Gravação simultânea limitada a 2 aquários.\n" +"\n" +"Seu projeto tem {count} aquários configurados.\n" +"\n" +"Opçõ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" + +#: src/zebtrack/ui/wizard/live_config_step.py:918 +#, python-brace-format +msgid "" +"A project with {count} aquariums exceeds the limit of 2 for simultaneous " +"recording." +msgstr "" +"Projeto com {count} aquários excede o limite de 2 para gravação simultânea." + +#: src/zebtrack/ui/wizard/live_config_step.py:928 +msgid "" +"Mode selection cancelled. Adjust the number of aquariums or select a " +"compatible mode." +msgstr "" +"Seleção de modo cancelada. Por favor, ajuste o número de aquários ou " +"selecione um modo compatível." + +#: src/zebtrack/ui/wizard/live_config_step.py:945 +#, python-brace-format +msgid "Error validating data: {error}" +msgstr "Erro ao validar dados: {error}" + +#: src/zebtrack/ui/wizard/live_config_step.py:1050 +#, python-brace-format +msgid "" +"⚠ The template's camera is unavailable — selected '{fallback}'. Please " +"check." +msgstr "" +"⚠ Câmera do template indisponível — selecionada '{fallback}'. Confira." + +#: src/zebtrack/ui/wizard/live_config_step.py:1086 +#, python-brace-format +msgid "" +"⚠ The template's port ({port}) is unavailable — select another one or " +"disable the Arduino." +msgstr "" +"⚠ Porta do template ({port}) indisponível — selecione outra ou desative o " +"Arduino." + +#: src/zebtrack/ui/wizard/templates.py:354 +#, python-brace-format +msgid "Template loaded: {details}" +msgstr "Template carregado: {details}" + #: src/zebtrack/ui/wizard/wizard_dialog.py:199 msgid "< Back" msgstr "< Voltar" # | msgid "Exit" +# | msgid "On Exit" #: src/zebtrack/ui/wizard/wizard_dialog.py:203 #: src/zebtrack/ui/wizard/wizard_dialog.py:308 -#| msgid "On Exit" msgid "Next >" msgstr "Próximo >" +# | msgid "Create New Project" #: src/zebtrack/ui/wizard/wizard_dialog.py:306 -#| msgid "Create New Project" msgid "Create Project" msgstr "Criar Projeto" +# | msgid "Validation Error" #: src/zebtrack/ui/wizard/wizard_dialog.py:464 -#| msgid "Validation Error" msgid "Validation" msgstr "Validação" diff --git a/src/zebtrack/locales/zebtrack.pot b/src/zebtrack/locales/zebtrack.pot index a9bc44e8..1f152c6b 100644 --- a/src/zebtrack/locales/zebtrack.pot +++ b/src/zebtrack/locales/zebtrack.pot @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: DRerio LogAI VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-08-14 10:49-0300\n" +"POT-Creation-Date: 2026-08-14 11:34-0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -224,6 +224,7 @@ msgstr "" #: src/zebtrack/ui/components/validation_manager.py:491 #: src/zebtrack/ui/components/validation_manager.py:1634 #: src/zebtrack/ui/gui.py:144 src/zebtrack/ui/gui.py:808 +#: src/zebtrack/ui/wizard/live_config_step.py:862 msgid "Error" msgstr "" @@ -729,19 +730,19 @@ msgstr "" #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1095 #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1795 -#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2314 +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2316 msgid "Countdown to start the live analysis." msgstr "" #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1097 #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1797 -#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2316 +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2318 msgid "Starting the live analysis." msgstr "" #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1190 #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1866 -#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2385 +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2387 msgid "Live analysis in progress." msgstr "" @@ -787,12 +788,18 @@ msgstr "" msgid "External Trigger Unavailable" msgstr "" -#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2168 +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2024 +#: src/zebtrack/coordinators/recording_session_coordinator.py:551 +#, python-brace-format +msgid "Waiting for external signal... (port {port})" +msgstr "" + +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2170 #: src/zebtrack/ui/components/project_initializer.py:347 msgid "Camera not found" msgstr "" -#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2170 +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2172 #, python-brace-format msgid "" "The camera saved in the project ('{name}') was not detected.\n" @@ -902,6 +909,7 @@ msgid "" msgstr "" #: src/zebtrack/coordinators/model_diagnostics_coordinator.py:742 +#: src/zebtrack/ui/wizard/confirmation_step.py:802 msgid "Error Saving" msgstr "" @@ -1648,6 +1656,7 @@ msgid "📊 Video status:" msgstr "" #: src/zebtrack/core/project/project_workflow_service.py:1331 +#: src/zebtrack/ui/wizard/confirmation_step.py:558 #, python-brace-format msgid " • Total videos: {count}" msgstr "" @@ -3274,6 +3283,7 @@ msgid "No project is currently loaded." msgstr "" #: src/zebtrack/ui/components/arduino_dashboard.py:310 +#: src/zebtrack/ui/wizard/live_config_step.py:863 msgid "" "The pyserial library is not installed.\n" "\n" @@ -3707,6 +3717,7 @@ msgstr "" #: src/zebtrack/ui/components/project_model_configuration_panel.py:189 #: src/zebtrack/ui/components/project_model_configuration_panel.py:378 #: src/zebtrack/ui/components/weight_hardware_manager.py:145 +#: src/zebtrack/ui/wizard/confirmation_step.py:529 msgid "Enabled" msgstr "" @@ -3715,6 +3726,7 @@ msgstr "" #: src/zebtrack/ui/components/project_model_configuration_panel.py:191 #: src/zebtrack/ui/components/project_model_configuration_panel.py:378 #: src/zebtrack/ui/components/weight_hardware_manager.py:145 +#: src/zebtrack/ui/wizard/confirmation_step.py:529 msgid "Disabled" msgstr "" @@ -7110,6 +7122,503 @@ msgstr "" msgid "Buffer radius must be >= 0" msgstr "" +#: src/zebtrack/ui/wizard/confirmation_step.py:88 +msgid "Project Confirmation and Creation" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:96 +msgid "Review the settings and create your project." +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:123 +msgid "Project Name:" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:140 +msgid "Location:" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:152 +msgid "Browse..." +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:158 +msgid "Project Summary" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:189 +msgid "💾 Save as Template" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:198 +msgid "" +"💡 Tip: Review every setting before creating the project. You can save it " +"as a template to reuse later." +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:243 +#, python-brace-format +msgid "Experiment_{group}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:245 +msgid "Experimental_Project" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:247 +msgid "Exploratory_Project" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:260 +msgid "Select the Project Folder" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:318 +msgid "📝 Template Loaded:" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:323 +#, python-brace-format +msgid " • Created at: {value}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:326 +#, python-brace-format +msgid " • Template version: {value}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:331 +msgid "📋 Project Type:" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:333 +msgid "Experimental (pre-recorded)" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:334 +msgid "Exploratory (pre-recorded)" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:335 +msgid "Live (real time)" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:348 +msgid "🔬 Experimental Design:" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:353 +#, python-brace-format +msgid " • {groups} groups x {days} days x {subjects} animals/group" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:360 +#, python-brace-format +msgid " • Total: {sessions} recordings ({animals} animals)" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:366 +#, python-brace-format +msgid " • Groups: {groups}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:370 +msgid "📹 Hardware:" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:375 +#, python-brace-format +msgid " • Camera: {name} (index {index})" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:380 +#, python-brace-format +msgid " • Camera: index {index}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:384 +#, python-brace-format +msgid " • Arduino: {port}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:386 +msgid " • Mode: External Trigger ✓" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:391 +msgid "⏱️ Recording Settings:" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:397 +#, python-brace-format +msgid " • Timed recording: {minutes}min {seconds}s" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:403 +#, python-brace-format +msgid " • Countdown: {seconds}s" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:410 +msgid "⚙️ Processing Intervals:" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:413 +#, python-brace-format +msgid " • Analysis: every {count} frames" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:417 +#, python-brace-format +msgid " • Display: every {count} frames" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:425 +msgid "🎯 Detection Configuration:" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:431 +#: src/zebtrack/ui/wizard/confirmation_step.py:524 +#, python-brace-format +msgid " • Aquarium weight: {weight}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:434 +#: src/zebtrack/ui/wizard/confirmation_step.py:526 +#, python-brace-format +msgid " • Animal weight: {weight}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:452 +msgid "🔍 Detected Design:" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:461 +#, python-brace-format +msgid " • Groups: {count} ({preview}{suffix})" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:467 +#, python-brace-format +msgid " • Days: {count}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:469 +#, python-brace-format +msgid " • Confidence: {value}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:477 +msgid "Groups" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:478 +msgid "Days" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:479 +msgid "Subjects" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:483 +msgid "🧩 Custom Regex:" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:503 +msgid "Segmentation (seg)" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:504 +msgid "Detection (det)" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:508 +msgid "🎯 Detection Settings:" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:515 +#, python-brace-format +msgid " • Aquarium method: {method}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:518 +#, python-brace-format +msgid " • Animal method: {method}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:530 +#, python-brace-format +msgid " • OpenVINO: {status}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:563 +msgid "🌳 Folder Structure (preview):" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:570 +msgid " • (+ 1 additional selection)" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:572 +#, python-brace-format +msgid " • (+ {count} additional selections)" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:577 +msgid "📏 Physical Calibration:" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:583 +#, python-brace-format +msgid " • Aquariums: {count}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:585 +#, python-brace-format +msgid " • Animals per aquarium: {count}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:588 +#, python-brace-format +msgid " • Dimensions: {width} x {height} cm" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:593 +msgid "⚙️ Processing Plan:" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:605 +msgid "Skip (complete data)" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:606 +msgid "Import Zones + track" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:607 +msgid "Partial (arena only)" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:608 +msgid "Full (process from scratch)" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:614 +#, python-brace-format +msgid " • 1 video: {name}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:616 +#, python-brace-format +msgid " • {count} videos: {name}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:628 +#, python-brace-format +msgid "⏱️ Estimated time: ~{minutes} minutes" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:631 +msgid " (1 video to process)" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:633 +#, python-brace-format +msgid " ({count} videos to process)" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:645 +msgid "📦 Existing Parquets:" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:647 +#, python-brace-format +msgid " • Scope: {scope}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:652 +#, python-brace-format +msgid " • Arena: {count}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:653 +#, python-brace-format +msgid " • ROIs: {count}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:654 +#, python-brace-format +msgid " • Trajectory: {count}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:655 +#, python-brace-format +msgid " • Complete: {count}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:668 +msgid "📥 Import Configuration:" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:672 +msgid " ✅ Arena: 1 video" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:674 +#, python-brace-format +msgid " ✅ Arena: {count} videos" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:679 +msgid " ✅ ROIs: 1 video" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:681 +#, python-brace-format +msgid " ✅ ROIs: {count} videos" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:686 +msgid " ✅ Trajectory: 1 video" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:688 +#, python-brace-format +msgid " ✅ Trajectory: {count} videos" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:703 +msgid "Replace existing ROIs" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:704 +msgid "Merge (keep both)" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:705 +msgid "Manual conflict resolution" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:708 +msgid "🔀 ROI Strategy:" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:713 +msgid "(selection)" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:721 +msgid "1 folder" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:721 +#, python-brace-format +msgid "{count} folders" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:725 +msgid "1 file" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:725 +#, python-brace-format +msgid "{count} files" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:728 +msgid "empty" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:749 +msgid " … Preview truncated (full details in step 2)" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:757 +msgid "Save Template" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:758 +msgid "Enter a name for the template:" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:770 +msgid "Save Wizard Template" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:772 +msgid "Wizard Templates" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:789 +#, python-brace-format +msgid "" +"Template '{name}' saved successfully!\n" +"\n" +"File: {path}\n" +"\n" +"You will be able to load this template later to create similar projects " +"quickly." +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:795 +msgid "Template Saved" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:804 +#, python-brace-format +msgid "" +"Could not save the template '{name}'.\n" +"\n" +"Check the logs for more details." +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:821 +msgid "Please enter a name for the project." +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:826 +msgid "" +"The project name contains invalid characters. Use only letters, digits, " +"spaces, '_' and '-'." +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:835 +msgid "Please select a location for the project." +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:838 +#, python-brace-format +msgid "Location does not exist: {location}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:843 +#, python-brace-format +msgid "No write permission at the location: {location}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:851 +#: src/zebtrack/ui/wizard/confirmation_step.py:863 +msgid "The project name is too long for the file system." +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:858 +#, python-brace-format +msgid "A file with that name already exists at: {location}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:874 +#, python-brace-format +msgid "A project with that name already exists at: {location}" +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:886 +msgid "No video selected. Go back and select videos." +msgstr "" + +#: src/zebtrack/ui/wizard/confirmation_step.py:891 +msgid "Configure the camera in the previous step before creating the project." +msgstr "" + #: src/zebtrack/ui/wizard/experimental_design_step.py:154 msgid "Experimental Design Configuration" msgstr "" @@ -7189,6 +7698,368 @@ msgstr "" msgid "📊 Total: {sessions} recordings ({animals} animals x {days} days)" msgstr "" +#: src/zebtrack/ui/wizard/live_config_step.py:159 +msgid "Live Recording Configuration" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:164 +msgid "Set up the camera and the recording options for the live project." +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:181 +msgid "Camera Configuration" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:190 +msgid "Select Camera:" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:204 +msgid "Select the camera for live recording." +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:209 +msgid "🔍 Detect Cameras" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:219 +msgid "Arduino Configuration (Optional)" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:227 +msgid "Use Arduino for synchronization" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:234 +msgid "Enable Arduino to synchronize external events with the recording." +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:243 +msgid "Arduino Port:" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:259 +msgid "🔍 Detect" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:268 +msgid "🔌 Test" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:281 +msgid "External Trigger Mode" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:289 +#, python-brace-format +msgid "" +"External Trigger Mode\n" +"\n" +"What starts the recording becomes the Arduino, not the operator.\n" +"\n" +"When 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" +"\n" +"Useful to synchronize the start with a stimulus, a gate or another " +"instrument.\n" +"\n" +"Requires a connected Arduino and the port selected above — without them " +"the session is refused, not started blind." +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:299 +msgid "Waiting for external signal" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:305 +msgid "Recording Settings" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:314 +msgid "Use timed recording" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:322 +msgid "" +"Enable recording with a fixed duration (stops automatically after the " +"specified time)." +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:332 +msgid "Recording duration (seconds):" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:344 +msgid "Total recording duration in seconds (e.g. 300 = 5 minutes)." +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:350 +msgid "Use a countdown before starting" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:357 +msgid "Show a countdown before starting the recording." +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:365 +msgid "Countdown duration (seconds):" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:379 +msgid "Countdown duration in seconds." +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:388 +msgid "⚙️ Advanced Processing Settings" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:397 +msgid "Preserve the real aquarium shape (segmentation)" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:404 +msgid "" +"Preserve the real aquarium shape\n" +"\n" +"When enabled, keeps the real polygon (N vertices) detected by the YOLO " +"segmentation mask instead of reducing the aquarium to a 4-corner " +"rectangle.\n" +"\n" +"Recommended for circular, hexagonal or irregularly shaped aquariums. " +"Requires a segmentation (seg) model — for detection (det) models this " +"option is ignored." +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:417 +msgid "About Live Projects" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:426 +msgid "" +"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." +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:538 +#, python-brace-format +msgid "" +"Hardware detected:\n" +"\n" +"Capability: {capability}\n" +"CPU: {cores} cores\n" +"RAM: {ram} GB\n" +"GPU: {gpu}\n" +"\n" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:547 +msgid "Yes" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:547 +msgid "No" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:552 +#, python-brace-format +msgid "" +"⚠️ Your system does NOT support real-time processing.\n" +"Recommendation: use '{mode}' mode (offline)." +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:554 +msgid "Recording Only" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:557 +#, python-brace-format +msgid "" +"Supported aquariums: {count}\n" +"Multi-aquarium in real time may not be possible." +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:561 +msgid "Hardware Detection" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:695 +#: src/zebtrack/ui/wizard/live_config_step.py:742 +msgid "Detecting..." +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:709 +#, python-brace-format +msgid "Camera {index}" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:720 +msgid "✓ 1 camera detected" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:722 +#, python-brace-format +msgid "✓ {count} cameras detected" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:735 +msgid "✗ No camera detected" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:762 +msgid "✓ 1 port detected" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:764 +#, python-brace-format +msgid "✓ {count} ports detected" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:780 +#, python-brace-format +msgid "✓ 1 port — using {device}" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:782 +#, python-brace-format +msgid "✓ {count} ports — using {device}" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:799 +msgid "✗ No port detected" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:804 +#: src/zebtrack/ui/wizard/live_config_step.py:860 +msgid "✗ pyserial not installed" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:814 +msgid "No Port Selected" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:815 +msgid "Please detect and select an Arduino port first." +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:825 +msgid "Testing..." +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:833 +msgid "✓ Connection OK" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:835 +msgid "Connection Test" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:837 +#, python-brace-format +msgid "" +"Connection to {port} established successfully!\n" +"\n" +"The port is reachable and ready to use." +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:845 +msgid "✗ Connection failed" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:847 +msgid "Connection Error" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:849 +#, python-brace-format +msgid "" +"Could not connect to port {port}.\n" +"\n" +"Error: {error}\n" +"\n" +"Check that:\n" +"• The Arduino is connected\n" +"• The port is not in use by another program\n" +"• You have permission to access the port" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:868 +msgid "✗ Unexpected error" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:870 +msgid "Unexpected Error" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:871 +#, python-brace-format +msgid "" +"An error occurred while testing the connection:\n" +"\n" +"{error}" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:905 +msgid "Aquarium Limit" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:907 +#, python-brace-format +msgid "" +"⚠️ Simultaneous recording is limited to 2 aquariums.\n" +"\n" +"Your project has {count} aquariums configured.\n" +"\n" +"Options:\n" +"• Reduce to 2 aquariums in the zone configuration\n" +"• Use sequential mode (process aquariums separately)\n" +"• Process offline after recording without detection" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:918 +#, python-brace-format +msgid "" +"A project with {count} aquariums exceeds the limit of 2 for simultaneous " +"recording." +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:928 +msgid "" +"Mode selection cancelled. Adjust the number of aquariums or select a " +"compatible mode." +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:945 +#, python-brace-format +msgid "Error validating data: {error}" +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:1050 +#, python-brace-format +msgid "" +"⚠ The template's camera is unavailable — selected '{fallback}'. Please " +"check." +msgstr "" + +#: src/zebtrack/ui/wizard/live_config_step.py:1086 +#, python-brace-format +msgid "" +"⚠ The template's port ({port}) is unavailable — select another one or " +"disable the Arduino." +msgstr "" + +#: src/zebtrack/ui/wizard/templates.py:354 +#, python-brace-format +msgid "Template loaded: {details}" +msgstr "" + #: src/zebtrack/ui/wizard/wizard_dialog.py:199 msgid "< Back" msgstr "" diff --git a/src/zebtrack/ui/wizard/confirmation_step.py b/src/zebtrack/ui/wizard/confirmation_step.py index 751108e7..e4c231bf 100644 --- a/src/zebtrack/ui/wizard/confirmation_step.py +++ b/src/zebtrack/ui/wizard/confirmation_step.py @@ -27,10 +27,15 @@ import structlog +from zebtrack.i18n import _ from zebtrack.ui.window_utils import create_scrollbar from zebtrack.ui.wizard.base import WizardStep from zebtrack.ui.wizard.enums import ImportAction, ProjectType, WizardStepID -from zebtrack.ui.wizard.templates import TemplateManager, format_template_banner +from zebtrack.ui.wizard.templates import ( + TemplateManager, + format_template_banner, + format_template_banner_details, +) log = structlog.get_logger() @@ -80,7 +85,7 @@ def build_ui(self): title_font = tkfont.Font(size=14, weight="bold") title = Label( self.content_container, - text="Confirmação e Criação do Projeto", + text=_("Project Confirmation and Creation"), font=title_font, bg=background_color, ) @@ -88,7 +93,7 @@ def build_ui(self): subtitle = Label( self.content_container, - text="Revise as configurações e crie seu projeto.", + text=_("Review the settings and create your project."), fg="gray", wraplength=720, bg=background_color, @@ -115,7 +120,7 @@ def build_ui(self): Label( name_frame, - text="Nome do Projeto:", + text=_("Project Name:"), width=20, anchor="w", ).pack(side="left") @@ -132,7 +137,7 @@ def build_ui(self): Label( location_frame, - text="Localização:", + text=_("Location:"), width=20, anchor="w", ).pack(side="left") @@ -144,13 +149,13 @@ def build_ui(self): ) Button( location_frame, - text="Procurar...", + text=_("Browse..."), command=self._browse_location, ).pack(side="left") # Summary (with controlled height to prevent button occlusion) summary_frame = LabelFrame( - self.content_container, text="Resumo do Projeto", padx=10, pady=10 + self.content_container, text=_("Project Summary"), padx=10, pady=10 ) summary_frame.pack(fill="both", expand=True, pady=(0, 10), padx=4) @@ -181,7 +186,7 @@ def build_ui(self): Button( template_btn_frame, - text="💾 Salvar como Template", + text=_("💾 Save as Template"), command=self._save_as_template, width=25, ).pack(side="right") @@ -189,9 +194,9 @@ def build_ui(self): # Help text help_text = Label( self.content_container, - text=( - "💡 Dica: Verifique todas as configurações antes de criar o " - "projeto. Você pode salvar como template para reutilizar." + text=_( + "💡 Tip: Review every setting before creating the project. " + "You can save it as a template to reuse later." ), fg="gray", wraplength=720, @@ -235,11 +240,11 @@ def _generate_default_project_name(self): detected_design = self.wizard_data.get("detected_design") if detected_design and detected_design.get("groups"): groups = detected_design["groups"] - name = f"Experimento_{groups[0]}" + name = _("Experiment_{group}").format(group=groups[0]) else: - name = "Projeto_Experimental" + name = _("Experimental_Project") else: - name = "Projeto_Exploratorio" + name = _("Exploratory_Project") # Add timestamp to make unique from datetime import datetime @@ -252,7 +257,7 @@ def _generate_default_project_name(self): def _browse_location(self): """Open directory browser for project location.""" directory = filedialog.askdirectory( - title="Selecione a Pasta do Projeto", + title=_("Select the Project Folder"), initialdir=self.project_location_var.get(), ) if directory: @@ -310,22 +315,24 @@ def _append_template_info(self, lines: list[str]) -> None: if not metadata: return - lines.append("📝 Template Carregado:") - banner_text = format_template_banner(metadata) - if banner_text: - lines.append(f" • {banner_text.replace('Template carregado: ', '')}") + lines.append(_("📝 Template Loaded:")) + details = format_template_banner_details(metadata) + if details: + lines.append(f" • {details}") if metadata.get("created_at"): - lines.append(f" • Criado em: {metadata['created_at']}") + lines.append(_(" • Created at: {value}").format(value=metadata["created_at"])) if metadata.get("schema_version"): - lines.append(f" • Versão do template: {metadata['schema_version']}") + lines.append( + _(" • Template version: {value}").format(value=metadata["schema_version"]) + ) lines.append("") def _append_project_type(self, lines: list[str], project_type: str) -> None: - lines.append("📋 Tipo de Projeto:") + lines.append(_("📋 Project Type:")) type_names = { - ProjectType.EXPERIMENTAL.value: "Experimental (pré-gravado)", - ProjectType.EXPLORATORY.value: "Exploratório (pré-gravado)", - ProjectType.LIVE.value: "Ao Vivo (tempo real)", + ProjectType.EXPERIMENTAL.value: _("Experimental (pre-recorded)"), + ProjectType.EXPLORATORY.value: _("Exploratory (pre-recorded)"), + ProjectType.LIVE.value: _("Live (real time)"), } lines.append(f" • {type_names.get(project_type, project_type.capitalize())}") @@ -338,72 +345,89 @@ def _append_live_configuration(self, lines: list[str]) -> None: if experiment_days or num_groups or subjects_per_group: lines.append("") - lines.append("🔬 Design Experimental:") + lines.append(_("🔬 Experimental Design:")) if num_groups and subjects_per_group and experiment_days: total_sessions = num_groups * subjects_per_group * experiment_days total_animals = num_groups * subjects_per_group lines.append( - f" • {num_groups} grupos x {experiment_days} dias x " - f"{subjects_per_group} animais/grupo" + _(" • {groups} groups x {days} days x {subjects} animals/group").format( + groups=num_groups, + days=experiment_days, + subjects=subjects_per_group, + ) + ) + lines.append( + _(" • Total: {sessions} recordings ({animals} animals)").format( + sessions=total_sessions, animals=total_animals + ) ) - lines.append(f" • Total: {total_sessions} gravações ({total_animals} animais)") if group_names: group_list = ", ".join(group_names) - lines.append(f" • Grupos: {group_list}") + lines.append(_(" • Groups: {groups}").format(groups=group_list)) # Camera & Hardware lines.append("") - lines.append("📹 Hardware:") + lines.append(_("📹 Hardware:")) camera_index = self.wizard_data.get("camera_index", 0) camera_friendly_name = self.wizard_data.get("camera_friendly_name", "") if camera_friendly_name: - lines.append(f" • Câmera: {camera_friendly_name} (índice {camera_index})") + lines.append( + _(" • Camera: {name} (index {index})").format( + name=camera_friendly_name, index=camera_index + ) + ) else: - lines.append(f" • Câmera: Índice {camera_index}") + lines.append(_(" • Camera: index {index}").format(index=camera_index)) if self.wizard_data.get("use_arduino"): arduino_port = self.wizard_data.get("arduino_port", "N/A") - lines.append(f" • Arduino: {arduino_port}") + lines.append(_(" • Arduino: {port}").format(port=arduino_port)) if self.wizard_data.get("external_trigger_mode"): - lines.append(" • Modo: Gatilho Externo (External Trigger) ✓") + lines.append(_(" • Mode: External Trigger ✓")) # Recording Settings if self.wizard_data.get("use_timed_recording") or self.wizard_data.get("use_countdown"): lines.append("") - lines.append("⏱️ Configurações de Gravação:") + lines.append(_("⏱️ Recording Settings:")) if self.wizard_data.get("use_timed_recording"): duration = self.wizard_data.get("recording_duration_s", 0) minutes = int(duration // 60) seconds = int(duration % 60) - lines.append(f" • Gravação temporizada: {minutes}min {seconds}s") + lines.append( + _(" • Timed recording: {minutes}min {seconds}s").format( + minutes=minutes, seconds=seconds + ) + ) if self.wizard_data.get("use_countdown"): countdown = self.wizard_data.get("countdown_duration_s", 0) - lines.append(f" • Contagem regressiva: {countdown}s") + lines.append(_(" • Countdown: {seconds}s").format(seconds=countdown)) # Processing Intervals analysis_interval = self.wizard_data.get("analysis_interval_frames") display_interval = self.wizard_data.get("display_interval_frames") if analysis_interval or display_interval: lines.append("") - lines.append("⚙️ Intervalos de Processamento:") + lines.append(_("⚙️ Processing Intervals:")) if analysis_interval: - lines.append(f" • Análise: a cada {analysis_interval} frames") + lines.append( + _(" • Analysis: every {count} frames").format(count=analysis_interval) + ) if display_interval: - lines.append(f" • Exibição: a cada {display_interval} frames") + lines.append(_(" • Display: every {count} frames").format(count=display_interval)) # Model Selection weight_assignments = self.wizard_data.get("weight_assignments") detector_params = self.wizard_data.get("detector_parameters") if weight_assignments or detector_params: lines.append("") - lines.append("🎯 Configuração de Detecção:") + lines.append(_("🎯 Detection Configuration:")) if weight_assignments: aquarium_weight = weight_assignments.get("aquarium") animal_weight = weight_assignments.get("animal") if aquarium_weight: - lines.append(f" • Peso aquário: {aquarium_weight}") + lines.append(_(" • Aquarium weight: {weight}").format(weight=aquarium_weight)) if animal_weight: - lines.append(f" • Peso animais: {animal_weight}") + lines.append(_(" • Animal weight: {weight}").format(weight=animal_weight)) if detector_params: conf = detector_params.get("confidence_threshold") nms = detector_params.get("nms_threshold") @@ -421,7 +445,7 @@ def _append_detected_design(self, lines: list[str]) -> None: return lines.append("") - lines.append("🔍 Design Detectado / Design:") + lines.append(_("🔍 Detected Design:")) groups = detected_design.get("groups", []) days = detected_design.get("days", []) confidence = detected_design.get("confidence", 0) @@ -429,12 +453,16 @@ def _append_detected_design(self, lines: list[str]) -> None: if groups: preview = ", ".join(groups[:3]) suffix = "..." if len(groups) > 3 else "" - lines.append(f" • Grupos: {len(groups)} ({preview}{suffix})") + lines.append( + _(" • Groups: {count} ({preview}{suffix})").format( + count=len(groups), preview=preview, suffix=suffix + ) + ) if days: - lines.append(f" • Dias: {len(days)}") + lines.append(_(" • Days: {count}").format(count=len(days))) - lines.append(f" • Confiança: {confidence:.0%}") + lines.append(_(" • Confidence: {value}").format(value=f"{confidence:.0%}")) def _append_custom_regex_info(self, lines: list[str]) -> None: patterns = self.wizard_data.get("custom_regex_patterns") or {} @@ -442,13 +470,13 @@ def _append_custom_regex_info(self, lines: list[str]) -> None: return label_map = { - "group_pattern": "Grupos", - "day_pattern": "Dias", - "subject_pattern": "Sujeitos", + "group_pattern": _("Groups"), + "day_pattern": _("Days"), + "subject_pattern": _("Subjects"), } lines.append("") - lines.append("🧩 Regex Personalizada:") + lines.append(_("🧩 Custom Regex:")) for key, label in label_map.items(): value = patterns.get(key) if value: @@ -468,34 +496,34 @@ def _append_detection_settings(self, lines: list[str]) -> None: return method_labels = { - "seg": "Segmentação (seg)", - "det": "Detecção (det)", + "seg": _("Segmentation (seg)"), + "det": _("Detection (det)"), } lines.append("") - lines.append("🎯 Configurações de Detecção:") + lines.append(_("🎯 Detection Settings:")) aquarium_method = model_selection.get("aquarium_method") animal_method = model_selection.get("animal_method") if aquarium_method or animal_method: if aquarium_method: aquarium_label = method_labels.get(aquarium_method, aquarium_method) - lines.append(f" • Método aquário: {aquarium_label}") + lines.append(_(" • Aquarium method: {method}").format(method=aquarium_label)) if animal_method: animal_label = method_labels.get(animal_method, animal_method) - lines.append(f" • Método animais: {animal_label}") + lines.append(_(" • Animal method: {method}").format(method=animal_label)) if weight_assignments: aquarium_weight = weight_assignments.get("aquarium") animal_weight = weight_assignments.get("animal") if aquarium_weight: - lines.append(f" • Peso aquário: {aquarium_weight}") + lines.append(_(" • Aquarium weight: {weight}").format(weight=aquarium_weight)) if animal_weight: - lines.append(f" • Peso animais: {animal_weight}") + lines.append(_(" • Animal weight: {weight}").format(weight=animal_weight)) if use_openvino is not None: - status = "Ativado" if use_openvino else "Desativado" - lines.append(f" • OpenVINO: {status}") + status = _("Enabled") if use_openvino else _("Disabled") + lines.append(_(" • OpenVINO: {status}").format(status=status)) if detector_params: conf = detector_params.get("confidence_threshold") @@ -523,34 +551,38 @@ def _append_detection_settings(self, lines: list[str]) -> None: def _append_folder_preview(self, lines: list[str]) -> None: video_count = self.wizard_data.get("video_count", 0) if video_count > 0: - lines.append(f" • Total de Vídeos: {video_count}") + lines.append(_(" • Total videos: {count}").format(count=video_count)) folder_preview = self.wizard_data.get("folder_preview") or [] if folder_preview: lines.append("") - lines.append("🌳 Estrutura de Pastas (prévia):") + lines.append(_("🌳 Folder Structure (preview):")) for entry in folder_preview[:2]: lines.extend(self._render_folder_preview(entry)) remaining = len(folder_preview) - 2 if remaining > 0: - lines.append(f" • (+ {remaining} seleção(ões) adicional(is))") + lines.append( + _(" • (+ 1 additional selection)") + if remaining == 1 + else _(" • (+ {count} additional selections)").format(count=remaining) + ) def _append_calibration(self, lines: list[str]) -> None: lines.append("") - lines.append("📏 Calibração Física:") + lines.append(_("📏 Physical Calibration:")) num_aquariums = self.wizard_data.get("num_aquariums", 1) animals_per_aquarium = self.wizard_data.get("animals_per_aquarium", 1) width = self.wizard_data.get("aquarium_width_cm", 10.0) height = self.wizard_data.get("aquarium_height_cm", 10.0) - lines.append(f" • Aquários: {num_aquariums}") - lines.append(f" • Animais por aquário: {animals_per_aquarium}") - lines.append(f" • Dimensões: {width} x {height} cm") + lines.append(_(" • Aquariums: {count}").format(count=num_aquariums)) + lines.append(_(" • Animals per aquarium: {count}").format(count=animals_per_aquarium)) + lines.append(_(" • Dimensions: {width} x {height} cm").format(width=width, height=height)) def _append_processing_plan(self, lines: list[str]) -> None: lines.append("") - lines.append("⚙️ Plano de Processamento:") + lines.append(_("⚙️ Processing Plan:")) import_config = self.wizard_data.get("import_config", []) if not import_config: @@ -562,15 +594,19 @@ def _append_processing_plan(self, lines: list[str]) -> None: action_counts[action] = action_counts.get(action, 0) + 1 action_names = { - ImportAction.SKIP.value: "Skip (dados completos)", - ImportAction.IMPORT_ZONES.value: "Import Zones + rastrear", - ImportAction.PARTIAL.value: "Partial (arena apenas)", - ImportAction.FULL.value: "Full (processar do zero)", + ImportAction.SKIP.value: _("Skip (complete data)"), + ImportAction.IMPORT_ZONES.value: _("Import Zones + track"), + ImportAction.PARTIAL.value: _("Partial (arena only)"), + ImportAction.FULL.value: _("Full (process from scratch)"), } for action, count in sorted(action_counts.items()): name = action_names.get(action, action) - lines.append(f" • {count} vídeo(s): {name}") + lines.append( + _(" • 1 video: {name}").format(name=name) + if count == 1 + else _(" • {count} videos: {name}").format(count=count, name=name) + ) # Estimate processing time (rough estimate: 5 min per video to process) videos_to_process = sum( @@ -580,8 +616,14 @@ def _append_processing_plan(self, lines: list[str]) -> None: if videos_to_process > 0: estimated_minutes = videos_to_process * 5 lines.append("") - lines.append(f"⏱️ Tempo Estimado: ~{estimated_minutes} minutos") - lines.append(f" ({videos_to_process} vídeo(s) para processar)") + lines.append( + _("⏱️ Estimated time: ~{minutes} minutes").format(minutes=estimated_minutes) + ) + lines.append( + _(" (1 video to process)") + if videos_to_process == 1 + else _(" ({count} videos to process)").format(count=videos_to_process) + ) def _append_parquet_summary(self, lines: list[str]) -> None: # Show parquet summary whenever data exists (scope optional for legacy flows) @@ -592,17 +634,17 @@ def _append_parquet_summary(self, lines: list[str]) -> None: parquet_import_scope = self.wizard_data.get("parquet_import_scope") lines.append("") - lines.append("📦 Parquets Existentes:") + lines.append(_("📦 Existing Parquets:")) if parquet_import_scope: - lines.append(f" • Escopo: {parquet_import_scope}") + lines.append(_(" • Scope: {scope}").format(scope=parquet_import_scope)) arena_total = parquet_summary.get("total_arena", 0) rois_total = parquet_summary.get("total_rois", 0) trajectory_total = parquet_summary.get("total_trajectory", 0) complete_total = parquet_summary.get("total_complete", 0) - lines.append(f" • Arena: {arena_total}") - lines.append(f" • ROIs: {rois_total}") - lines.append(f" • Trajetória: {trajectory_total}") - lines.append(f" • Completos: {complete_total}") + lines.append(_(" • Arena: {count}").format(count=arena_total)) + lines.append(_(" • ROIs: {count}").format(count=rois_total)) + lines.append(_(" • Trajectory: {count}").format(count=trajectory_total)) + lines.append(_(" • Complete: {count}").format(count=complete_total)) def _append_import_configuration(self, lines: list[str]) -> None: import_config = self.wizard_data.get("import_config", []) @@ -615,16 +657,28 @@ def _append_import_configuration(self, lines: list[str]) -> None: if importing_arena or importing_rois or importing_trajectory: lines.append("") - lines.append("📥 Configuração de Importação:") + lines.append(_("📥 Import Configuration:")) if importing_arena: arena_count = sum(1 for c in import_config if c.get("import_arena")) - lines.append(f" ✅ Arena: {arena_count} vídeo(s)") + lines.append( + _(" ✅ Arena: 1 video") + if arena_count == 1 + else _(" ✅ Arena: {count} videos").format(count=arena_count) + ) if importing_rois: rois_count = sum(1 for c in import_config if c.get("import_rois")) - lines.append(f" ✅ ROIs: {rois_count} vídeo(s)") + lines.append( + _(" ✅ ROIs: 1 video") + if rois_count == 1 + else _(" ✅ ROIs: {count} videos").format(count=rois_count) + ) if importing_trajectory: traj_count = sum(1 for c in import_config if c.get("import_trajectory")) - lines.append(f" ✅ Trajetória: {traj_count} vídeo(s)") + lines.append( + _(" ✅ Trajectory: 1 video") + if traj_count == 1 + else _(" ✅ Trajectory: {count} videos").format(count=traj_count) + ) def _append_roi_strategy(self, lines: list[str]) -> None: import_config = self.wizard_data.get("import_config", []) @@ -638,28 +692,32 @@ def _append_roi_strategy(self, lines: list[str]) -> None: roi_strategy = self.wizard_data.get("roi_merge_strategy", "replace") strategy_names = { - "replace": "Substituir ROIs existentes", - "merge": "Mesclar (manter ambos)", - "manual": "Resolução manual de conflitos", + "replace": _("Replace existing ROIs"), + "merge": _("Merge (keep both)"), + "manual": _("Manual conflict resolution"), } lines.append("") - lines.append("🔀 Estratégia de ROIs:") + lines.append(_("🔀 ROI Strategy:")) lines.append(f" • {strategy_names.get(roi_strategy, roi_strategy)}") def _render_folder_preview(self, entry: dict) -> list[str]: """Convert folder preview structure into formatted summary lines.""" - label = entry.get("label") or entry.get("path") or "(seleção)" + label = entry.get("label") or entry.get("path") or _("(selection)") counts = entry.get("counts", {}) folders = counts.get("folders", 0) files = counts.get("files", 0) summary_bits: list[str] = [] if folders: - summary_bits.append(f"{folders} pasta(s)") + summary_bits.append( + _("1 folder") if folders == 1 else _("{count} folders").format(count=folders) + ) if files: - summary_bits.append(f"{files} arquivo(s)") + summary_bits.append( + _("1 file") if files == 1 else _("{count} files").format(count=files) + ) - summary_text = ", ".join(summary_bits) if summary_bits else "vazio" + summary_text = ", ".join(summary_bits) if summary_bits else _("empty") lines = [f" • {label}: {summary_text}"] def walk(nodes: list[dict], depth: int) -> None: @@ -680,7 +738,7 @@ def walk(nodes: list[dict], depth: int) -> None: walk(entry.get("nodes", []), 0) if entry.get("truncated"): - lines.append(" … Prévia limitada (detalhes completos na etapa 2)") + lines.append(_(" … Preview truncated (full details in step 2)")) return lines @@ -688,8 +746,8 @@ def _save_as_template(self): """Save current wizard configuration as a template.""" # Ask for template name template_name = simpledialog.askstring( - "Salvar Template", - "Digite um nome para o template:", + _("Save Template"), + _("Enter a name for the template:"), parent=self, ) @@ -701,9 +759,9 @@ def _save_as_template(self): ) + ".json" file_path = filedialog.asksaveasfilename( - title="Salvar Template do Wizard", + title=_("Save Wizard Template"), defaultextension=".json", - filetypes=[("Templates do Wizard", "*.json"), ("JSON", "*.json")], + filetypes=[(_("Wizard Templates"), "*.json"), ("JSON", "*.json")], initialdir=str(self.template_manager.templates_dir), initialfile=suggested_filename, ) @@ -719,23 +777,24 @@ def _save_as_template(self): ) if success: - template_message = ( - f"Template '{template_name}' salvo com sucesso!\n\n" - f"Arquivo: {file_path}\n\n" - "Você poderá carregar este template no futuro " - "para criar projetos similares rapidamente." - ) + template_message = _( + "Template '{name}' saved successfully!\n\n" + "File: {path}\n\n" + "You will be able to load this template later to create " + "similar projects quickly." + ).format(name=template_name, path=file_path) messagebox.showinfo( - "Template Salvo", + _("Template Saved"), template_message, parent=self, ) log.info("wizard.template_saved", name=template_name) else: messagebox.showerror( - "Erro ao Salvar", - f"Não foi possível salvar o template '{template_name}'.\n\n" - f"Verifique os logs para mais detalhes.", + _("Error Saving"), + _( + "Could not save the template '{name}'.\n\nCheck the logs for more details." + ).format(name=template_name), parent=self, ) @@ -750,13 +809,13 @@ def validate(self) -> tuple[bool, str]: project_name = self.project_name_var.get().strip() if not project_name: - return (False, "Por favor, informe um nome para o projeto.") + return (False, _("Please enter a name for the project.")) # Check valid characters (alphanumeric, underscore, hyphen, space) if not re.match(r"^[A-Za-z0-9_\- ]+$", project_name): - message = ( - "Nome do projeto contém caracteres inválidos. " - "Use apenas letras, números, espaços, '_' e '-'." + message = _( + "The project name contains invalid characters. " + "Use only letters, digits, spaces, '_' and '-'." ) return (False, message) @@ -764,27 +823,35 @@ def validate(self) -> tuple[bool, str]: location = self.project_location_var.get().strip() if not location: - return (False, "Por favor, selecione uma localização para o projeto.") + return (False, _("Please select a location for the project.")) if not os.path.exists(location): - return (False, f"Localização não existe: {location}") + return (False, _("Location does not exist: {location}").format(location=location)) if not os.access(location, os.W_OK): - return (False, f"Sem permissão de escrita na localização: {location}") + return ( + False, + _("No write permission at the location: {location}").format(location=location), + ) # Check if project directory already exists project_path = Path(location) / project_name try: project_exists = project_path.exists() except OSError: - return (False, "Nome do projeto é muito longo para o sistema de arquivos.") + return (False, _("The project name is too long for the file system.")) if project_exists: try: if project_path.is_file(): - return (False, f"Já existe um arquivo com esse nome em: {location}") + return ( + False, + _("A file with that name already exists at: {location}").format( + location=location + ), + ) except OSError: - return (False, "Nome do projeto é muito longo para o sistema de arquivos.") + return (False, _("The project name is too long for the file system.")) # Allow reusing an empty directory so long as it has no content try: @@ -795,7 +862,9 @@ def validate(self) -> tuple[bool, str]: if has_contents: return ( False, - f"Já existe um projeto com esse nome em: {location}", + _("A project with that name already exists at: {location}").format( + location=location + ), ) # Validate sources: prerecorded projects require selected videos; @@ -805,12 +874,12 @@ def validate(self) -> tuple[bool, str]: if project_type != ProjectType.LIVE.value: video_count = self.wizard_data.get("video_count", 0) if video_count == 0: - return (False, "Nenhum vídeo selecionado. Volte e selecione vídeos.") + return (False, _("No video selected. Go back and select videos.")) else: if "camera_index" not in self.wizard_data: return ( False, - "Configure a câmera na etapa anterior antes de criar o projeto.", + _("Configure the camera in the previous step before creating the project."), ) return (True, "") diff --git a/src/zebtrack/ui/wizard/live_config_step.py b/src/zebtrack/ui/wizard/live_config_step.py index f1577f97..6185afb1 100644 --- a/src/zebtrack/ui/wizard/live_config_step.py +++ b/src/zebtrack/ui/wizard/live_config_step.py @@ -32,6 +32,7 @@ LiveCameraModeSelector, ) from zebtrack.core.services.wizard_service import WizardService +from zebtrack.i18n import _ from zebtrack.ui.wizard.base import WizardStep from zebtrack.ui.wizard.enums import WizardStepID from zebtrack.ui.wizard.templates import format_template_banner @@ -155,12 +156,12 @@ def build_ui(self): """Build live configuration UI.""" # Title title_font = tkfont.Font(size=14, weight="bold") - title = Label(self, text="Configuração de Gravação ao Vivo", font=title_font) + title = Label(self, text=_("Live Recording Configuration"), font=title_font) title.pack(pady=(0, 10)) subtitle = Label( self, - text="Configure a câmera e as opções de gravação para o projeto ao vivo.", + text=_("Set up the camera and the recording options for the live project."), fg="gray", wraplength=500, ) @@ -177,7 +178,7 @@ def build_ui(self): self.template_info_label.pack_forget() # Camera configuration - camera_frame = LabelFrame(self, text="Configuração de Câmera", padx=15, pady=10) + camera_frame = LabelFrame(self, text=_("Camera Configuration"), padx=15, pady=10) camera_frame.pack(fill="x", pady=(0, 15)) # Camera selection @@ -186,7 +187,7 @@ def build_ui(self): Label( camera_row, - text="Selecionar Câmera:", + text=_("Select Camera:"), width=25, anchor="w", ).pack(side="left") @@ -200,12 +201,12 @@ def build_ui(self): self.camera_combo.pack(side="left", padx=(5, 10)) ToolTip( self.camera_combo, - ("Selecione a câmera para gravação ao vivo."), + _("Select the camera for live recording."), ) Button( camera_row, - text="🔍 Detectar Câmeras", + text=_("🔍 Detect Cameras"), command=self._detect_cameras, ).pack(side="left", padx=10) @@ -215,7 +216,7 @@ def build_ui(self): # Arduino configuration arduino_frame = LabelFrame( self, - text="Configuração de Arduino (Opcional)", + text=_("Arduino Configuration (Optional)"), padx=15, pady=10, ) @@ -223,14 +224,14 @@ def build_ui(self): use_arduino_cb = Checkbutton( arduino_frame, - text="Usar Arduino para sincronização", + text=_("Use Arduino for synchronization"), variable=self.use_arduino_var, command=self._on_arduino_toggle, ) use_arduino_cb.pack(anchor="w", pady=5) ToolTip( use_arduino_cb, - ("Habilitar Arduino para sincronizar eventos externos com a gravação."), + _("Enable Arduino to synchronize external events with the recording."), ) # Arduino port selection @@ -239,7 +240,7 @@ def build_ui(self): Label( self.arduino_port_frame, - text="Porta do Arduino:", + text=_("Arduino Port:"), width=25, anchor="w", ).pack(side="left") @@ -255,7 +256,7 @@ def build_ui(self): self.detect_arduino_btn = Button( self.arduino_port_frame, - text="🔍 Detectar", + text=_("🔍 Detect"), command=self._detect_arduino_ports, state="disabled", width=10, @@ -264,7 +265,7 @@ def build_ui(self): self.test_arduino_btn = Button( self.arduino_port_frame, - text="🔌 Testar", + text=_("🔌 Test"), command=self._test_arduino_connection, state="disabled", width=10, @@ -277,31 +278,31 @@ def build_ui(self): # External trigger mode self.external_trigger_cb = Checkbutton( arduino_frame, - text="Modo de Gatilho Externo (External Trigger)", + text=_("External Trigger Mode"), variable=self.external_trigger_mode_var, state="disabled", ) self.external_trigger_cb.pack(anchor="w", pady=5) ToolTip( self.external_trigger_cb, - ( - "Modo de Gatilho Externo\n\n" - "Quem dá a partida na gravação passa a ser o Arduino, não o " - "operador.\n\n" - "Ao iniciar uma sessão, o app NÃO grava de imediato: ele exibe " - "'Aguardando sinal externo' 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\n" - "Requer Arduino conectado e a porta selecionada acima — sem " - "isso a sessão é recusada, não iniciada às cegas." - ), + _( + "External Trigger Mode\n\n" + "What starts the recording becomes the Arduino, not the " + "operator.\n\n" + "When 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\n" + "Useful to synchronize the start with a stimulus, a gate or " + "another instrument.\n\n" + "Requires a connected Arduino and the port selected above — " + "without them the session is refused, not started blind." + ).format(waiting=_("Waiting for external signal")), ) # Recording settings recording_frame = LabelFrame( self, - text="Configurações de Gravação", + text=_("Recording Settings"), padx=15, pady=10, ) @@ -310,16 +311,16 @@ def build_ui(self): # Timed recording timed_cb = Checkbutton( recording_frame, - text="Usar gravação temporizada", + text=_("Use timed recording"), variable=self.use_timed_recording_var, command=self._on_timed_toggle, ) timed_cb.pack(anchor="w", pady=5) ToolTip( timed_cb, - ( - "Habilitar gravação com duração fixa (desliga automaticamente " - "após o tempo especificado)." + _( + "Enable recording with a fixed duration (stops automatically " + "after the specified time)." ), ) @@ -328,7 +329,7 @@ def build_ui(self): Label( self.duration_frame, - text="Duração da gravação (segundos):", + text=_("Recording duration (seconds):"), width=30, anchor="w", ).pack(side="left") @@ -340,20 +341,20 @@ def build_ui(self): duration_entry.pack(side="left", padx=(5, 0)) ToolTip( duration_entry, - "Duração total da gravação em segundos (ex: 300 = 5 minutos).", + _("Total recording duration in seconds (e.g. 300 = 5 minutes)."), ) # Countdown countdown_cb = Checkbutton( recording_frame, - text="Usar contagem regressiva antes de iniciar", + text=_("Use a countdown before starting"), variable=self.use_countdown_var, command=self._on_countdown_toggle, ) countdown_cb.pack(anchor="w", pady=5) ToolTip( countdown_cb, - "Mostrar contagem regressiva antes de iniciar a gravação.", + _("Show a countdown before starting the recording."), ) self.countdown_frame = Frame(recording_frame) @@ -361,7 +362,7 @@ def build_ui(self): Label( self.countdown_frame, - text="Duração da contagem (segundos):", + text=_("Countdown duration (seconds):"), width=30, anchor="w", ).pack(side="left") @@ -375,7 +376,7 @@ def build_ui(self): countdown_spinbox.pack(side="left", padx=(5, 0)) ToolTip( countdown_spinbox, - "Duração da contagem regressiva em segundos.", + _("Countdown duration in seconds."), ) # Advanced processing settings — only the segmentation-shape toggle @@ -384,7 +385,7 @@ def build_ui(self): # pre-recorded flow. advanced_frame = LabelFrame( self, - text="⚙️ Configurações Avançadas de Processamento", + text=_("⚙️ Advanced Processing Settings"), padx=15, pady=10, ) @@ -393,27 +394,27 @@ def build_ui(self): # Preserve real aquarium shape (segmentation only) preserve_shape_cb = Checkbutton( advanced_frame, - text="Preservar formato real do aquário (segmentação)", + text=_("Preserve the real aquarium shape (segmentation)"), variable=self.preserve_real_aquarium_shape_var, ) preserve_shape_cb.pack(anchor="w", pady=(4, 4)) ToolTip( preserve_shape_cb, - ( - "Preservar formato real do aquário\n\n" - "Quando 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\n" - "Recomendado 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." + _( + "Preserve the real aquarium shape\n\n" + "When enabled, keeps the real polygon (N vertices) detected by " + "the YOLO segmentation mask instead of reducing the aquarium to " + "a 4-corner rectangle.\n\n" + "Recommended for circular, hexagonal or irregularly shaped " + "aquariums. Requires a segmentation (seg) model — for detection " + "(det) models this option is ignored." ), ) # Help text help_frame = LabelFrame( self, - text="Sobre Projetos ao Vivo", + text=_("About Live Projects"), padx=15, pady=10, ) @@ -421,15 +422,15 @@ def build_ui(self): help_text = Label( help_frame, - text=( - "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." + text=_( + "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." ), fg="gray", wraplength=500, @@ -533,26 +534,31 @@ def _detect_hardware_capability(self) -> None: MultiAquariumCapability.LIMITED, MultiAquariumCapability.INSUFFICIENT, ]: - msg = ( - f"Hardware Detectado:\n\n" - f"Capacidade: {self.hardware_report.capability.name}\n" - f"CPU: {self.hardware_report.cpu_cores} cores\n" - f"RAM: {self.hardware_report.available_memory_gb:.1f} GB\n" - f"GPU: {'Sim' if self.hardware_report.has_gpu else 'Não'}\n\n" + msg = _( + "Hardware detected:\n\n" + "Capability: {capability}\n" + "CPU: {cores} cores\n" + "RAM: {ram} GB\n" + "GPU: {gpu}\n\n" + ).format( + capability=self.hardware_report.capability.name, + cores=self.hardware_report.cpu_cores, + ram=f"{self.hardware_report.available_memory_gb:.1f}", + gpu=_("Yes") if self.hardware_report.has_gpu else _("No"), ) if self.hardware_report.capability == MultiAquariumCapability.INSUFFICIENT: - msg += ( - "⚠️ Seu sistema NÃO suporta processamento em tempo real.\n" - "Recomendação: Use modo 'Apenas Gravação' (offline)." - ) + msg += _( + "⚠️ Your system does NOT support real-time processing.\n" + "Recommendation: use '{mode}' mode (offline)." + ).format(mode=_("Recording Only")) else: - msg += ( - f"Aquários suportados: {self.hardware_report.max_aquariums_recommended}\n" - f"Multi-aquário em tempo real pode não ser possível." - ) + msg += _( + "Supported aquariums: {count}\n" + "Multi-aquarium in real time may not be possible." + ).format(count=self.hardware_report.max_aquariums_recommended) - messagebox.showinfo("Detecção de Hardware", msg, parent=self) + messagebox.showinfo(_("Hardware Detection"), msg, parent=self) except Exception as e: # except Exception justified: hardware detection multi-library log.error("live_config.hardware_detection_failed", error=str(e)) @@ -686,7 +692,7 @@ def _on_countdown_toggle(self): def _detect_cameras(self): """Detect available cameras using WizardService.""" - self.camera_status_label.config(text="Detectando...", fg="blue") + self.camera_status_label.config(text=_("Detecting..."), fg="blue") self.update_idletasks() # Use WizardService for camera detection @@ -699,7 +705,7 @@ def _detect_cameras(self): self.camera_friendly_name_map.clear() for cam in cameras: - description = cam.get("description", f"Câmera {cam['index']}") + description = cam.get("description", _("Camera {index}").format(index=cam["index"])) camera_list.append(description) self.camera_index_map[description] = cam["index"] self.camera_friendly_name_map[description] = cam.get("friendly_name", "") @@ -708,7 +714,11 @@ def _detect_cameras(self): self.camera_combo["values"] = camera_list self.camera_status_label.config( - text=f"✓ {len(cameras)} câmera(s) detectada(s)", + text=( + _("✓ 1 camera detected") + if len(cameras) == 1 + else _("✓ {count} cameras detected").format(count=len(cameras)) + ), fg="green", ) @@ -720,14 +730,14 @@ def _detect_cameras(self): self.camera_index_map.clear() self.camera_friendly_name_map.clear() self.camera_status_label.config( - text="✗ Nenhuma câmera detectada", + text=_("✗ No camera detected"), fg="red", ) def _detect_arduino_ports(self): """Detect available Arduino ports using WizardService.""" try: - self.arduino_status_label.config(text="Detectando...", fg="blue") + self.arduino_status_label.config(text=_("Detecting..."), fg="blue") self.update_idletasks() # Use WizardService for Arduino port detection @@ -746,7 +756,11 @@ def _detect_arduino_ports(self): self.arduino_port_combo["values"] = display_list self.arduino_status_label.config( - text=f"✓ {len(ports_info)} porta(s) detectada(s)", + text=( + _("✓ 1 port detected") + if len(ports_info) == 1 + else _("✓ {count} ports detected").format(count=len(ports_info)) + ), fg="green", ) @@ -760,7 +774,13 @@ def _detect_arduino_ports(self): # Dizer QUAL porta foi escolhida — auto-seleção silenciosa # parece mágica e o usuário não confere. self.arduino_status_label.config( - text=f"✓ {len(ports_info)} porta(s) — usando {chosen['device']}", + text=( + _("✓ 1 port — using {device}").format(device=chosen["device"]) + if len(ports_info) == 1 + else _("✓ {count} ports — using {device}").format( + count=len(ports_info), device=chosen["device"] + ) + ), fg="green", ) log.info( @@ -774,12 +794,12 @@ def _detect_arduino_ports(self): self.arduino_port_combo["values"] = [] self.arduino_port_map.clear() self.arduino_status_label.config( - text="✗ Nenhuma porta detectada", + text=_("✗ No port detected"), fg="red", ) except ImportError: self.arduino_status_label.config( - text="✗ pyserial não instalado", + text=_("✗ pyserial not installed"), fg="red", ) log.warning("live_config.pyserial_not_available") @@ -789,8 +809,8 @@ def _test_arduino_connection(self): selected_display = self.arduino_port_var.get() if not selected_display: messagebox.showwarning( - "Nenhuma Porta Selecionada", - "Por favor, detecte e selecione uma porta Arduino primeiro.", + _("No Port Selected"), + _("Please detect and select an Arduino port first."), ) return @@ -800,7 +820,7 @@ def _test_arduino_connection(self): try: import serial - self.arduino_status_label.config(text="Testando...", fg="blue") + self.arduino_status_label.config(text=_("Testing..."), fg="blue") self.update_idletasks() # Try to open serial connection @@ -808,41 +828,47 @@ def _test_arduino_connection(self): ser.close() # Success - self.arduino_status_label.config(text="✓ Conexão OK", fg="green") + self.arduino_status_label.config(text=_("✓ Connection OK"), fg="green") messagebox.showinfo( - "Teste de Conexão", - f"Conexão com {port_device} estabelecida com sucesso!\n\n" - "A porta está acessível e pronta para uso.", + _("Connection Test"), + _( + "Connection to {port} established successfully!\n\n" + "The port is reachable and ready to use." + ).format(port=port_device), ) log.info("live_config.arduino_test_success", port=port_device) except serial.SerialException as e: - self.arduino_status_label.config(text="✗ Falha na conexão", fg="red") + self.arduino_status_label.config(text=_("✗ Connection failed"), fg="red") messagebox.showerror( - "Erro de Conexão", - f"Não foi possível conectar à porta {port_device}.\n\n" - f"Erro: {e!s}\n\n" - "Verifique 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", + _("Connection Error"), + _( + "Could not connect to port {port}.\n\n" + "Error: {error}\n\n" + "Check that:\n" + "• The Arduino is connected\n" + "• The port is not in use by another program\n" + "• You have permission to access the port" + ).format(port=port_device, error=str(e)), ) log.error("live_config.arduino_test_failed", port=port_device, error=str(e)) except ImportError: - self.arduino_status_label.config(text="✗ pyserial não disponível", fg="red") + self.arduino_status_label.config(text=_("✗ pyserial not installed"), fg="red") messagebox.showerror( - "Erro", - "A biblioteca pyserial não está instalada.\n\nExecute: pip install pyserial", + _("Error"), + _("The pyserial library is not installed.\n\nRun: pip install pyserial"), ) log.warning("live_config.pyserial_not_available") except Exception as e: # except Exception justified: serial port + pyserial multi-error - self.arduino_status_label.config(text="✗ Erro inesperado", fg="red") + self.arduino_status_label.config(text=_("✗ Unexpected error"), fg="red") messagebox.showerror( - "Erro Inesperado", - f"Ocorreu um erro ao testar a conexão:\n\n{e!s}", + _("Unexpected Error"), + _("An error occurred while testing the connection:\n\n{error}").format( + error=str(e) + ), ) log.error("live_config.arduino_test_error", error=str(e)) @@ -874,26 +900,32 @@ def validate(self) -> tuple[bool, str]: import tkinter.messagebox as messagebox messagebox.showwarning( - "Limitação de Aquários", - f"⚠️ Gravação simultânea limitada a 2 aquários.\n\n" - f"Seu projeto tem {requested_aquariums} aquários configurados.\n\n" - f"Opções:\n" - f"• Reduza para 2 aquários na configuração de zonas\n" - f"• Use modo sequencial (processar aquários separadamente)\n" - f"• Processe offline após gravação sem detecção", + _("Aquarium Limit"), + _( + "⚠️ Simultaneous recording is limited to 2 aquariums.\n\n" + "Your project has {count} aquariums configured.\n\n" + "Options:\n" + "• Reduce to 2 aquariums in the zone configuration\n" + "• Use sequential mode (process aquariums separately)\n" + "• Process offline after recording without detection" + ).format(count=requested_aquariums), ) return ( False, - f"Projeto com {requested_aquariums} aquários excede o " - f"limite de 2 para gravação simultânea.", + _( + "A project with {count} aquariums exceeds the limit of 2 " + "for simultaneous recording." + ).format(count=requested_aquariums), ) # Check if hardware supports requested aquarium count if not self._check_mode_compatibility(requested_aquariums): return ( False, - "Seleção de modo cancelada. Por favor, ajuste o número " - "de aquários ou selecione um modo compatível.", + _( + "Mode selection cancelled. Adjust the number of aquariums " + "or select a compatible mode." + ), ) # Store selected mode in wizard_data for later use @@ -908,7 +940,7 @@ def validate(self) -> tuple[bool, str]: return (True, "") except (ValueError, KeyError, AttributeError) as e: - return (False, f"Erro ao validar dados: {e!s}") + return (False, _("Error validating data: {error}").format(error=str(e))) def get_data(self) -> dict: """ @@ -1012,7 +1044,9 @@ def _restore_camera(self, data: dict): fallback = next(iter(self.camera_index_map)) self.camera_selection_var.set(fallback) self.camera_status_label.config( - text=f"⚠ Câmera do template indisponível — selecionada '{fallback}'. Confira.", + text=_( + "⚠ The template's camera is unavailable — selected '{fallback}'. Please check." + ).format(fallback=fallback), fg="red", ) log.warning( @@ -1045,10 +1079,10 @@ def _restore_arduino(self, data: dict): # disable Arduino. self.arduino_port_var.set("") self.arduino_status_label.config( - text=( - f"⚠ Porta do template ({port_device}) indisponível — " - "selecione outra ou desative o Arduino." - ), + text=_( + "⚠ The template's port ({port}) is unavailable — select " + "another one or disable the Arduino." + ).format(port=port_device), fg="red", ) log.warning("live_config.arduino_reconcile.port_unavailable", saved_port=port_device) diff --git a/src/zebtrack/ui/wizard/templates.py b/src/zebtrack/ui/wizard/templates.py index 23db0684..e6f608a2 100644 --- a/src/zebtrack/ui/wizard/templates.py +++ b/src/zebtrack/ui/wizard/templates.py @@ -11,6 +11,8 @@ import structlog +from zebtrack.i18n import _ + log = structlog.get_logger() @@ -313,8 +315,15 @@ def _sanitize_name(name: str) -> str: return safe_name or "template" -def format_template_banner(metadata: dict | None) -> str: - """Format banner text for loaded templates.""" +def format_template_banner_details(metadata: dict | None) -> str: + """Return only the template identity — name plus file — with no prefix. + + ``format_template_banner`` wraps this in a translated "Template loaded:" + prefix. ConfirmationStep's summary wants the identity *without* that prefix + and used to obtain it by calling ``str.replace`` on the rendered banner, + which silently stops matching the moment the prefix is translated. Anything + that needs the bare identity must call this instead. + """ if not metadata: return "" @@ -327,9 +336,19 @@ def format_template_banner(metadata: dict | None) -> str: if not name: return "" - banner = f"Template carregado: {name}" + details = str(name) if path: - banner += f" ({Path(path).name})" + details += f" ({Path(path).name})" + + return details + + +def format_template_banner(metadata: dict | None) -> str: + """Format banner text for loaded templates.""" + details = format_template_banner_details(metadata) + + if not details: + return "" - return banner + return _("Template loaded: {details}").format(details=details) diff --git a/tests/ui/wizard/test_calibration_step.py b/tests/ui/wizard/test_calibration_step.py index cea1aefe..c0f1393b 100644 --- a/tests/ui/wizard/test_calibration_step.py +++ b/tests/ui/wizard/test_calibration_step.py @@ -260,7 +260,7 @@ def test_template_banner_display_when_metadata_present(self): # Template info should be set banner_text = step.template_info_var.get() - assert "Template carregado" in banner_text or "Template" in banner_text + assert "Template loaded" in banner_text def test_template_banner_hidden_when_no_metadata(self): """Template banner should be hidden when no template_metadata.""" diff --git a/tests/ui/wizard/test_live_config_step.py b/tests/ui/wizard/test_live_config_step.py index e009994b..e82d2aa3 100644 --- a/tests/ui/wizard/test_live_config_step.py +++ b/tests/ui/wizard/test_live_config_step.py @@ -189,7 +189,7 @@ def test_template_camera_unavailable_falls_back_and_warns(self, monkeypatch): # Saved camera absent → fall back to the only available one + warn. assert step.camera_selection_var.get() == "USB Cam" - assert "indispon" in step.camera_status_label.cget("text").lower() + assert "unavailable" in step.camera_status_label.cget("text").lower() def test_template_camera_available_is_selected_without_warning(self, monkeypatch): monkeypatch.setattr( @@ -206,7 +206,7 @@ def test_template_camera_available_is_selected_without_warning(self, monkeypatch step.on_show() assert step.camera_selection_var.get() == "USB Cam" - assert "indispon" not in step.camera_status_label.cget("text").lower() + assert "unavailable" not in step.camera_status_label.cget("text").lower() def test_template_arduino_port_unavailable_is_cleared_and_blocks_validation(self, monkeypatch): monkeypatch.setattr(WizardService, "detect_available_cameras", lambda *a, **k: []) @@ -227,7 +227,7 @@ def test_template_arduino_port_unavailable_is_cleared_and_blocks_validation(self # Phantom port cleared + warned, not silently carried. assert step.arduino_port_var.get() == "" - assert "indispon" in step.arduino_status_label.cget("text").lower() + assert "unavailable" in step.arduino_status_label.cget("text").lower() # End-to-end safety: validation blocks advancing (Arduino on, no port). is_valid, _msg = WizardService.validate_live_config(step.get_data()) @@ -251,7 +251,7 @@ def test_template_arduino_port_available_is_selected(self, monkeypatch): step.on_show() assert step.arduino_port_var.get() == "COM3 - Arduino" - assert "indispon" not in step.arduino_status_label.cget("text").lower() + assert "unavailable" not in step.arduino_status_label.cget("text").lower() def test_no_template_metadata_skips_reconciliation(self, monkeypatch): """Normal flow (no template): must not force detection or warn.""" diff --git a/tests/ui/wizard/test_wizard_confirmation.py b/tests/ui/wizard/test_wizard_confirmation.py index 9f2c280b..e774c670 100644 --- a/tests/ui/wizard/test_wizard_confirmation.py +++ b/tests/ui/wizard/test_wizard_confirmation.py @@ -100,7 +100,7 @@ def test_default_project_name_experimental(self): name = step.project_name_var.get() # Should contain group name and timestamp - assert "Experimento" in name + assert "Experiment" in name assert "Control" in name def test_default_project_name_exploratory(self): @@ -115,7 +115,7 @@ def test_default_project_name_exploratory(self): name = step.project_name_var.get() - assert "Exploratorio" in name + assert "Exploratory" in name def test_summary_generation_with_design(self): """Summary should be generated with detected design.""" @@ -149,13 +149,13 @@ def test_summary_generation_with_design(self): # Should contain design info assert "Experimental" in summary - assert "Grupos: 2" in summary - assert "Dias: 2" in summary + assert "Groups: 2" in summary + assert "Days: 2" in summary assert "85%" in summary # Confidence - assert "Total de Vídeos: 10" in summary + assert "Total videos: 10" in summary # Should contain processing plan - assert "Plano de Processamento" in summary + assert "Processing Plan" in summary # Should contain parquet summary assert "Arena: 5" in summary @@ -178,7 +178,7 @@ def test_summary_includes_template_metadata(self): assert "Template" in summary assert "Template Especial" in summary - assert "Template carregado" in step.template_info_var.get() + assert "Template loaded" in step.template_info_var.get() def test_summary_includes_folder_preview(self): """Summary should include folder preview details when provided.""" @@ -208,7 +208,7 @@ def test_summary_includes_folder_preview(self): step._generate_summary() summary = step.summary_text - assert "Estrutura de Pastas" in summary + assert "Folder Structure" in summary assert "📁 Estudo" in summary def test_validate_succeeds_with_valid_data(self): @@ -243,7 +243,7 @@ def test_validate_fails_with_empty_name(self): is_valid, error_message = step.validate() assert not is_valid - assert "nome" in error_message.lower() + assert "enter a name" in error_message.lower() def test_validate_fails_with_invalid_characters(self): """Validation should fail with invalid characters in name.""" @@ -260,7 +260,7 @@ def test_validate_fails_with_invalid_characters(self): is_valid, error_message = step.validate() assert not is_valid - assert "caracteres inválidos" in error_message.lower() + assert "invalid characters" in error_message.lower() def test_validate_fails_with_nonexistent_location(self): """Validation should fail with non-existent location.""" @@ -277,7 +277,7 @@ def test_validate_fails_with_nonexistent_location(self): is_valid, error_message = step.validate() assert not is_valid - assert "não existe" in error_message.lower() + assert "does not exist" in error_message.lower() def test_validate_fails_with_existing_project(self): """Validation should fail if project directory already exists.""" @@ -304,7 +304,7 @@ def test_validate_fails_with_existing_project(self): existing_project.rmdir() assert not is_valid - assert "já existe um projeto com esse nome" in error_message.lower() + assert "a project with that name already exists" in error_message.lower() def test_validate_fails_with_no_videos(self): """Validation should fail with no videos.""" @@ -322,7 +322,7 @@ def test_validate_fails_with_no_videos(self): is_valid, error_message = step.validate() assert not is_valid - assert "nenhum vídeo" in error_message.lower() + assert "no video selected" in error_message.lower() def test_validate_allows_live_without_videos(self): """Live projects without prerecorded videos should still validate.""" @@ -446,7 +446,7 @@ def test_error_display_for_missing_project_name(self): assert not is_valid assert error_message != "" - assert "nome" in error_message.lower() + assert "enter a name" in error_message.lower() def test_error_display_for_whitespace_only_name(self): """Validation should fail with whitespace-only project name.""" @@ -460,7 +460,7 @@ def test_error_display_for_whitespace_only_name(self): is_valid, error_message = step.validate() assert not is_valid - assert "nome" in error_message.lower() or "vazio" in error_message.lower() + assert "enter a name" in error_message.lower() def test_validation_handles_unicode_characters(self): """Project names with Unicode characters should be handled gracefully.""" @@ -540,7 +540,7 @@ def test_summary_displays_model_selection_info(self): summary = step.summary_text # Should mention model configuration - assert "Modelo" in summary or "Detecção" in summary or "Segmentação" in summary + assert "Detection Settings" in summary or "Segmentation" in summary def test_summary_displays_calibration_info(self): """Summary should include calibration information.""" @@ -560,7 +560,7 @@ def test_summary_displays_calibration_info(self): summary = step.summary_text # Should mention calibration details - assert "Calibração" in summary or "Dimensões" in summary + assert "Physical Calibration" in summary or "Dimensions" in summary assert "30" in summary # Width assert "25" in summary # Height @@ -619,7 +619,7 @@ def test_live_project_summary_mentions_camera(self): summary = step.summary_text # Should mention live or camera - assert "Ao Vivo" in summary or "Live" in summary or "Câmera" in summary + assert "Live" in summary or "Camera" in summary def test_validation_rejects_very_long_project_names(self): """Validation should reject excessively long project names.""" diff --git a/tests/ui/wizard/test_wizard_integration.py b/tests/ui/wizard/test_wizard_integration.py index 976b082c..87808e2f 100644 --- a/tests/ui/wizard/test_wizard_integration.py +++ b/tests/ui/wizard/test_wizard_integration.py @@ -207,7 +207,7 @@ def test_step4_to_step5_data_flow(self, mock_scan): assert "Experimental" in step5.summary_text # Verify default project name generated - assert "Experimento" in step5.project_name_var.get() + assert "Experiment" in step5.project_name_var.get() @patch("zebtrack.ui.wizard.detection_step.ProjectManager.scan_input_paths") def test_complete_wizard_flow_experimental(self, mock_scan): @@ -335,7 +335,7 @@ def test_complete_wizard_flow_exploratory(self, mock_scan): wizard_data.update(step5.get_data()) # Verify exploratory project name - assert "Exploratorio" in step5.project_name_var.get() + assert "Exploratory" in step5.project_name_var.get() def test_set_data_restores_state_across_all_steps(self): """Test that set_data works for all steps (back navigation).""" From 97d1225d27320ddde835ec2da0bf2a3284c7a7cc Mon Sep 17 00:00:00 2001 From: MarkSant Date: Fri, 14 Aug 2026 15:57:04 -0300 Subject: [PATCH 11/21] feat(i18n): traduzir model_selection_step e detection_step (lote 4c) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 69 literais acentuados saem de ui/wizard (561 -> 97 na pasta; wizard fica so com os seis arquivos do lote 4d). Os dois arquivos tinham um dict de rotulos em CORPO DE MODULO (_METHOD_LABELS, _METHOD_OPTIONS): traduzir no lugar resolveria o catalogo no import e congelaria o idioma, entao viraram funcao. Tres defeitos estruturais em model_selection_step, nenhum visivel no scan: 1. " ⭐ Recomendado" era anexado ao peso recomendado PARA EXIBIR e retirado de volta por _strip_annotation() e pelo startswith de revalidacao. Traduzir so o lado da exibicao deixaria o marcador colado no nome do peso, e entao validate() recusaria um peso que o usuario acabou de escolher na propria lista -- com a mensagem "selecione um peso valido". Agora existe uma definicao unica, _recommended_suffix(), usada nos tres pontos. 2. _refresh_weight_dropdowns fazia `rec, _ = ...` duas linhas acima de onde o marcador precisa chamar _(). O gettext viraria o segundo elemento da tupla e seria chamado como funcao (TypeError), ou ficaria nao-atribuido quando nao ha perspectiva (UnboundLocalError). Mais tres `name, _ =` em _default_weight_for_method, renomeados pelo mesmo motivo. 3. O erro de faixa montava f"❌ {label.capitalize()} deve estar entre 0 e 1", capitalizando um substantivo traduzido. Virou uma frase pronta por campo, que e o que o guia pede e tambem resolve a concordancia. Nos testes, quatro sitios DIRIGIAM o widget com o rotulo em portugues (set("Detecção (det)")). Com o rotulo traduzido eles deixam de resolver, _method_key_from_label devolve a string crua e o hint fica vazio -- o que fazia test_animal_method_hint_cleared_for_seg, que exige justamente hint vazio, passar pelo motivo errado. Passam a usar _method_display(). Verificacao: ruff limpo, mypy limpo em 698 arquivos, 4210 fast, 1079 gui. Co-Authored-By: Claude Opus 5 --- docs/tasks/active/ROLLING_TASK_LOG.md | 21 +- .../locales/_pairs/pr3-wizard-4c.json | 114 +++ .../locales/pt_BR/LC_MESSAGES/zebtrack.mo | Bin 180629 -> 194544 bytes .../locales/pt_BR/LC_MESSAGES/zebtrack.po | 858 +++++++++++++++--- src/zebtrack/locales/zebtrack.pot | 758 +++++++++++++--- src/zebtrack/ui/wizard/detection_step.py | 185 ++-- .../ui/wizard/model_selection_step.py | 309 ++++--- tests/ui/wizard/test_model_selection_step.py | 16 +- tests/ui/wizard/test_wizard_detection.py | 4 +- 9 files changed, 1804 insertions(+), 461 deletions(-) create mode 100644 src/zebtrack/locales/_pairs/pr3-wizard-4c.json diff --git a/docs/tasks/active/ROLLING_TASK_LOG.md b/docs/tasks/active/ROLLING_TASK_LOG.md index 592cd9f5..718c1450 100644 --- a/docs/tasks/active/ROLLING_TASK_LOG.md +++ b/docs/tasks/active/ROLLING_TASK_LOG.md @@ -79,7 +79,26 @@ no topo do ratchet; depois, atualizar a seção "A migração" em 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`. - - [ ] 4c: `model_selection_step.py` (41) + `detection_step.py` (28). + - [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()`. - [ ] 4d: `discovery_step` (20), `custom_regex_dialog` (19), `file_selection_step` (19), `import_config_step` (16), `calibration_step` (14), `design_editor_dialog` (9). 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/pt_BR/LC_MESSAGES/zebtrack.mo b/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.mo index ef85fa08f377700b2b45aa4598c8e6c806afb677..2c0df80c53697fd8952cff02d2abdea10a9b8248 100644 GIT binary patch delta 39264 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 delta 27021 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\n" "Language-Team: pt_BR \n" @@ -259,7 +259,7 @@ msgstr "Vídeo Único" #: src/zebtrack/ui/components/validation_manager.py:491 #: src/zebtrack/ui/components/validation_manager.py:1634 #: src/zebtrack/ui/gui.py:144 src/zebtrack/ui/gui.py:808 -#: src/zebtrack/ui/wizard/live_config_step.py:862 +#: src/zebtrack/ui/wizard/live_config_step.py:860 msgid "Error" msgstr "Erro" @@ -470,6 +470,7 @@ 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/wizard/detection_step.py:223 msgid "No video selected." msgstr "Nenhum vídeo selecionado." @@ -949,7 +950,7 @@ msgstr "Análise concluída." #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1095 #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1795 -#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2316 +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2314 msgid "Countdown to start the live analysis." msgstr "Contagem regressiva para iniciar a análise ao vivo." @@ -957,14 +958,14 @@ msgstr "Contagem regressiva para iniciar a análise ao vivo." # | msgid "Single Video Analysis Options" #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1097 #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1797 -#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2318 +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2316 msgid "Starting the live analysis." msgstr "Iniciando análise ao vivo." # | msgid "Analysis in Progress" #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1190 #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1866 -#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2387 +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2385 msgid "Live analysis in progress." msgstr "Análise ao vivo em andamento." @@ -1017,19 +1018,19 @@ 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:551 +#: 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:2170 +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2168 #: src/zebtrack/ui/components/project_initializer.py:347 msgid "Camera not found" msgstr "Câmera não encontrada" -#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2172 +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2170 #, python-brace-format msgid "" "The camera saved in the project ('{name}') was not detected.\n" @@ -1156,7 +1157,7 @@ msgstr "" # | msgid "Error Saving to Project" #: src/zebtrack/coordinators/model_diagnostics_coordinator.py:742 -#: src/zebtrack/ui/wizard/confirmation_step.py:802 +#: src/zebtrack/ui/wizard/confirmation_step.py:794 msgid "Error Saving" msgstr "Erro ao Salvar" @@ -2088,7 +2089,7 @@ msgstr "📊 Status dos vídeos:" # | msgid "Failure: {error}" # | msgid "❌ Failures: {count}" #: src/zebtrack/core/project/project_workflow_service.py:1331 -#: src/zebtrack/ui/wizard/confirmation_step.py:558 +#: src/zebtrack/ui/wizard/confirmation_step.py:554 #, python-brace-format msgid " • Total videos: {count}" msgstr " • Total de vídeos: {count}" @@ -4078,7 +4079,7 @@ msgid "No project is currently loaded." msgstr "Nenhum projeto está carregado no momento." #: src/zebtrack/ui/components/arduino_dashboard.py:310 -#: src/zebtrack/ui/wizard/live_config_step.py:863 +#: src/zebtrack/ui/wizard/live_config_step.py:861 msgid "" "The pyserial library is not installed.\n" "\n" @@ -4625,7 +4626,8 @@ msgstr "💡 Abre o diálogo de Calibração e Detecção." #: src/zebtrack/ui/components/project_model_configuration_panel.py:189 #: src/zebtrack/ui/components/project_model_configuration_panel.py:378 #: src/zebtrack/ui/components/weight_hardware_manager.py:145 -#: src/zebtrack/ui/wizard/confirmation_step.py:529 +#: src/zebtrack/ui/wizard/confirmation_step.py:525 +#: src/zebtrack/ui/wizard/detection_step.py:928 msgid "Enabled" msgstr "Ativado" @@ -4634,7 +4636,8 @@ msgstr "Ativado" #: src/zebtrack/ui/components/project_model_configuration_panel.py:191 #: src/zebtrack/ui/components/project_model_configuration_panel.py:378 #: src/zebtrack/ui/components/weight_hardware_manager.py:145 -#: src/zebtrack/ui/wizard/confirmation_step.py:529 +#: src/zebtrack/ui/wizard/confirmation_step.py:525 +#: src/zebtrack/ui/wizard/detection_step.py:928 msgid "Disabled" msgstr "Desativado" @@ -5357,6 +5360,7 @@ msgid "Optimise with OpenVINO (for Intel hardware)" msgstr "Otimizar com OpenVINO (para hardware Intel)" #: src/zebtrack/ui/components/global_model_configuration_panel.py:281 +#: src/zebtrack/ui/wizard/model_selection_step.py:458 msgid "OpenVINO device:" msgstr "Dispositivo OpenVINO:" @@ -6645,6 +6649,7 @@ msgstr "Σ Sumário" #: src/zebtrack/ui/components/processing_reports.py:186 #: src/zebtrack/ui/components/project_overview.py:137 +#: src/zebtrack/ui/wizard/detection_step.py:149 msgid "Status" msgstr "Status" @@ -8971,6 +8976,7 @@ msgid " • Total: {sessions} recordings ({animals} animals)" msgstr " • Total: {sessions} gravações ({animals} animais)" #: src/zebtrack/ui/wizard/confirmation_step.py:366 +#: src/zebtrack/ui/wizard/detection_step.py:819 #, python-brace-format msgid " • Groups: {groups}" msgstr " • Grupos: {groups}" @@ -9021,288 +9027,304 @@ msgstr "⚙️ Intervalos de Processamento:" msgid " • Analysis: every {count} frames" msgstr " • Análise: a cada {count} frames" -#: src/zebtrack/ui/wizard/confirmation_step.py:417 +#: src/zebtrack/ui/wizard/confirmation_step.py:416 #, python-brace-format msgid " • Display: every {count} frames" msgstr " • Exibição: a cada {count} frames" -#: src/zebtrack/ui/wizard/confirmation_step.py:425 +#: src/zebtrack/ui/wizard/confirmation_step.py:423 msgid "🎯 Detection Configuration:" msgstr "🎯 Configuração de Detecção:" -#: src/zebtrack/ui/wizard/confirmation_step.py:431 -#: src/zebtrack/ui/wizard/confirmation_step.py:524 +#: src/zebtrack/ui/wizard/confirmation_step.py:428 +#: src/zebtrack/ui/wizard/confirmation_step.py:520 +#: src/zebtrack/ui/wizard/detection_step.py:923 #, python-brace-format msgid " • Aquarium weight: {weight}" msgstr " • Peso aquário: {weight}" -#: src/zebtrack/ui/wizard/confirmation_step.py:434 -#: src/zebtrack/ui/wizard/confirmation_step.py:526 +#: src/zebtrack/ui/wizard/confirmation_step.py:430 +#: src/zebtrack/ui/wizard/confirmation_step.py:522 +#: src/zebtrack/ui/wizard/detection_step.py:925 #, python-brace-format msgid " • Animal weight: {weight}" msgstr " • Peso animais: {weight}" -#: src/zebtrack/ui/wizard/confirmation_step.py:452 +#: src/zebtrack/ui/wizard/confirmation_step.py:448 msgid "🔍 Detected Design:" msgstr "🔍 Design Detectado / Design:" -#: src/zebtrack/ui/wizard/confirmation_step.py:461 +#: src/zebtrack/ui/wizard/confirmation_step.py:457 #, python-brace-format msgid " • Groups: {count} ({preview}{suffix})" msgstr " • Grupos: {count} ({preview}{suffix})" -#: src/zebtrack/ui/wizard/confirmation_step.py:467 +#: src/zebtrack/ui/wizard/confirmation_step.py:463 #, python-brace-format msgid " • Days: {count}" msgstr " • Dias: {count}" -#: src/zebtrack/ui/wizard/confirmation_step.py:469 +#: src/zebtrack/ui/wizard/confirmation_step.py:465 +#: src/zebtrack/ui/wizard/detection_step.py:832 #, python-brace-format msgid " • Confidence: {value}" msgstr " • Confiança: {value}" -#: src/zebtrack/ui/wizard/confirmation_step.py:477 +#: src/zebtrack/ui/wizard/confirmation_step.py:473 +#: src/zebtrack/ui/wizard/detection_step.py:873 msgid "Groups" msgstr "Grupos" -#: src/zebtrack/ui/wizard/confirmation_step.py:478 +#: src/zebtrack/ui/wizard/confirmation_step.py:474 +#: src/zebtrack/ui/wizard/detection_step.py:874 msgid "Days" msgstr "Dias" -#: src/zebtrack/ui/wizard/confirmation_step.py:479 +#: src/zebtrack/ui/wizard/confirmation_step.py:475 +#: src/zebtrack/ui/wizard/detection_step.py:875 msgid "Subjects" msgstr "Sujeitos" -#: src/zebtrack/ui/wizard/confirmation_step.py:483 +#: src/zebtrack/ui/wizard/confirmation_step.py:479 msgid "🧩 Custom Regex:" msgstr "🧩 Regex Personalizada:" -#: src/zebtrack/ui/wizard/confirmation_step.py:503 +#: src/zebtrack/ui/wizard/confirmation_step.py:499 +#: src/zebtrack/ui/wizard/detection_step.py:42 +#: src/zebtrack/ui/wizard/model_selection_step.py:40 msgid "Segmentation (seg)" msgstr "Segmentação (seg)" -#: src/zebtrack/ui/wizard/confirmation_step.py:504 +#: src/zebtrack/ui/wizard/confirmation_step.py:500 +#: src/zebtrack/ui/wizard/detection_step.py:43 +#: src/zebtrack/ui/wizard/model_selection_step.py:41 msgid "Detection (det)" msgstr "Detecção (det)" -#: src/zebtrack/ui/wizard/confirmation_step.py:508 +#: src/zebtrack/ui/wizard/confirmation_step.py:504 msgid "🎯 Detection Settings:" msgstr "🎯 Configurações de Detecção:" -#: src/zebtrack/ui/wizard/confirmation_step.py:515 +#: src/zebtrack/ui/wizard/confirmation_step.py:511 +#: src/zebtrack/ui/wizard/detection_step.py:915 #, python-brace-format msgid " • Aquarium method: {method}" msgstr " • Método aquário: {method}" -#: src/zebtrack/ui/wizard/confirmation_step.py:518 +#: src/zebtrack/ui/wizard/confirmation_step.py:514 +#: src/zebtrack/ui/wizard/detection_step.py:917 #, python-brace-format msgid " • Animal method: {method}" msgstr " • Método animais: {method}" -#: src/zebtrack/ui/wizard/confirmation_step.py:530 +#: src/zebtrack/ui/wizard/confirmation_step.py:526 +#: src/zebtrack/ui/wizard/detection_step.py:929 #, python-brace-format msgid " • OpenVINO: {status}" msgstr " • OpenVINO: {status}" -#: src/zebtrack/ui/wizard/confirmation_step.py:563 +#: src/zebtrack/ui/wizard/confirmation_step.py:559 msgid "🌳 Folder Structure (preview):" msgstr "🌳 Estrutura de Pastas (prévia):" -#: src/zebtrack/ui/wizard/confirmation_step.py:570 +#: src/zebtrack/ui/wizard/confirmation_step.py:566 msgid " • (+ 1 additional selection)" msgstr " • (+ 1 seleção adicional)" -#: src/zebtrack/ui/wizard/confirmation_step.py:572 +#: src/zebtrack/ui/wizard/confirmation_step.py:568 #, python-brace-format msgid " • (+ {count} additional selections)" msgstr " • (+ {count} seleções adicionais)" -#: src/zebtrack/ui/wizard/confirmation_step.py:577 +#: src/zebtrack/ui/wizard/confirmation_step.py:573 msgid "📏 Physical Calibration:" msgstr "📏 Calibração Física:" -#: src/zebtrack/ui/wizard/confirmation_step.py:583 +#: src/zebtrack/ui/wizard/confirmation_step.py:579 #, python-brace-format msgid " • Aquariums: {count}" msgstr " • Aquários: {count}" -#: src/zebtrack/ui/wizard/confirmation_step.py:585 +#: src/zebtrack/ui/wizard/confirmation_step.py:580 #, python-brace-format msgid " • Animals per aquarium: {count}" msgstr " • Animais por aquário: {count}" -#: src/zebtrack/ui/wizard/confirmation_step.py:588 +#: src/zebtrack/ui/wizard/confirmation_step.py:581 #, python-brace-format msgid " • Dimensions: {width} x {height} cm" msgstr " • Dimensões: {width} x {height} cm" -#: src/zebtrack/ui/wizard/confirmation_step.py:593 +#: src/zebtrack/ui/wizard/confirmation_step.py:585 msgid "⚙️ Processing Plan:" msgstr "⚙️ Plano de Processamento:" -#: src/zebtrack/ui/wizard/confirmation_step.py:605 +#: src/zebtrack/ui/wizard/confirmation_step.py:597 msgid "Skip (complete data)" msgstr "Skip (dados completos)" -#: src/zebtrack/ui/wizard/confirmation_step.py:606 +#: src/zebtrack/ui/wizard/confirmation_step.py:598 msgid "Import Zones + track" msgstr "Import Zones + rastrear" -#: src/zebtrack/ui/wizard/confirmation_step.py:607 +#: src/zebtrack/ui/wizard/confirmation_step.py:599 msgid "Partial (arena only)" msgstr "Partial (arena apenas)" -#: src/zebtrack/ui/wizard/confirmation_step.py:608 +#: src/zebtrack/ui/wizard/confirmation_step.py:600 msgid "Full (process from scratch)" msgstr "Full (processar do zero)" -#: src/zebtrack/ui/wizard/confirmation_step.py:614 +#: src/zebtrack/ui/wizard/confirmation_step.py:606 #, python-brace-format msgid " • 1 video: {name}" msgstr " • 1 vídeo: {name}" -#: src/zebtrack/ui/wizard/confirmation_step.py:616 +#: src/zebtrack/ui/wizard/confirmation_step.py:608 #, python-brace-format msgid " • {count} videos: {name}" msgstr " • {count} vídeos: {name}" -#: src/zebtrack/ui/wizard/confirmation_step.py:628 +#: src/zebtrack/ui/wizard/confirmation_step.py:620 #, python-brace-format msgid "⏱️ Estimated time: ~{minutes} minutes" msgstr "⏱️ Tempo Estimado: ~{minutes} minutos" -#: src/zebtrack/ui/wizard/confirmation_step.py:631 +#: src/zebtrack/ui/wizard/confirmation_step.py:623 msgid " (1 video to process)" msgstr " (1 vídeo para processar)" -#: src/zebtrack/ui/wizard/confirmation_step.py:633 +#: src/zebtrack/ui/wizard/confirmation_step.py:625 #, python-brace-format msgid " ({count} videos to process)" msgstr " ({count} vídeos para processar)" -#: src/zebtrack/ui/wizard/confirmation_step.py:645 +#: src/zebtrack/ui/wizard/confirmation_step.py:637 msgid "📦 Existing Parquets:" msgstr "📦 Parquets Existentes:" -#: src/zebtrack/ui/wizard/confirmation_step.py:647 +#: src/zebtrack/ui/wizard/confirmation_step.py:639 #, python-brace-format msgid " • Scope: {scope}" msgstr " • Escopo: {scope}" -#: src/zebtrack/ui/wizard/confirmation_step.py:652 +#: src/zebtrack/ui/wizard/confirmation_step.py:644 +#: src/zebtrack/ui/wizard/detection_step.py:796 #, python-brace-format msgid " • Arena: {count}" msgstr " • Arena: {count}" -#: src/zebtrack/ui/wizard/confirmation_step.py:653 +#: src/zebtrack/ui/wizard/confirmation_step.py:645 +#: src/zebtrack/ui/wizard/detection_step.py:797 #, python-brace-format msgid " • ROIs: {count}" msgstr " • ROIs: {count}" -#: src/zebtrack/ui/wizard/confirmation_step.py:654 +#: src/zebtrack/ui/wizard/confirmation_step.py:646 +#: src/zebtrack/ui/wizard/detection_step.py:799 #, python-brace-format msgid " • Trajectory: {count}" msgstr " • Trajetória: {count}" -#: src/zebtrack/ui/wizard/confirmation_step.py:655 +#: src/zebtrack/ui/wizard/confirmation_step.py:647 #, python-brace-format msgid " • Complete: {count}" msgstr " • Completos: {count}" -#: src/zebtrack/ui/wizard/confirmation_step.py:668 +#: src/zebtrack/ui/wizard/confirmation_step.py:660 msgid "📥 Import Configuration:" msgstr "📥 Configuração de Importação:" -#: src/zebtrack/ui/wizard/confirmation_step.py:672 +#: src/zebtrack/ui/wizard/confirmation_step.py:664 msgid " ✅ Arena: 1 video" msgstr " ✅ Arena: 1 vídeo" -#: src/zebtrack/ui/wizard/confirmation_step.py:674 +#: src/zebtrack/ui/wizard/confirmation_step.py:666 #, python-brace-format msgid " ✅ Arena: {count} videos" msgstr " ✅ Arena: {count} vídeos" -#: src/zebtrack/ui/wizard/confirmation_step.py:679 +#: src/zebtrack/ui/wizard/confirmation_step.py:671 msgid " ✅ ROIs: 1 video" msgstr " ✅ ROIs: 1 vídeo" -#: src/zebtrack/ui/wizard/confirmation_step.py:681 +#: src/zebtrack/ui/wizard/confirmation_step.py:673 #, python-brace-format msgid " ✅ ROIs: {count} videos" msgstr " ✅ ROIs: {count} vídeos" -#: src/zebtrack/ui/wizard/confirmation_step.py:686 +#: src/zebtrack/ui/wizard/confirmation_step.py:678 msgid " ✅ Trajectory: 1 video" msgstr " ✅ Trajetória: 1 vídeo" -#: src/zebtrack/ui/wizard/confirmation_step.py:688 +#: src/zebtrack/ui/wizard/confirmation_step.py:680 #, python-brace-format msgid " ✅ Trajectory: {count} videos" msgstr " ✅ Trajetória: {count} vídeos" -#: src/zebtrack/ui/wizard/confirmation_step.py:703 +#: src/zebtrack/ui/wizard/confirmation_step.py:695 msgid "Replace existing ROIs" msgstr "Substituir ROIs existentes" -#: src/zebtrack/ui/wizard/confirmation_step.py:704 +#: src/zebtrack/ui/wizard/confirmation_step.py:696 msgid "Merge (keep both)" msgstr "Mesclar (manter ambos)" -#: src/zebtrack/ui/wizard/confirmation_step.py:705 +#: src/zebtrack/ui/wizard/confirmation_step.py:697 msgid "Manual conflict resolution" msgstr "Resolução manual de conflitos" -#: src/zebtrack/ui/wizard/confirmation_step.py:708 +#: src/zebtrack/ui/wizard/confirmation_step.py:700 msgid "🔀 ROI Strategy:" msgstr "🔀 Estratégia de ROIs:" -#: src/zebtrack/ui/wizard/confirmation_step.py:713 +#: src/zebtrack/ui/wizard/confirmation_step.py:705 msgid "(selection)" msgstr "(seleção)" -#: src/zebtrack/ui/wizard/confirmation_step.py:721 +#: src/zebtrack/ui/wizard/confirmation_step.py:713 msgid "1 folder" msgstr "1 pasta" -#: src/zebtrack/ui/wizard/confirmation_step.py:721 +#: src/zebtrack/ui/wizard/confirmation_step.py:713 #, python-brace-format msgid "{count} folders" msgstr "{count} pastas" -#: src/zebtrack/ui/wizard/confirmation_step.py:725 +#: src/zebtrack/ui/wizard/confirmation_step.py:717 msgid "1 file" msgstr "1 arquivo" -#: src/zebtrack/ui/wizard/confirmation_step.py:725 +#: src/zebtrack/ui/wizard/confirmation_step.py:717 #, python-brace-format msgid "{count} files" msgstr "{count} arquivos" -#: src/zebtrack/ui/wizard/confirmation_step.py:728 +#: src/zebtrack/ui/wizard/confirmation_step.py:720 msgid "empty" msgstr "vazio" -#: src/zebtrack/ui/wizard/confirmation_step.py:749 +#: src/zebtrack/ui/wizard/confirmation_step.py:741 msgid " … Preview truncated (full details in step 2)" msgstr " … Prévia limitada (detalhes completos na etapa 2)" -#: src/zebtrack/ui/wizard/confirmation_step.py:757 +#: src/zebtrack/ui/wizard/confirmation_step.py:749 msgid "Save Template" msgstr "Salvar Template" -#: src/zebtrack/ui/wizard/confirmation_step.py:758 +#: src/zebtrack/ui/wizard/confirmation_step.py:750 msgid "Enter a name for the template:" msgstr "Digite um nome para o template:" -#: src/zebtrack/ui/wizard/confirmation_step.py:770 +#: src/zebtrack/ui/wizard/confirmation_step.py:762 msgid "Save Wizard Template" msgstr "Salvar Template do Wizard" -#: src/zebtrack/ui/wizard/confirmation_step.py:772 +#: src/zebtrack/ui/wizard/confirmation_step.py:764 msgid "Wizard Templates" msgstr "Templates do Wizard" -#: src/zebtrack/ui/wizard/confirmation_step.py:789 +#: src/zebtrack/ui/wizard/confirmation_step.py:781 #, python-brace-format msgid "" "Template '{name}' saved successfully!\n" @@ -9317,11 +9339,11 @@ msgstr "" "\n" "Você poderá carregar este template no futuro para criar projetos similares rapidamente." -#: src/zebtrack/ui/wizard/confirmation_step.py:795 +#: src/zebtrack/ui/wizard/confirmation_step.py:787 msgid "Template Saved" msgstr "Template Salvo" -#: src/zebtrack/ui/wizard/confirmation_step.py:804 +#: src/zebtrack/ui/wizard/confirmation_step.py:796 #, python-brace-format msgid "" "Could not save the template '{name}'.\n" @@ -9332,11 +9354,11 @@ msgstr "" "\n" "Verifique os logs para mais detalhes." -#: src/zebtrack/ui/wizard/confirmation_step.py:821 +#: src/zebtrack/ui/wizard/confirmation_step.py:812 msgid "Please enter a name for the project." msgstr "Por favor, informe um nome para o projeto." -#: src/zebtrack/ui/wizard/confirmation_step.py:826 +#: src/zebtrack/ui/wizard/confirmation_step.py:817 msgid "" "The project name contains invalid characters. Use only letters, digits, " "spaces, '_' and '-'." @@ -9344,43 +9366,255 @@ msgstr "" "Nome do projeto contém caracteres inválidos. Use apenas letras, números, " "espaços, '_' e '-'." -#: src/zebtrack/ui/wizard/confirmation_step.py:835 +#: src/zebtrack/ui/wizard/confirmation_step.py:826 msgid "Please select a location for the project." msgstr "Por favor, selecione uma localização para o projeto." -#: src/zebtrack/ui/wizard/confirmation_step.py:838 +#: src/zebtrack/ui/wizard/confirmation_step.py:829 #, python-brace-format msgid "Location does not exist: {location}" msgstr "Localização não existe: {location}" -#: src/zebtrack/ui/wizard/confirmation_step.py:843 +#: src/zebtrack/ui/wizard/confirmation_step.py:834 #, python-brace-format msgid "No write permission at the location: {location}" msgstr "Sem permissão de escrita na localização: {location}" -#: src/zebtrack/ui/wizard/confirmation_step.py:851 -#: src/zebtrack/ui/wizard/confirmation_step.py:863 +#: src/zebtrack/ui/wizard/confirmation_step.py:842 +#: src/zebtrack/ui/wizard/confirmation_step.py:854 msgid "The project name is too long for the file system." msgstr "Nome do projeto é muito longo para o sistema de arquivos." -#: src/zebtrack/ui/wizard/confirmation_step.py:858 +#: src/zebtrack/ui/wizard/confirmation_step.py:849 #, python-brace-format msgid "A file with that name already exists at: {location}" msgstr "Já existe um arquivo com esse nome em: {location}" -#: src/zebtrack/ui/wizard/confirmation_step.py:874 +#: src/zebtrack/ui/wizard/confirmation_step.py:865 #, python-brace-format msgid "A project with that name already exists at: {location}" msgstr "Já existe um projeto com esse nome em: {location}" -#: src/zebtrack/ui/wizard/confirmation_step.py:886 +#: src/zebtrack/ui/wizard/confirmation_step.py:877 msgid "No video selected. Go back and select videos." msgstr "Nenhum vídeo selecionado. Volte e selecione vídeos." -#: src/zebtrack/ui/wizard/confirmation_step.py:891 +#: src/zebtrack/ui/wizard/confirmation_step.py:882 msgid "Configure the camera in the previous step before creating the project." msgstr "Configure a câmera na etapa anterior antes de criar o projeto." +#: src/zebtrack/ui/wizard/detection_step.py:86 +msgid "Waiting for analysis..." +msgstr "Aguardando análise..." + +#: src/zebtrack/ui/wizard/detection_step.py:96 +msgid "Automatic Design Detection" +msgstr "Detecção Automática de Design" + +#: src/zebtrack/ui/wizard/detection_step.py:101 +msgid "Analyzing folder structure and parquet files..." +msgstr "Analisando estrutura de pastas e arquivos parquet..." + +#: src/zebtrack/ui/wizard/detection_step.py:125 +msgid "Detection Results" +msgstr "Resultados da Detecção" + +#: src/zebtrack/ui/wizard/detection_step.py:161 +msgid "Actions" +msgstr "Ações" + +#: src/zebtrack/ui/wizard/detection_step.py:166 +#: src/zebtrack/ui/wizard/detection_step.py:1211 +msgid "🔄 Re-analyze" +msgstr "🔄 Re-analisar" + +#: src/zebtrack/ui/wizard/detection_step.py:173 +msgid "✏️ Edit Design" +msgstr "✏️ Editar Design" + +#: src/zebtrack/ui/wizard/detection_step.py:182 +msgid "🔧 Custom Regex" +msgstr "🔧 Regex Customizado" + +#: src/zebtrack/ui/wizard/detection_step.py:188 +msgid "💡 Tip" +msgstr "💡 Dica" + +#: src/zebtrack/ui/wizard/detection_step.py:194 +msgid "" +"Automatic detection identifies groups, days and subjects from the folder " +"structure." +msgstr "" +"A detecção automática identifica grupos, dias e sujeitos baseando-se na " +"estrutura de pastas." + +#: src/zebtrack/ui/wizard/detection_step.py:215 +msgid "Analyzing (using custom regex)..." +msgstr "Analisando (usando regex personalizada)..." + +#: src/zebtrack/ui/wizard/detection_step.py:217 +msgid "Analyzing..." +msgstr "Analisando..." + +#: src/zebtrack/ui/wizard/detection_step.py:274 +msgid "Analysis complete!" +msgstr "Análise concluída!" + +#: src/zebtrack/ui/wizard/detection_step.py:286 +#, python-brace-format +msgid "Failed to complete detection: {error}" +msgstr "Falha ao concluir a detecção: {error}" + +#: src/zebtrack/ui/wizard/detection_step.py:332 +#, python-brace-format +msgid "" +"Experimental design detected!\n" +"\n" +"Groups found: {groups}\n" +"Days: {days}\n" +"\n" +"Review or customize the names before continuing." +msgstr "" +"Design experimental detectado!\n" +"\n" +"Grupos encontrados: {groups}\n" +"Dias: {days}\n" +"\n" +"Revise ou personalize os nomes antes de continuar." + +#: src/zebtrack/ui/wizard/detection_step.py:340 +msgid "Design Detected" +msgstr "Design Detectado" + +#: src/zebtrack/ui/wizard/detection_step.py:363 +msgid "Confirmation Required" +msgstr "Confirmação Necessária" + +#: src/zebtrack/ui/wizard/detection_step.py:364 +msgid "Confirm the group names before moving on." +msgstr "Confirme os nomes dos grupos antes de avançar." + +#: src/zebtrack/ui/wizard/detection_step.py:792 +#, python-brace-format +msgid "📊 Videos found: {count}" +msgstr "📊 Vídeos Encontrados: {count}" + +#: src/zebtrack/ui/wizard/detection_step.py:795 +msgid "📦 Existing Parquet Files:" +msgstr "📦 Arquivos Parquet Existentes:" + +#: src/zebtrack/ui/wizard/detection_step.py:802 +#, python-brace-format +msgid " • Complete (all 3): {count}" +msgstr " • Completos (todos 3): {count}" + +#: src/zebtrack/ui/wizard/detection_step.py:808 +msgid "🎯 Experimental Design Detected:" +msgstr "🎯 Design Experimental Detectado:" + +#: src/zebtrack/ui/wizard/detection_step.py:823 +#, python-brace-format +msgid " • Days: {days}" +msgstr " • Dias: {days}" + +#: src/zebtrack/ui/wizard/detection_step.py:828 +#, python-brace-format +msgid " • Pattern: {pattern}" +msgstr " • Padrão: {pattern}" + +#: src/zebtrack/ui/wizard/detection_step.py:840 +msgid " 📋 Subjects per Group:" +msgstr " 📋 Sujeitos por Grupo:" + +#: src/zebtrack/ui/wizard/detection_step.py:846 +#, python-brace-format +msgid " - {label}: 1 subject" +msgstr " - {label}: 1 sujeito" + +#: src/zebtrack/ui/wizard/detection_step.py:848 +#, python-brace-format +msgid " - {label}: {count} subjects" +msgstr " - {label}: {count} sujeitos" + +#: src/zebtrack/ui/wizard/detection_step.py:856 +msgid "" +"⚠️ Experimental design was not detected automatically.\n" +"\n" +"Possible causes:\n" +" • The folder structure does not follow a recognized pattern\n" +" • Group/day names are not detectable (e.g. Grupo1, Day01)\n" +"\n" +"You can continue without a detected design, or reorganize the files.\n" +msgstr "" +"⚠️ Design experimental não detectado automaticamente.\n" +"\n" +"Possí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" +"\n" +"Você pode prosseguir sem design detectado ou reorganizar os arquivos.\n" + +#: src/zebtrack/ui/wizard/detection_step.py:863 +msgid "ℹ️ Design detection disabled (exploratory project)." +msgstr "ℹ️ Detecção de design desativada (projeto exploratório)." + +#: src/zebtrack/ui/wizard/detection_step.py:871 +msgid "🧩 Custom regex in use:" +msgstr "🧩 Regex personalizada em uso:" + +#: src/zebtrack/ui/wizard/detection_step.py:898 +msgid "⚙️ Current Detector Configuration:" +msgstr "⚙️ Configuração Atual do Detector:" + +#: src/zebtrack/ui/wizard/detection_step.py:951 +msgid "Error!" +msgstr "Erro!" + +#: src/zebtrack/ui/wizard/detection_step.py:954 +#, python-brace-format +msgid "❌ Error: {message}" +msgstr "❌ Erro: {message}" + +#: src/zebtrack/ui/wizard/detection_step.py:1005 +msgid "Custom regex applied ✓" +msgstr "Regex personalizado aplicado ✓" + +#: src/zebtrack/ui/wizard/detection_step.py:1009 +msgid "" +"The custom regex found no design; adjust the patterns or edit manually." +msgstr "" +"Regex personalizado não encontrou design; ajuste os padrões ou edite " +"manualmente." + +#: src/zebtrack/ui/wizard/detection_step.py:1015 +msgid "Custom regex removed. Default detection reapplied ✓" +msgstr "Regex personalizado removido. Detecção padrão reaplicada ✓" + +#: src/zebtrack/ui/wizard/detection_step.py:1018 +msgid "Default detection reapplied, but no design was found." +msgstr "Detecção padrão reaplicada, mas nenhum design foi encontrado." + +#: src/zebtrack/ui/wizard/detection_step.py:1136 +msgid "Design edited manually ✓ (custom regex applied)" +msgstr "Design editado manualmente ✓ (regex personalizado aplicado)" + +#: src/zebtrack/ui/wizard/detection_step.py:1138 +msgid "Design edited manually ✓ (default regex)" +msgstr "Design editado manualmente ✓ (regex padrão)" + +#: src/zebtrack/ui/wizard/detection_step.py:1150 +msgid "No video was found. Go back and select valid videos." +msgstr "Nenhum vídeo foi encontrado. Volte e selecione vídeos válidos." + +#: src/zebtrack/ui/wizard/detection_step.py:1162 +msgid "Confirm the group names in the editor before moving on." +msgstr "Confirme os nomes dos grupos no editor antes de avançar." + +#: src/zebtrack/ui/wizard/detection_step.py:1210 +#, python-brace-format +msgid "Previous results (use '{button}' to refresh)" +msgstr "Resultados anteriores (use '{button}' para atualizar)" + # | msgid "Invalid Configuration" #: src/zebtrack/ui/wizard/experimental_design_step.py:154 msgid "Experimental Design Configuration" @@ -9712,77 +9946,77 @@ msgid "Hardware Detection" msgstr "Detecção de Hardware" #: src/zebtrack/ui/wizard/live_config_step.py:695 -#: src/zebtrack/ui/wizard/live_config_step.py:742 +#: src/zebtrack/ui/wizard/live_config_step.py:740 msgid "Detecting..." msgstr "Detectando..." -#: src/zebtrack/ui/wizard/live_config_step.py:709 +#: src/zebtrack/ui/wizard/live_config_step.py:708 #, python-brace-format msgid "Camera {index}" msgstr "Câmera {index}" -#: src/zebtrack/ui/wizard/live_config_step.py:720 +#: src/zebtrack/ui/wizard/live_config_step.py:718 msgid "✓ 1 camera detected" msgstr "✓ 1 câmera detectada" -#: src/zebtrack/ui/wizard/live_config_step.py:722 +#: src/zebtrack/ui/wizard/live_config_step.py:720 #, python-brace-format msgid "✓ {count} cameras detected" msgstr "✓ {count} câmeras detectadas" -#: src/zebtrack/ui/wizard/live_config_step.py:735 +#: src/zebtrack/ui/wizard/live_config_step.py:733 msgid "✗ No camera detected" msgstr "✗ Nenhuma câmera detectada" -#: src/zebtrack/ui/wizard/live_config_step.py:762 +#: src/zebtrack/ui/wizard/live_config_step.py:760 msgid "✓ 1 port detected" msgstr "✓ 1 porta detectada" -#: src/zebtrack/ui/wizard/live_config_step.py:764 +#: src/zebtrack/ui/wizard/live_config_step.py:762 #, python-brace-format msgid "✓ {count} ports detected" msgstr "✓ {count} portas detectadas" -#: src/zebtrack/ui/wizard/live_config_step.py:780 +#: src/zebtrack/ui/wizard/live_config_step.py:778 #, python-brace-format msgid "✓ 1 port — using {device}" msgstr "✓ 1 porta — usando {device}" -#: src/zebtrack/ui/wizard/live_config_step.py:782 +#: src/zebtrack/ui/wizard/live_config_step.py:780 #, python-brace-format msgid "✓ {count} ports — using {device}" msgstr "✓ {count} portas — usando {device}" -#: src/zebtrack/ui/wizard/live_config_step.py:799 +#: src/zebtrack/ui/wizard/live_config_step.py:797 msgid "✗ No port detected" msgstr "✗ Nenhuma porta detectada" -#: src/zebtrack/ui/wizard/live_config_step.py:804 -#: src/zebtrack/ui/wizard/live_config_step.py:860 +#: src/zebtrack/ui/wizard/live_config_step.py:802 +#: src/zebtrack/ui/wizard/live_config_step.py:858 msgid "✗ pyserial not installed" msgstr "✗ pyserial não instalado" -#: src/zebtrack/ui/wizard/live_config_step.py:814 +#: src/zebtrack/ui/wizard/live_config_step.py:812 msgid "No Port Selected" msgstr "Nenhuma Porta Selecionada" -#: src/zebtrack/ui/wizard/live_config_step.py:815 +#: src/zebtrack/ui/wizard/live_config_step.py:813 msgid "Please detect and select an Arduino port first." msgstr "Por favor, detecte e selecione uma porta Arduino primeiro." -#: src/zebtrack/ui/wizard/live_config_step.py:825 +#: src/zebtrack/ui/wizard/live_config_step.py:823 msgid "Testing..." msgstr "Testando..." -#: src/zebtrack/ui/wizard/live_config_step.py:833 +#: src/zebtrack/ui/wizard/live_config_step.py:831 msgid "✓ Connection OK" msgstr "✓ Conexão OK" -#: src/zebtrack/ui/wizard/live_config_step.py:835 +#: src/zebtrack/ui/wizard/live_config_step.py:833 msgid "Connection Test" msgstr "Teste de Conexão" -#: src/zebtrack/ui/wizard/live_config_step.py:837 +#: src/zebtrack/ui/wizard/live_config_step.py:835 #, python-brace-format msgid "" "Connection to {port} established successfully!\n" @@ -9793,15 +10027,15 @@ msgstr "" "\n" "A porta está acessível e pronta para uso." -#: src/zebtrack/ui/wizard/live_config_step.py:845 +#: src/zebtrack/ui/wizard/live_config_step.py:843 msgid "✗ Connection failed" msgstr "✗ Falha na conexão" -#: src/zebtrack/ui/wizard/live_config_step.py:847 +#: src/zebtrack/ui/wizard/live_config_step.py:845 msgid "Connection Error" msgstr "Erro de Conexão" -#: src/zebtrack/ui/wizard/live_config_step.py:849 +#: src/zebtrack/ui/wizard/live_config_step.py:847 #, python-brace-format msgid "" "Could not connect to port {port}.\n" @@ -9822,15 +10056,15 @@ msgstr "" "• A porta não está em uso por outro programa\n" "• Você tem permissão para acessar a porta" -#: src/zebtrack/ui/wizard/live_config_step.py:868 +#: src/zebtrack/ui/wizard/live_config_step.py:866 msgid "✗ Unexpected error" msgstr "✗ Erro inesperado" -#: src/zebtrack/ui/wizard/live_config_step.py:870 +#: src/zebtrack/ui/wizard/live_config_step.py:868 msgid "Unexpected Error" msgstr "Erro Inesperado" -#: src/zebtrack/ui/wizard/live_config_step.py:871 +#: src/zebtrack/ui/wizard/live_config_step.py:869 #, python-brace-format msgid "" "An error occurred while testing the connection:\n" @@ -9841,11 +10075,11 @@ msgstr "" "\n" "{error}" -#: src/zebtrack/ui/wizard/live_config_step.py:905 +#: src/zebtrack/ui/wizard/live_config_step.py:903 msgid "Aquarium Limit" msgstr "Limitação de Aquários" -#: src/zebtrack/ui/wizard/live_config_step.py:907 +#: src/zebtrack/ui/wizard/live_config_step.py:905 #, python-brace-format msgid "" "⚠️ Simultaneous recording is limited to 2 aquariums.\n" @@ -9866,7 +10100,7 @@ msgstr "" "• Use modo sequencial (processar aquários separadamente)\n" "• Processe offline após gravação sem detecção" -#: src/zebtrack/ui/wizard/live_config_step.py:918 +#: src/zebtrack/ui/wizard/live_config_step.py:916 #, python-brace-format msgid "" "A project with {count} aquariums exceeds the limit of 2 for simultaneous " @@ -9874,7 +10108,7 @@ msgid "" msgstr "" "Projeto com {count} aquários excede o limite de 2 para gravação simultânea." -#: src/zebtrack/ui/wizard/live_config_step.py:928 +#: src/zebtrack/ui/wizard/live_config_step.py:926 msgid "" "Mode selection cancelled. Adjust the number of aquariums or select a " "compatible mode." @@ -9882,12 +10116,12 @@ msgstr "" "Seleção de modo cancelada. Por favor, ajuste o número de aquários ou " "selecione um modo compatível." -#: src/zebtrack/ui/wizard/live_config_step.py:945 +#: src/zebtrack/ui/wizard/live_config_step.py:943 #, python-brace-format msgid "Error validating data: {error}" msgstr "Erro ao validar dados: {error}" -#: src/zebtrack/ui/wizard/live_config_step.py:1050 +#: src/zebtrack/ui/wizard/live_config_step.py:1048 #, python-brace-format msgid "" "⚠ The template's camera is unavailable — selected '{fallback}'. Please " @@ -9895,7 +10129,7 @@ msgid "" msgstr "" "⚠ Câmera do template indisponível — selecionada '{fallback}'. Confira." -#: src/zebtrack/ui/wizard/live_config_step.py:1086 +#: src/zebtrack/ui/wizard/live_config_step.py:1083 #, python-brace-format msgid "" "⚠ The template's port ({port}) is unavailable — select another one or " @@ -9904,6 +10138,384 @@ msgstr "" "⚠ Porta do template ({port}) indisponível — selecione outra ou desative o " "Arduino." +#: src/zebtrack/ui/wizard/model_selection_step.py:54 +msgid " ⭐ Recommended" +msgstr " ⭐ Recomendado" + +#: src/zebtrack/ui/wizard/model_selection_step.py:349 +msgid "Models and Weights" +msgstr "Modelos e Pesos" + +#: src/zebtrack/ui/wizard/model_selection_step.py:355 +msgid "" +"Adjust how ZebTrack will use each detection model.\n" +"If you prefer, keep the recommended defaults and move on." +msgstr "" +"Ajuste como o ZebTrack utilizará cada modelo de detecção.\n" +"Se preferir, mantenha os padrões recomendados e avance." + +#: src/zebtrack/ui/wizard/model_selection_step.py:399 +msgid "Methods and Weights per Role" +msgstr "Métodos e Pesos por Função" + +#: src/zebtrack/ui/wizard/model_selection_step.py:409 +msgid "Aquarium (arena detection)" +msgstr "Aquário (detecção de arena)" + +#: src/zebtrack/ui/wizard/model_selection_step.py:418 +msgid "Animals (tracking)" +msgstr "Animais (rastreamento)" + +#: src/zebtrack/ui/wizard/model_selection_step.py:436 +msgid "Acceleration / OpenVINO" +msgstr "Aceleração / OpenVINO" + +#: src/zebtrack/ui/wizard/model_selection_step.py:444 +msgid "Use OpenVINO (requires converting the weight)" +msgstr "Usar OpenVINO (requer conversão do peso)" + +#: src/zebtrack/ui/wizard/model_selection_step.py:451 +msgid "" +"Enable this once the matching OpenVINO model has been converted. It allows " +"faster inference on compatible CPUs." +msgstr "" +"Ative quando o modelo OpenVINO correspondente já foi convertido. Permite " +"inferência mais rápida em CPUs compatíveis." + +#: src/zebtrack/ui/wizard/model_selection_step.py:471 +msgid "" +"AUTO lets OpenVINO choose the target automatically.\n" +"Pick CPU/GPU/NPU to force the target, when available." +msgstr "" +"AUTO usa seleção automática do OpenVINO.\n" +"Escolha CPU/GPU/NPU para forçar o alvo, quando disponível." + +#: src/zebtrack/ui/wizard/model_selection_step.py:478 +msgid "Detection Parameters (YOLO)" +msgstr "Parâmetros de Detecção (YOLO)" + +#: src/zebtrack/ui/wizard/model_selection_step.py:486 +msgid "Minimum confidence (0-1):" +msgstr "Confiança mínima (0-1):" + +#: src/zebtrack/ui/wizard/model_selection_step.py:490 +msgid "" +"🎯 Minimum Confidence (Confidence Threshold)\n" +"\n" +"Filters 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" +msgstr "" +"🎯 Confiança Mínima (Confidence Threshold)\n" +"\n" +"Filtra 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" + +#: src/zebtrack/ui/wizard/model_selection_step.py:504 +msgid "NMS (overlap, 0-1):" +msgstr "NMS (sobreposição, 0-1):" + +#: src/zebtrack/ui/wizard/model_selection_step.py:508 +msgid "" +"🔲 NMS - Non-Maximum Suppression\n" +"\n" +"Removes 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" +msgstr "" +"🔲 NMS - Non-Maximum Suppression\n" +"\n" +"Elimina 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" + +#: src/zebtrack/ui/wizard/model_selection_step.py:524 +msgid "Tracking Parameters (ByteTrack)" +msgstr "Parâmetros de Rastreamento (ByteTrack)" + +#: src/zebtrack/ui/wizard/model_selection_step.py:537 +msgid "Use ByteTrack (Recommended)" +msgstr "Usar ByteTrack (Recomendado)" + +#: src/zebtrack/ui/wizard/model_selection_step.py:545 +msgid "" +"Enables the ByteTrack algorithm for robust tracking with a Kalman filter.\n" +"Recommended for most experiments." +msgstr "" +"Ativa o algoritmo ByteTrack para rastreamento robusto com Filtro de Kalman.\n" +"Recomendado para a maioria dos experimentos." + +#: src/zebtrack/ui/wizard/model_selection_step.py:563 +msgid "Track Threshold (0-1):" +msgstr "Track Threshold (0-1):" + +#: src/zebtrack/ui/wizard/model_selection_step.py:568 +msgid "" +"🛤️ Track Threshold\n" +"\n" +"Minimum confidence to START or KEEP a trajectory.\n" +"Low values help keep the trail of animals that are hard to detect.\n" +"\n" +"💡 Default: 0.25" +msgstr "" +"🛤️ Track Threshold\n" +"\n" +"Confiança mínima para INICIAR ou MANTER uma trajetória.\n" +"Valores baixos ajudam a manter o rastro de animais difíceis de detectar.\n" +"\n" +"💡 Padrão: 0.25" + +#: src/zebtrack/ui/wizard/model_selection_step.py:577 +msgid "Match Threshold (0-1):" +msgstr "Match Threshold (0-1):" + +#: src/zebtrack/ui/wizard/model_selection_step.py:582 +msgid "" +"🔗 Match Threshold\n" +"\n" +"Tolerance when associating boxes.\n" +"HIGH values (close to 1.0) are more permissive for fast movement.\n" +"\n" +"💡 Default: 0.95 (for fast zebrafish)" +msgstr "" +"🔗 Match Threshold\n" +"\n" +"Tolerância para associação de caixas.\n" +"Valores ALTOS (perto de 1.0) são mais permissivos para movimento rápido.\n" +"\n" +"💡 Padrão: 0.95 (para zebrafish rápidos)" + +#: src/zebtrack/ui/wizard/model_selection_step.py:592 +msgid "Track Buffer (frames):" +msgstr "Track Buffer (frames):" + +#: src/zebtrack/ui/wizard/model_selection_step.py:597 +msgid "" +"🧠 Track Buffer\n" +"\n" +"The 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)" +msgstr "" +"🧠 Track Buffer\n" +"\n" +"Memó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)" + +#: src/zebtrack/ui/wizard/model_selection_step.py:606 +#: src/zebtrack/ui/wizard/model_selection_step.py:756 +msgid "Max distance (px):" +msgstr "Distância Máx (px):" + +#: src/zebtrack/ui/wizard/model_selection_step.py:611 +msgid "" +"📏 Maximum Centre Distance\n" +"\n" +"The 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" +msgstr "" +"📏 Distância Máxima de Centro\n" +"\n" +"Distâ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" + +#: src/zebtrack/ui/wizard/model_selection_step.py:621 +#: src/zebtrack/ui/wizard/model_selection_step.py:757 +msgid "IoU Threshold (0-1):" +msgstr "IoU Threshold (0-1):" + +#: src/zebtrack/ui/wizard/model_selection_step.py:626 +msgid "" +"🔳 IoU Threshold\n" +"\n" +"Minimum overlap to prefer a box match over a distance match.\n" +"For small, fast fish, low values work better.\n" +"\n" +"💡 Default: 0.1" +msgstr "" +"🔳 IoU Threshold\n" +"\n" +"Sobreposição mínima para preferir 'Match por Caixa' em vez de distância.\n" +"Para peixes pequenos e rápidos, valores baixos são melhores.\n" +"\n" +"💡 Padrão: 0.1" + +#: src/zebtrack/ui/wizard/model_selection_step.py:639 +msgid "" +"📊 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" +msgstr "" +"📊 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" + +#: src/zebtrack/ui/wizard/model_selection_step.py:659 +msgid "💡 Tip: adjust ONE parameter at a time (±0.05) and test!" +msgstr "💡 Dica: Ajuste UM parâmetro por vez (±0.05) e teste!" + +#: src/zebtrack/ui/wizard/model_selection_step.py:674 +#, python-brace-format +msgid "Current YOLO defaults: confidence {confidence}, NMS {nms}." +msgstr "Padrões atuais YOLO: confiança {confidence}, NMS {nms}." + +#: src/zebtrack/ui/wizard/model_selection_step.py:690 +msgid "🔄 Restore Recommended Defaults" +msgstr "🔄 Restaurar Padrões Recomendados" + +#: src/zebtrack/ui/wizard/model_selection_step.py:702 +msgid "" +"Restores every threshold to its recommended default value.\n" +"\n" +"Useful if you have made adjustments and want to start over." +msgstr "" +"Restaura todos os thresholds para os valores padrão recomendados.\n" +"\n" +"Útil se você fez ajustes e quer voltar ao ponto de partida." + +#: src/zebtrack/ui/wizard/model_selection_step.py:710 +msgid "" +"Tip: keep the defaults if you are still setting up the videos. You can " +"review these values later in the project settings." +msgstr "" +"Dica: mantenha os padrões se ainda estiver configurando os vídeos. Você pode" +" revisar esses valores depois nas configurações do projeto." + +#: src/zebtrack/ui/wizard/model_selection_step.py:752 +#, python-brace-format +msgid "" +"ℹ️ 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." +msgstr "" +"ℹ️ 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." + +#: src/zebtrack/ui/wizard/model_selection_step.py:768 +msgid "" +"💡 ByteTrack uses a Kalman filter to predict positions even when the fish " +"briefly disappears. Adjust the fields below for more stability." +msgstr "" +"💡 O ByteTrack usa Filtro de Kalman para prever posições mesmo quando o peixe" +" some brevemente. Ajuste os campos abaixo para maior estabilidade." + +#: src/zebtrack/ui/wizard/model_selection_step.py:795 +msgid "" +"Segmentation supports several animals per aquarium.\n" +"Detection is optimized for one animal per aquarium and uses ByteTrack." +msgstr "" +"Segmentação suporta múltiplos animais por aquário.\n" +"Detecção é otimizada para um animal por aquário e usa ByteTrack." + +#: src/zebtrack/ui/wizard/model_selection_step.py:810 +msgid "Select the weight file loaded for this role." +msgstr "Selecione o arquivo de peso carregado para esta função." + +#: src/zebtrack/ui/wizard/model_selection_step.py:881 +msgid "❌ Confidence must be between 0 and 1" +msgstr "❌ Confiança deve estar entre 0 e 1" + +#: src/zebtrack/ui/wizard/model_selection_step.py:882 +msgid "❌ NMS must be between 0 and 1" +msgstr "❌ NMS deve estar entre 0 e 1" + +#: src/zebtrack/ui/wizard/model_selection_step.py:883 +msgid "❌ Track must be between 0 and 1" +msgstr "❌ Track deve estar entre 0 e 1" + +#: src/zebtrack/ui/wizard/model_selection_step.py:884 +msgid "❌ Match must be between 0 and 1" +msgstr "❌ Associação deve estar entre 0 e 1" + +#: src/zebtrack/ui/wizard/model_selection_step.py:914 +msgid "❌ Value must be a decimal (e.g. 0.25)" +msgstr "❌ Valor deve ser decimal (ex: 0.25)" + +#: src/zebtrack/ui/wizard/model_selection_step.py:1028 +msgid "" +"⚠️ Detection (det) is recommended for only 1 animal per aquarium. Consider " +"segmentation (seg) for several animals." +msgstr "" +"⚠️ Detecção (det) é recomendada para apenas 1 animal por aquário. Considere " +"segmentação (seg) para múltiplos animais." + +#: src/zebtrack/ui/wizard/model_selection_step.py:1189 +msgid "Enter decimal values between 0 and 1 for the parameters." +msgstr "Informe valores decimais entre 0 e 1 para os parâmetros." + +#: src/zebtrack/ui/wizard/model_selection_step.py:1192 +msgid "The confidence parameter must be between 0 and 1." +msgstr "O parâmetro de confiança deve estar entre 0 e 1." + +#: src/zebtrack/ui/wizard/model_selection_step.py:1193 +msgid "The NMS parameter must be between 0 and 1." +msgstr "O parâmetro de NMS deve estar entre 0 e 1." + +#: src/zebtrack/ui/wizard/model_selection_step.py:1194 +msgid "The track parameter must be between 0 and 1." +msgstr "O parâmetro de track deve estar entre 0 e 1." + +#: src/zebtrack/ui/wizard/model_selection_step.py:1195 +msgid "The match parameter must be between 0 and 1." +msgstr "O parâmetro de associação deve estar entre 0 e 1." + +#: src/zebtrack/ui/wizard/model_selection_step.py:1203 +msgid "" +"Select a valid weight for the aquarium. The file must match the chosen " +"method." +msgstr "" +"Selecione um peso válido para aquário. O arquivo precisa corresponder ao " +"método escolhido." + +#: src/zebtrack/ui/wizard/model_selection_step.py:1211 +msgid "" +"Select a valid weight for the animals. The file must match the chosen " +"method." +msgstr "" +"Selecione um peso válido para animais. O arquivo precisa corresponder ao " +"método escolhido." + #: src/zebtrack/ui/wizard/templates.py:354 #, python-brace-format msgid "Template loaded: {details}" diff --git a/src/zebtrack/locales/zebtrack.pot b/src/zebtrack/locales/zebtrack.pot index 1f152c6b..4fe30a09 100644 --- a/src/zebtrack/locales/zebtrack.pot +++ b/src/zebtrack/locales/zebtrack.pot @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: DRerio LogAI VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-08-14 11:34-0300\n" +"POT-Creation-Date: 2026-08-14 15:49-0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -224,7 +224,7 @@ msgstr "" #: src/zebtrack/ui/components/validation_manager.py:491 #: src/zebtrack/ui/components/validation_manager.py:1634 #: src/zebtrack/ui/gui.py:144 src/zebtrack/ui/gui.py:808 -#: src/zebtrack/ui/wizard/live_config_step.py:862 +#: src/zebtrack/ui/wizard/live_config_step.py:860 msgid "Error" msgstr "" @@ -381,6 +381,7 @@ msgstr "" #: src/zebtrack/coordinators/_video_selection_mixin.py:224 #: src/zebtrack/ui/components/model_diagnostics_panel.py:56 +#: src/zebtrack/ui/wizard/detection_step.py:223 msgid "No video selected." msgstr "" @@ -730,19 +731,19 @@ msgstr "" #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1095 #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1795 -#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2316 +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2314 msgid "Countdown to start the live analysis." msgstr "" #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1097 #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1797 -#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2318 +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2316 msgid "Starting the live analysis." msgstr "" #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1190 #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1866 -#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2387 +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2385 msgid "Live analysis in progress." msgstr "" @@ -789,17 +790,17 @@ msgid "External Trigger Unavailable" msgstr "" #: src/zebtrack/coordinators/live_camera_session_coordinator.py:2024 -#: src/zebtrack/coordinators/recording_session_coordinator.py:551 +#: src/zebtrack/coordinators/recording_session_coordinator.py:550 #, python-brace-format msgid "Waiting for external signal... (port {port})" msgstr "" -#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2170 +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2168 #: src/zebtrack/ui/components/project_initializer.py:347 msgid "Camera not found" msgstr "" -#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2172 +#: src/zebtrack/coordinators/live_camera_session_coordinator.py:2170 #, python-brace-format msgid "" "The camera saved in the project ('{name}') was not detected.\n" @@ -909,7 +910,7 @@ msgid "" msgstr "" #: src/zebtrack/coordinators/model_diagnostics_coordinator.py:742 -#: src/zebtrack/ui/wizard/confirmation_step.py:802 +#: src/zebtrack/ui/wizard/confirmation_step.py:794 msgid "Error Saving" msgstr "" @@ -1656,7 +1657,7 @@ msgid "📊 Video status:" msgstr "" #: src/zebtrack/core/project/project_workflow_service.py:1331 -#: src/zebtrack/ui/wizard/confirmation_step.py:558 +#: src/zebtrack/ui/wizard/confirmation_step.py:554 #, python-brace-format msgid " • Total videos: {count}" msgstr "" @@ -3283,7 +3284,7 @@ msgid "No project is currently loaded." msgstr "" #: src/zebtrack/ui/components/arduino_dashboard.py:310 -#: src/zebtrack/ui/wizard/live_config_step.py:863 +#: src/zebtrack/ui/wizard/live_config_step.py:861 msgid "" "The pyserial library is not installed.\n" "\n" @@ -3717,7 +3718,8 @@ msgstr "" #: src/zebtrack/ui/components/project_model_configuration_panel.py:189 #: src/zebtrack/ui/components/project_model_configuration_panel.py:378 #: src/zebtrack/ui/components/weight_hardware_manager.py:145 -#: src/zebtrack/ui/wizard/confirmation_step.py:529 +#: src/zebtrack/ui/wizard/confirmation_step.py:525 +#: src/zebtrack/ui/wizard/detection_step.py:928 msgid "Enabled" msgstr "" @@ -3726,7 +3728,8 @@ msgstr "" #: src/zebtrack/ui/components/project_model_configuration_panel.py:191 #: src/zebtrack/ui/components/project_model_configuration_panel.py:378 #: src/zebtrack/ui/components/weight_hardware_manager.py:145 -#: src/zebtrack/ui/wizard/confirmation_step.py:529 +#: src/zebtrack/ui/wizard/confirmation_step.py:525 +#: src/zebtrack/ui/wizard/detection_step.py:928 msgid "Disabled" msgstr "" @@ -4301,6 +4304,7 @@ msgid "Optimise with OpenVINO (for Intel hardware)" msgstr "" #: src/zebtrack/ui/components/global_model_configuration_panel.py:281 +#: src/zebtrack/ui/wizard/model_selection_step.py:458 msgid "OpenVINO device:" msgstr "" @@ -5413,6 +5417,7 @@ msgstr "" #: src/zebtrack/ui/components/processing_reports.py:186 #: src/zebtrack/ui/components/project_overview.py:137 +#: src/zebtrack/ui/wizard/detection_step.py:149 msgid "Status" msgstr "" @@ -7218,6 +7223,7 @@ msgid " • Total: {sessions} recordings ({animals} animals)" msgstr "" #: src/zebtrack/ui/wizard/confirmation_step.py:366 +#: src/zebtrack/ui/wizard/detection_step.py:819 #, python-brace-format msgid " • Groups: {groups}" msgstr "" @@ -7268,288 +7274,304 @@ msgstr "" msgid " • Analysis: every {count} frames" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:417 +#: src/zebtrack/ui/wizard/confirmation_step.py:416 #, python-brace-format msgid " • Display: every {count} frames" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:425 +#: src/zebtrack/ui/wizard/confirmation_step.py:423 msgid "🎯 Detection Configuration:" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:431 -#: src/zebtrack/ui/wizard/confirmation_step.py:524 +#: src/zebtrack/ui/wizard/confirmation_step.py:428 +#: src/zebtrack/ui/wizard/confirmation_step.py:520 +#: src/zebtrack/ui/wizard/detection_step.py:923 #, python-brace-format msgid " • Aquarium weight: {weight}" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:434 -#: src/zebtrack/ui/wizard/confirmation_step.py:526 +#: src/zebtrack/ui/wizard/confirmation_step.py:430 +#: src/zebtrack/ui/wizard/confirmation_step.py:522 +#: src/zebtrack/ui/wizard/detection_step.py:925 #, python-brace-format msgid " • Animal weight: {weight}" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:452 +#: src/zebtrack/ui/wizard/confirmation_step.py:448 msgid "🔍 Detected Design:" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:461 +#: src/zebtrack/ui/wizard/confirmation_step.py:457 #, python-brace-format msgid " • Groups: {count} ({preview}{suffix})" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:467 +#: src/zebtrack/ui/wizard/confirmation_step.py:463 #, python-brace-format msgid " • Days: {count}" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:469 +#: src/zebtrack/ui/wizard/confirmation_step.py:465 +#: src/zebtrack/ui/wizard/detection_step.py:832 #, python-brace-format msgid " • Confidence: {value}" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:477 +#: src/zebtrack/ui/wizard/confirmation_step.py:473 +#: src/zebtrack/ui/wizard/detection_step.py:873 msgid "Groups" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:478 +#: src/zebtrack/ui/wizard/confirmation_step.py:474 +#: src/zebtrack/ui/wizard/detection_step.py:874 msgid "Days" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:479 +#: src/zebtrack/ui/wizard/confirmation_step.py:475 +#: src/zebtrack/ui/wizard/detection_step.py:875 msgid "Subjects" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:483 +#: src/zebtrack/ui/wizard/confirmation_step.py:479 msgid "🧩 Custom Regex:" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:503 +#: src/zebtrack/ui/wizard/confirmation_step.py:499 +#: src/zebtrack/ui/wizard/detection_step.py:42 +#: src/zebtrack/ui/wizard/model_selection_step.py:40 msgid "Segmentation (seg)" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:504 +#: src/zebtrack/ui/wizard/confirmation_step.py:500 +#: src/zebtrack/ui/wizard/detection_step.py:43 +#: src/zebtrack/ui/wizard/model_selection_step.py:41 msgid "Detection (det)" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:508 +#: src/zebtrack/ui/wizard/confirmation_step.py:504 msgid "🎯 Detection Settings:" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:515 +#: src/zebtrack/ui/wizard/confirmation_step.py:511 +#: src/zebtrack/ui/wizard/detection_step.py:915 #, python-brace-format msgid " • Aquarium method: {method}" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:518 +#: src/zebtrack/ui/wizard/confirmation_step.py:514 +#: src/zebtrack/ui/wizard/detection_step.py:917 #, python-brace-format msgid " • Animal method: {method}" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:530 +#: src/zebtrack/ui/wizard/confirmation_step.py:526 +#: src/zebtrack/ui/wizard/detection_step.py:929 #, python-brace-format msgid " • OpenVINO: {status}" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:563 +#: src/zebtrack/ui/wizard/confirmation_step.py:559 msgid "🌳 Folder Structure (preview):" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:570 +#: src/zebtrack/ui/wizard/confirmation_step.py:566 msgid " • (+ 1 additional selection)" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:572 +#: src/zebtrack/ui/wizard/confirmation_step.py:568 #, python-brace-format msgid " • (+ {count} additional selections)" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:577 +#: src/zebtrack/ui/wizard/confirmation_step.py:573 msgid "📏 Physical Calibration:" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:583 +#: src/zebtrack/ui/wizard/confirmation_step.py:579 #, python-brace-format msgid " • Aquariums: {count}" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:585 +#: src/zebtrack/ui/wizard/confirmation_step.py:580 #, python-brace-format msgid " • Animals per aquarium: {count}" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:588 +#: src/zebtrack/ui/wizard/confirmation_step.py:581 #, python-brace-format msgid " • Dimensions: {width} x {height} cm" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:593 +#: src/zebtrack/ui/wizard/confirmation_step.py:585 msgid "⚙️ Processing Plan:" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:605 +#: src/zebtrack/ui/wizard/confirmation_step.py:597 msgid "Skip (complete data)" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:606 +#: src/zebtrack/ui/wizard/confirmation_step.py:598 msgid "Import Zones + track" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:607 +#: src/zebtrack/ui/wizard/confirmation_step.py:599 msgid "Partial (arena only)" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:608 +#: src/zebtrack/ui/wizard/confirmation_step.py:600 msgid "Full (process from scratch)" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:614 +#: src/zebtrack/ui/wizard/confirmation_step.py:606 #, python-brace-format msgid " • 1 video: {name}" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:616 +#: src/zebtrack/ui/wizard/confirmation_step.py:608 #, python-brace-format msgid " • {count} videos: {name}" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:628 +#: src/zebtrack/ui/wizard/confirmation_step.py:620 #, python-brace-format msgid "⏱️ Estimated time: ~{minutes} minutes" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:631 +#: src/zebtrack/ui/wizard/confirmation_step.py:623 msgid " (1 video to process)" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:633 +#: src/zebtrack/ui/wizard/confirmation_step.py:625 #, python-brace-format msgid " ({count} videos to process)" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:645 +#: src/zebtrack/ui/wizard/confirmation_step.py:637 msgid "📦 Existing Parquets:" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:647 +#: src/zebtrack/ui/wizard/confirmation_step.py:639 #, python-brace-format msgid " • Scope: {scope}" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:652 +#: src/zebtrack/ui/wizard/confirmation_step.py:644 +#: src/zebtrack/ui/wizard/detection_step.py:796 #, python-brace-format msgid " • Arena: {count}" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:653 +#: src/zebtrack/ui/wizard/confirmation_step.py:645 +#: src/zebtrack/ui/wizard/detection_step.py:797 #, python-brace-format msgid " • ROIs: {count}" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:654 +#: src/zebtrack/ui/wizard/confirmation_step.py:646 +#: src/zebtrack/ui/wizard/detection_step.py:799 #, python-brace-format msgid " • Trajectory: {count}" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:655 +#: src/zebtrack/ui/wizard/confirmation_step.py:647 #, python-brace-format msgid " • Complete: {count}" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:668 +#: src/zebtrack/ui/wizard/confirmation_step.py:660 msgid "📥 Import Configuration:" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:672 +#: src/zebtrack/ui/wizard/confirmation_step.py:664 msgid " ✅ Arena: 1 video" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:674 +#: src/zebtrack/ui/wizard/confirmation_step.py:666 #, python-brace-format msgid " ✅ Arena: {count} videos" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:679 +#: src/zebtrack/ui/wizard/confirmation_step.py:671 msgid " ✅ ROIs: 1 video" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:681 +#: src/zebtrack/ui/wizard/confirmation_step.py:673 #, python-brace-format msgid " ✅ ROIs: {count} videos" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:686 +#: src/zebtrack/ui/wizard/confirmation_step.py:678 msgid " ✅ Trajectory: 1 video" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:688 +#: src/zebtrack/ui/wizard/confirmation_step.py:680 #, python-brace-format msgid " ✅ Trajectory: {count} videos" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:703 +#: src/zebtrack/ui/wizard/confirmation_step.py:695 msgid "Replace existing ROIs" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:704 +#: src/zebtrack/ui/wizard/confirmation_step.py:696 msgid "Merge (keep both)" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:705 +#: src/zebtrack/ui/wizard/confirmation_step.py:697 msgid "Manual conflict resolution" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:708 +#: src/zebtrack/ui/wizard/confirmation_step.py:700 msgid "🔀 ROI Strategy:" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:713 +#: src/zebtrack/ui/wizard/confirmation_step.py:705 msgid "(selection)" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:721 +#: src/zebtrack/ui/wizard/confirmation_step.py:713 msgid "1 folder" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:721 +#: src/zebtrack/ui/wizard/confirmation_step.py:713 #, python-brace-format msgid "{count} folders" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:725 +#: src/zebtrack/ui/wizard/confirmation_step.py:717 msgid "1 file" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:725 +#: src/zebtrack/ui/wizard/confirmation_step.py:717 #, python-brace-format msgid "{count} files" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:728 +#: src/zebtrack/ui/wizard/confirmation_step.py:720 msgid "empty" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:749 +#: src/zebtrack/ui/wizard/confirmation_step.py:741 msgid " … Preview truncated (full details in step 2)" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:757 +#: src/zebtrack/ui/wizard/confirmation_step.py:749 msgid "Save Template" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:758 +#: src/zebtrack/ui/wizard/confirmation_step.py:750 msgid "Enter a name for the template:" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:770 +#: src/zebtrack/ui/wizard/confirmation_step.py:762 msgid "Save Wizard Template" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:772 +#: src/zebtrack/ui/wizard/confirmation_step.py:764 msgid "Wizard Templates" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:789 +#: src/zebtrack/ui/wizard/confirmation_step.py:781 #, python-brace-format msgid "" "Template '{name}' saved successfully!\n" @@ -7560,11 +7582,11 @@ msgid "" "quickly." msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:795 +#: src/zebtrack/ui/wizard/confirmation_step.py:787 msgid "Template Saved" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:804 +#: src/zebtrack/ui/wizard/confirmation_step.py:796 #, python-brace-format msgid "" "Could not save the template '{name}'.\n" @@ -7572,53 +7594,247 @@ msgid "" "Check the logs for more details." msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:821 +#: src/zebtrack/ui/wizard/confirmation_step.py:812 msgid "Please enter a name for the project." msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:826 +#: src/zebtrack/ui/wizard/confirmation_step.py:817 msgid "" "The project name contains invalid characters. Use only letters, digits, " "spaces, '_' and '-'." msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:835 +#: src/zebtrack/ui/wizard/confirmation_step.py:826 msgid "Please select a location for the project." msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:838 +#: src/zebtrack/ui/wizard/confirmation_step.py:829 #, python-brace-format msgid "Location does not exist: {location}" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:843 +#: src/zebtrack/ui/wizard/confirmation_step.py:834 #, python-brace-format msgid "No write permission at the location: {location}" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:851 -#: src/zebtrack/ui/wizard/confirmation_step.py:863 +#: src/zebtrack/ui/wizard/confirmation_step.py:842 +#: src/zebtrack/ui/wizard/confirmation_step.py:854 msgid "The project name is too long for the file system." msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:858 +#: src/zebtrack/ui/wizard/confirmation_step.py:849 #, python-brace-format msgid "A file with that name already exists at: {location}" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:874 +#: src/zebtrack/ui/wizard/confirmation_step.py:865 #, python-brace-format msgid "A project with that name already exists at: {location}" msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:886 +#: src/zebtrack/ui/wizard/confirmation_step.py:877 msgid "No video selected. Go back and select videos." msgstr "" -#: src/zebtrack/ui/wizard/confirmation_step.py:891 +#: src/zebtrack/ui/wizard/confirmation_step.py:882 msgid "Configure the camera in the previous step before creating the project." msgstr "" +#: src/zebtrack/ui/wizard/detection_step.py:86 +msgid "Waiting for analysis..." +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:96 +msgid "Automatic Design Detection" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:101 +msgid "Analyzing folder structure and parquet files..." +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:125 +msgid "Detection Results" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:161 +msgid "Actions" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:166 +#: src/zebtrack/ui/wizard/detection_step.py:1211 +msgid "🔄 Re-analyze" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:173 +msgid "✏️ Edit Design" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:182 +msgid "🔧 Custom Regex" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:188 +msgid "💡 Tip" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:194 +msgid "" +"Automatic detection identifies groups, days and subjects from the folder " +"structure." +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:215 +msgid "Analyzing (using custom regex)..." +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:217 +msgid "Analyzing..." +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:274 +msgid "Analysis complete!" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:286 +#, python-brace-format +msgid "Failed to complete detection: {error}" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:332 +#, python-brace-format +msgid "" +"Experimental design detected!\n" +"\n" +"Groups found: {groups}\n" +"Days: {days}\n" +"\n" +"Review or customize the names before continuing." +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:340 +msgid "Design Detected" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:363 +msgid "Confirmation Required" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:364 +msgid "Confirm the group names before moving on." +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:792 +#, python-brace-format +msgid "📊 Videos found: {count}" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:795 +msgid "📦 Existing Parquet Files:" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:802 +#, python-brace-format +msgid " • Complete (all 3): {count}" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:808 +msgid "🎯 Experimental Design Detected:" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:823 +#, python-brace-format +msgid " • Days: {days}" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:828 +#, python-brace-format +msgid " • Pattern: {pattern}" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:840 +msgid " 📋 Subjects per Group:" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:846 +#, python-brace-format +msgid " - {label}: 1 subject" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:848 +#, python-brace-format +msgid " - {label}: {count} subjects" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:856 +msgid "" +"⚠️ Experimental design was not detected automatically.\n" +"\n" +"Possible causes:\n" +" • The folder structure does not follow a recognized pattern\n" +" • Group/day names are not detectable (e.g. Grupo1, Day01)\n" +"\n" +"You can continue without a detected design, or reorganize the files.\n" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:863 +msgid "ℹ️ Design detection disabled (exploratory project)." +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:871 +msgid "🧩 Custom regex in use:" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:898 +msgid "⚙️ Current Detector Configuration:" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:951 +msgid "Error!" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:954 +#, python-brace-format +msgid "❌ Error: {message}" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:1005 +msgid "Custom regex applied ✓" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:1009 +msgid "The custom regex found no design; adjust the patterns or edit manually." +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:1015 +msgid "Custom regex removed. Default detection reapplied ✓" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:1018 +msgid "Default detection reapplied, but no design was found." +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:1136 +msgid "Design edited manually ✓ (custom regex applied)" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:1138 +msgid "Design edited manually ✓ (default regex)" +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:1150 +msgid "No video was found. Go back and select valid videos." +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:1162 +msgid "Confirm the group names in the editor before moving on." +msgstr "" + +#: src/zebtrack/ui/wizard/detection_step.py:1210 +#, python-brace-format +msgid "Previous results (use '{button}' to refresh)" +msgstr "" + #: src/zebtrack/ui/wizard/experimental_design_step.py:154 msgid "Experimental Design Configuration" msgstr "" @@ -7891,77 +8107,77 @@ msgid "Hardware Detection" msgstr "" #: src/zebtrack/ui/wizard/live_config_step.py:695 -#: src/zebtrack/ui/wizard/live_config_step.py:742 +#: src/zebtrack/ui/wizard/live_config_step.py:740 msgid "Detecting..." msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:709 +#: src/zebtrack/ui/wizard/live_config_step.py:708 #, python-brace-format msgid "Camera {index}" msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:720 +#: src/zebtrack/ui/wizard/live_config_step.py:718 msgid "✓ 1 camera detected" msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:722 +#: src/zebtrack/ui/wizard/live_config_step.py:720 #, python-brace-format msgid "✓ {count} cameras detected" msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:735 +#: src/zebtrack/ui/wizard/live_config_step.py:733 msgid "✗ No camera detected" msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:762 +#: src/zebtrack/ui/wizard/live_config_step.py:760 msgid "✓ 1 port detected" msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:764 +#: src/zebtrack/ui/wizard/live_config_step.py:762 #, python-brace-format msgid "✓ {count} ports detected" msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:780 +#: src/zebtrack/ui/wizard/live_config_step.py:778 #, python-brace-format msgid "✓ 1 port — using {device}" msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:782 +#: src/zebtrack/ui/wizard/live_config_step.py:780 #, python-brace-format msgid "✓ {count} ports — using {device}" msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:799 +#: src/zebtrack/ui/wizard/live_config_step.py:797 msgid "✗ No port detected" msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:804 -#: src/zebtrack/ui/wizard/live_config_step.py:860 +#: src/zebtrack/ui/wizard/live_config_step.py:802 +#: src/zebtrack/ui/wizard/live_config_step.py:858 msgid "✗ pyserial not installed" msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:814 +#: src/zebtrack/ui/wizard/live_config_step.py:812 msgid "No Port Selected" msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:815 +#: src/zebtrack/ui/wizard/live_config_step.py:813 msgid "Please detect and select an Arduino port first." msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:825 +#: src/zebtrack/ui/wizard/live_config_step.py:823 msgid "Testing..." msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:833 +#: src/zebtrack/ui/wizard/live_config_step.py:831 msgid "✓ Connection OK" msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:835 +#: src/zebtrack/ui/wizard/live_config_step.py:833 msgid "Connection Test" msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:837 +#: src/zebtrack/ui/wizard/live_config_step.py:835 #, python-brace-format msgid "" "Connection to {port} established successfully!\n" @@ -7969,15 +8185,15 @@ msgid "" "The port is reachable and ready to use." msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:845 +#: src/zebtrack/ui/wizard/live_config_step.py:843 msgid "✗ Connection failed" msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:847 +#: src/zebtrack/ui/wizard/live_config_step.py:845 msgid "Connection Error" msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:849 +#: src/zebtrack/ui/wizard/live_config_step.py:847 #, python-brace-format msgid "" "Could not connect to port {port}.\n" @@ -7990,15 +8206,15 @@ msgid "" "• You have permission to access the port" msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:868 +#: src/zebtrack/ui/wizard/live_config_step.py:866 msgid "✗ Unexpected error" msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:870 +#: src/zebtrack/ui/wizard/live_config_step.py:868 msgid "Unexpected Error" msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:871 +#: src/zebtrack/ui/wizard/live_config_step.py:869 #, python-brace-format msgid "" "An error occurred while testing the connection:\n" @@ -8006,11 +8222,11 @@ msgid "" "{error}" msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:905 +#: src/zebtrack/ui/wizard/live_config_step.py:903 msgid "Aquarium Limit" msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:907 +#: src/zebtrack/ui/wizard/live_config_step.py:905 #, python-brace-format msgid "" "⚠️ Simultaneous recording is limited to 2 aquariums.\n" @@ -8023,38 +8239,334 @@ msgid "" "• Process offline after recording without detection" msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:918 +#: src/zebtrack/ui/wizard/live_config_step.py:916 #, python-brace-format msgid "" "A project with {count} aquariums exceeds the limit of 2 for simultaneous " "recording." msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:928 +#: src/zebtrack/ui/wizard/live_config_step.py:926 msgid "" "Mode selection cancelled. Adjust the number of aquariums or select a " "compatible mode." msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:945 +#: src/zebtrack/ui/wizard/live_config_step.py:943 #, python-brace-format msgid "Error validating data: {error}" msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:1050 +#: src/zebtrack/ui/wizard/live_config_step.py:1048 #, python-brace-format msgid "" "⚠ The template's camera is unavailable — selected '{fallback}'. Please " "check." msgstr "" -#: src/zebtrack/ui/wizard/live_config_step.py:1086 +#: src/zebtrack/ui/wizard/live_config_step.py:1083 #, python-brace-format msgid "" "⚠ The template's port ({port}) is unavailable — select another one or " "disable the Arduino." msgstr "" +#: src/zebtrack/ui/wizard/model_selection_step.py:54 +msgid " ⭐ Recommended" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:349 +msgid "Models and Weights" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:355 +msgid "" +"Adjust how ZebTrack will use each detection model.\n" +"If you prefer, keep the recommended defaults and move on." +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:399 +msgid "Methods and Weights per Role" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:409 +msgid "Aquarium (arena detection)" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:418 +msgid "Animals (tracking)" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:436 +msgid "Acceleration / OpenVINO" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:444 +msgid "Use OpenVINO (requires converting the weight)" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:451 +msgid "" +"Enable this once the matching OpenVINO model has been converted. It " +"allows faster inference on compatible CPUs." +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:471 +msgid "" +"AUTO lets OpenVINO choose the target automatically.\n" +"Pick CPU/GPU/NPU to force the target, when available." +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:478 +msgid "Detection Parameters (YOLO)" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:486 +msgid "Minimum confidence (0-1):" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:490 +msgid "" +"🎯 Minimum Confidence (Confidence Threshold)\n" +"\n" +"Filters 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" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:504 +msgid "NMS (overlap, 0-1):" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:508 +msgid "" +"🔲 NMS - Non-Maximum Suppression\n" +"\n" +"Removes 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" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:524 +msgid "Tracking Parameters (ByteTrack)" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:537 +msgid "Use ByteTrack (Recommended)" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:545 +msgid "" +"Enables the ByteTrack algorithm for robust tracking with a Kalman filter." +"\n" +"Recommended for most experiments." +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:563 +msgid "Track Threshold (0-1):" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:568 +msgid "" +"🛤️ Track Threshold\n" +"\n" +"Minimum confidence to START or KEEP a trajectory.\n" +"Low values help keep the trail of animals that are hard to detect.\n" +"\n" +"💡 Default: 0.25" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:577 +msgid "Match Threshold (0-1):" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:582 +msgid "" +"🔗 Match Threshold\n" +"\n" +"Tolerance when associating boxes.\n" +"HIGH values (close to 1.0) are more permissive for fast movement.\n" +"\n" +"💡 Default: 0.95 (for fast zebrafish)" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:592 +msgid "Track Buffer (frames):" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:597 +msgid "" +"🧠 Track Buffer\n" +"\n" +"The 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)" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:606 +#: src/zebtrack/ui/wizard/model_selection_step.py:756 +msgid "Max distance (px):" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:611 +msgid "" +"📏 Maximum Centre Distance\n" +"\n" +"The 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" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:621 +#: src/zebtrack/ui/wizard/model_selection_step.py:757 +msgid "IoU Threshold (0-1):" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:626 +msgid "" +"🔳 IoU Threshold\n" +"\n" +"Minimum overlap to prefer a box match over a distance match.\n" +"For small, fast fish, low values work better.\n" +"\n" +"💡 Default: 0.1" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:639 +msgid "" +"📊 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" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:659 +msgid "💡 Tip: adjust ONE parameter at a time (±0.05) and test!" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:674 +#, python-brace-format +msgid "Current YOLO defaults: confidence {confidence}, NMS {nms}." +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:690 +msgid "🔄 Restore Recommended Defaults" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:702 +msgid "" +"Restores every threshold to its recommended default value.\n" +"\n" +"Useful if you have made adjustments and want to start over." +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:710 +msgid "" +"Tip: keep the defaults if you are still setting up the videos. You can " +"review these values later in the project settings." +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:752 +#, python-brace-format +msgid "" +"ℹ️ 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." +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:768 +msgid "" +"💡 ByteTrack uses a Kalman filter to predict positions even when the fish " +"briefly disappears. Adjust the fields below for more stability." +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:795 +msgid "" +"Segmentation supports several animals per aquarium.\n" +"Detection is optimized for one animal per aquarium and uses ByteTrack." +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:810 +msgid "Select the weight file loaded for this role." +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:881 +msgid "❌ Confidence must be between 0 and 1" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:882 +msgid "❌ NMS must be between 0 and 1" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:883 +msgid "❌ Track must be between 0 and 1" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:884 +msgid "❌ Match must be between 0 and 1" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:914 +msgid "❌ Value must be a decimal (e.g. 0.25)" +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:1028 +msgid "" +"⚠️ Detection (det) is recommended for only 1 animal per aquarium. " +"Consider segmentation (seg) for several animals." +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:1189 +msgid "Enter decimal values between 0 and 1 for the parameters." +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:1192 +msgid "The confidence parameter must be between 0 and 1." +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:1193 +msgid "The NMS parameter must be between 0 and 1." +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:1194 +msgid "The track parameter must be between 0 and 1." +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:1195 +msgid "The match parameter must be between 0 and 1." +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:1203 +msgid "" +"Select a valid weight for the aquarium. The file must match the chosen " +"method." +msgstr "" + +#: src/zebtrack/ui/wizard/model_selection_step.py:1211 +msgid "" +"Select a valid weight for the animals. The file must match the chosen " +"method." +msgstr "" + #: src/zebtrack/ui/wizard/templates.py:354 #, python-brace-format msgid "Template loaded: {details}" diff --git a/src/zebtrack/ui/wizard/detection_step.py b/src/zebtrack/ui/wizard/detection_step.py index 05d63f85..ce33b366 100644 --- a/src/zebtrack/ui/wizard/detection_step.py +++ b/src/zebtrack/ui/wizard/detection_step.py @@ -20,6 +20,7 @@ import structlog from zebtrack.core.project.project_manager import ProjectManager +from zebtrack.i18n import _ from zebtrack.ui.window_utils import create_scrollbar from zebtrack.ui.wizard.base import WizardStep from zebtrack.ui.wizard.custom_regex_dialog import CustomRegexDialog @@ -30,10 +31,17 @@ log = structlog.get_logger() -_METHOD_LABELS = { - "seg": "Segmentação (seg)", - "det": "Detecção (det)", -} +def _method_labels() -> dict[str, str]: + """Map a detector method key to the label shown to the operator. + + This is a function, not a module-level dict, because a dict literal would + call _() at import time and freeze whatever language happened to be + installed then -- see docs/guides/developer/i18n.md. + """ + return { + "seg": _("Segmentation (seg)"), + "det": _("Detection (det)"), + } class DetectionStep(WizardStep): @@ -75,7 +83,7 @@ def __init__(self, parent, wizard_data: dict): # State self.scanned_videos: list[dict] = [] self.detected_design: dict[str, Any] | None = None - self.status_var = StringVar(value="Aguardando análise...") + self.status_var = StringVar(value=_("Waiting for analysis...")) self.custom_regex_patterns: dict[str, str] | None = None # User-defined regex patterns self.design_editor_confirmed = False self.template_info_var = StringVar(value="") @@ -85,12 +93,12 @@ def build_ui(self): """Build detection step UI - horizontal 2-column layout for better space usage.""" # Title (full width) title_font = tkfont.Font(size=14, weight="bold") - title = Label(self, text="Detecção Automática de Design", font=title_font) + title = Label(self, text=_("Automatic Design Detection"), font=title_font) title.pack(pady=(0, 5)) subtitle = Label( self, - text="Analisando estrutura de pastas e arquivos parquet...", + text=_("Analyzing folder structure and parquet files..."), fg="gray", wraplength=700, ) @@ -113,7 +121,7 @@ def build_ui(self): content_frame.rowconfigure(0, weight=1) # LEFT COLUMN: Detection results - results_frame = LabelFrame(content_frame, text="Resultados da Detecção", padx=10, pady=10) + results_frame = LabelFrame(content_frame, text=_("Detection Results"), padx=10, pady=10) results_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 10)) # Scrollable text widget for results (REDUCED height from 15 to 12) @@ -136,7 +144,7 @@ def build_ui(self): right_panel.grid(row=0, column=1, sticky="nsew") # Status message (top of right panel) - status_label_frame = LabelFrame(right_panel, text="Status", padx=10, pady=10) + status_label_frame = LabelFrame(right_panel, text=_("Status"), padx=10, pady=10) status_label_frame.pack(fill="x", pady=(0, 10)) Label( @@ -148,19 +156,19 @@ def build_ui(self): ).pack() # Action buttons (vertical stack in right panel) - button_frame = LabelFrame(right_panel, text="Ações", padx=10, pady=10) + button_frame = LabelFrame(right_panel, text=_("Actions"), padx=10, pady=10) button_frame.pack(fill="x", pady=(0, 10)) Button( button_frame, - text="🔄 Re-analisar", + text=_("🔄 Re-analyze"), command=self._run_detection, width=22, ).pack(pady=3, fill="x") self.edit_design_btn = Button( button_frame, - text="✏️ Editar Design", + text=_("✏️ Edit Design"), command=self._edit_design, width=22, state="disabled", @@ -169,20 +177,20 @@ def build_ui(self): Button( button_frame, - text="🔧 Regex Customizado", + text=_("🔧 Custom Regex"), command=self._configure_custom_regex, width=22, ).pack(pady=3, fill="x") # Help text (bottom of right panel) - help_frame = LabelFrame(right_panel, text="💡 Dica", padx=10, pady=10) + help_frame = LabelFrame(right_panel, text=_("💡 Tip"), padx=10, pady=10) help_frame.pack(fill="both", expand=True) help_text = Label( help_frame, - text=( - "A detecção automática identifica grupos, dias e sujeitos " - "baseando-se na estrutura de pastas." + text=_( + "Automatic detection identifies groups, days and subjects " + "from the folder structure." ), fg="gray", wraplength=240, @@ -202,15 +210,15 @@ def on_show(self): def _run_detection(self): """Run file scanning and design detection.""" if self.custom_regex_patterns: - self.status_var.set("Analisando (usando regex personalizada)...") + self.status_var.set(_("Analyzing (using custom regex)...")) else: - self.status_var.set("Analisando...") + self.status_var.set(_("Analyzing...")) # Get video paths from previous step video_paths = self.wizard_data.get("video_paths", []) if not video_paths: - self._show_error("Nenhum vídeo selecionado.") + self._show_error(_("No video selected.")) return try: @@ -261,7 +269,7 @@ def _run_detection(self): # Enable edit button (available for both detected and non-detected designs) self.edit_design_btn.config(state="normal") - self.status_var.set("Análise concluída!") + self.status_var.set(_("Analysis complete!")) log.info( "wizard.detection.completed", video_count=len(self.scanned_videos), @@ -273,7 +281,7 @@ def _run_detection(self): self.detected_design = None self.design_editor_confirmed = False self.edit_design_btn.config(state="disabled") - self._show_error(f"Falha ao concluir a detecção: {exc}") + self._show_error(_("Failed to complete detection: {error}").format(error=exc)) def _ensure_group_display_names(self) -> None: """Ensure detected design carries a friendly-name mapping.""" @@ -318,13 +326,16 @@ def _open_design_editor_for_confirmation(self, auto_invoked: bool = False) -> No return if auto_invoked: - message = ( - "Design experimental detectado!\n\n" - f"Grupos encontrados: {len(groups)}\n" - f"Dias: {len(self.detected_design.get('days') or [])}\n\n" - "Revise ou personalize os nomes antes de continuar." + message = _( + "Experimental design detected!\n\n" + "Groups found: {groups}\n" + "Days: {days}\n\n" + "Review or customize the names before continuing." + ).format( + groups=len(groups), + days=len(self.detected_design.get("days") or []), ) - messagebox.showinfo("Design Detectado", message, parent=self) + messagebox.showinfo(_("Design Detected"), message, parent=self) editor = DesignEditorDialog( self, @@ -347,8 +358,8 @@ def _open_design_editor_for_confirmation(self, auto_invoked: bool = False) -> No else: if auto_invoked: messagebox.showwarning( - "Confirmação Necessária", - "Confirme os nomes dos grupos antes de avançar.", + _("Confirmation Required"), + _("Confirm the group names before moving on."), parent=self, ) self.design_editor_confirmed = False @@ -776,18 +787,23 @@ def _display_results(self, parquet_summary: dict): self.results_text.delete("1.0", "end") # Video count - text = f"📊 Vídeos Encontrados: {len(self.scanned_videos)}\n\n" + text = _("📊 Videos found: {count}").format(count=len(self.scanned_videos)) + "\n\n" # Parquet summary - text += "📦 Arquivos Parquet Existentes:\n" - text += f" • Arena: {parquet_summary['total_arena']}\n" - text += f" • ROIs: {parquet_summary['total_rois']}\n" - text += f" • Trajetória: {parquet_summary['total_trajectory']}\n" - text += f" • Completos (todos 3): {parquet_summary['total_complete']}\n\n" + text += _("📦 Existing Parquet Files:") + "\n" + text += _(" • Arena: {count}").format(count=parquet_summary["total_arena"]) + "\n" + text += _(" • ROIs: {count}").format(count=parquet_summary["total_rois"]) + "\n" + text += ( + _(" • Trajectory: {count}").format(count=parquet_summary["total_trajectory"]) + "\n" + ) + text += ( + _(" • Complete (all 3): {count}").format(count=parquet_summary["total_complete"]) + + "\n\n" + ) # Design detection if self.detected_design: - text += "🎯 Design Experimental Detectado:\n" + text += _("🎯 Experimental Design Detected:") + "\n" groups = self.detected_design.get("groups") or [] friendly_names = self.detected_design.get("group_display_names") or {} group_descriptions = [] @@ -798,32 +814,51 @@ def _display_results(self, parquet_summary: dict): else: group_descriptions.append(group) - text += f" • Grupos: {', '.join(group_descriptions)}\n" + text += _(" • Groups: {groups}").format(groups=", ".join(group_descriptions)) + "\n" if self.detected_design.get("days"): - text += f" • Dias: {', '.join(self.detected_design['days'])}\n" + text += ( + _(" • Days: {days}").format(days=", ".join(self.detected_design["days"])) + + "\n" + ) - text += f" • Padrão: {self.detected_design['pattern_used']}\n" - text += f" • Confiança: {self.detected_design['confidence']:.0%}\n\n" + text += ( + _(" • Pattern: {pattern}").format(pattern=self.detected_design["pattern_used"]) + + "\n" + ) + text += ( + _(" • Confidence: {value}").format( + value=f"{self.detected_design['confidence']:.0%}" + ) + + "\n\n" + ) # Subjects per group if self.detected_design.get("subjects_per_group"): - text += " 📋 Sujeitos por Grupo:\n" + text += _(" 📋 Subjects per Group:") + "\n" for group, subjects in self.detected_design["subjects_per_group"].items(): if subjects: display = friendly_names.get(group, group) label = f"{group} ({display})" if display != group else group - text += f" - {label}: {len(subjects)} sujeito(s)\n" + text += ( + _(" - {label}: 1 subject").format(label=label) + if len(subjects) == 1 + else _(" - {label}: {count} subjects").format( + label=label, count=len(subjects) + ) + ) + "\n" else: project_type = self.wizard_data.get("project_type") if project_type == ProjectType.EXPERIMENTAL.value: - text += "⚠️ Design experimental não detectado automaticamente.\n\n" - text += "Possíveis causas:\n" - text += " • Estrutura de pastas não segue padrões reconhecidos\n" - text += " • Nomes de grupos/dias não são detectáveis (ex: Grupo1, Day01)\n\n" - text += "Você pode prosseguir sem design detectado ou reorganizar os arquivos.\n" + text += _( + "⚠️ Experimental design was not detected automatically.\n\n" + "Possible causes:\n" + " • The folder structure does not follow a recognized pattern\n" + " • Group/day names are not detectable (e.g. Grupo1, Day01)\n\n" + "You can continue without a detected design, or reorganize the files.\n" + ) else: - text += "ℹ️ Detecção de design desativada (projeto exploratório).\n" + text += _("ℹ️ Design detection disabled (exploratory project).") + "\n" # Detector configuration snapshot (helps confirm template application) detection_section = self._format_detector_configuration() @@ -831,11 +866,11 @@ def _display_results(self, parquet_summary: dict): text += f"\n{detection_section}\n" if self.custom_regex_patterns: - text += "\n🧩 Regex personalizada em uso:\n" + text += "\n" + _("🧩 Custom regex in use:") + "\n" for key, label in ( - ("group_pattern", "Grupos"), - ("day_pattern", "Dias"), - ("subject_pattern", "Sujeitos"), + ("group_pattern", _("Groups")), + ("day_pattern", _("Days")), + ("subject_pattern", _("Subjects")), ): pattern_value = self.custom_regex_patterns.get(key) if pattern_value: @@ -858,37 +893,38 @@ def _format_detector_configuration(self) -> str: ): return "" - lines = ["⚙️ Configuração Atual do Detector:"] + lines = [_("⚙️ Current Detector Configuration:")] aquarium_method = model_selection.get("aquarium_method") animal_method = model_selection.get("animal_method") if aquarium_method or animal_method: + method_labels = _method_labels() aquarium_label = ( - _METHOD_LABELS.get(aquarium_method, aquarium_method) + method_labels.get(aquarium_method, aquarium_method) if isinstance(aquarium_method, str) else None ) animal_label = ( - _METHOD_LABELS.get(animal_method, animal_method) + method_labels.get(animal_method, animal_method) if isinstance(animal_method, str) else None ) if aquarium_label: - lines.append(f" • Método aquário: {aquarium_label}") + lines.append(_(" • Aquarium method: {method}").format(method=aquarium_label)) if animal_label: - lines.append(f" • Método animais: {animal_label}") + lines.append(_(" • Animal method: {method}").format(method=animal_label)) aquarium_weight = weight_assignments.get("aquarium") animal_weight = weight_assignments.get("animal") if aquarium_weight or animal_weight: if aquarium_weight: - lines.append(f" • Peso aquário: {aquarium_weight}") + lines.append(_(" • Aquarium weight: {weight}").format(weight=aquarium_weight)) if animal_weight: - lines.append(f" • Peso animais: {animal_weight}") + lines.append(_(" • Animal weight: {weight}").format(weight=animal_weight)) if use_openvino is not None: - status = "Ativado" if use_openvino else "Desativado" - lines.append(f" • OpenVINO: {status}") + status = _("Enabled") if use_openvino else _("Disabled") + lines.append(_(" • OpenVINO: {status}").format(status=status)) conf = detector_params.get("confidence_threshold") nms = detector_params.get("nms_threshold") @@ -910,10 +946,10 @@ def _format_detector_configuration(self) -> str: def _show_error(self, message: str): """Display error message.""" - self.status_var.set("Erro!") + self.status_var.set(_("Error!")) self.results_text.config(state="normal") self.results_text.delete("1.0", "end") - self.results_text.insert("1.0", f"❌ Erro: {message}") + self.results_text.insert("1.0", _("❌ Error: {message}").format(message=message)) self.results_text.config(state="disabled") def _configure_custom_regex(self): @@ -964,17 +1000,16 @@ def _handle_custom_regex_from_editor(self, patterns: dict | None) -> dict | None if self.custom_regex_patterns: if new_design: - self.status_var.set("Regex personalizado aplicado ✓") + self.status_var.set(_("Custom regex applied ✓")) else: self.status_var.set( - "Regex personalizado não encontrou design; " - "ajuste os padrões ou edite manualmente." + _("The custom regex found no design; adjust the patterns or edit manually.") ) else: if new_design: - self.status_var.set("Regex personalizado removido. Detecção padrão reaplicada ✓") + self.status_var.set(_("Custom regex removed. Default detection reapplied ✓")) else: - self.status_var.set("Detecção padrão reaplicada, mas nenhum design foi encontrado.") + self.status_var.set(_("Default detection reapplied, but no design was found.")) return new_design @@ -1091,9 +1126,9 @@ def _edit_design(self): parquet_summary = self._calculate_parquet_summary() self._display_results(parquet_summary) if self.custom_regex_patterns: - self.status_var.set("Design editado manualmente ✓ (regex personalizado aplicado)") + self.status_var.set(_("Design edited manually ✓ (custom regex applied)")) else: - self.status_var.set("Design editado manualmente ✓ (regex padrão)") + self.status_var.set(_("Design edited manually ✓ (default regex)")) def validate(self) -> tuple[bool, str]: """ @@ -1105,7 +1140,7 @@ def validate(self) -> tuple[bool, str]: if not self.scanned_videos: return ( False, - "Nenhum vídeo foi encontrado. Volte e selecione vídeos válidos.", + _("No video was found. Go back and select valid videos."), ) project_type = self.wizard_data.get("project_type") @@ -1117,7 +1152,7 @@ def validate(self) -> tuple[bool, str]: ): return ( False, - "Confirme os nomes dos grupos no editor antes de avançar.", + _("Confirm the group names in the editor before moving on."), ) return (True, "") @@ -1164,7 +1199,9 @@ def set_data(self, data: dict): if self.scanned_videos: parquet_summary = data.get("parquet_summary", self._calculate_parquet_summary()) self._display_results(parquet_summary) - self.status_var.set("Resultados anteriores (use Re-analisar para atualizar)") + self.status_var.set( + _("Previous results (use '{button}' to refresh)").format(button=_("🔄 Re-analyze")) + ) self._update_template_banner() def _update_template_banner(self): diff --git a/src/zebtrack/ui/wizard/model_selection_step.py b/src/zebtrack/ui/wizard/model_selection_step.py index 90e759b2..da1eb04f 100644 --- a/src/zebtrack/ui/wizard/model_selection_step.py +++ b/src/zebtrack/ui/wizard/model_selection_step.py @@ -9,6 +9,7 @@ import structlog from zebtrack.core.services.weight_manager import WeightManager +from zebtrack.i18n import _ from zebtrack.ui.wizard.base import WizardStep from zebtrack.ui.wizard.enums import WizardStepID from zebtrack.ui.wizard.templates import format_template_banner @@ -27,10 +28,31 @@ DEFAULT_MAX_CENTER_DISTANCE = 200.0 # Pixels, ~6 body lengths for 30px zebrafish DEFAULT_IOU_THRESHOLD = 0.1 # Low for small objects with little overlap -_METHOD_OPTIONS: dict[str, str] = { - "seg": "Segmentação (seg)", - "det": "Detecção (det)", -} + +def _method_options() -> dict[str, str]: + """Map a detector method key to the label shown in the combobox. + + A module-level dict would call _() at import time and freeze the language + (see docs/guides/developer/i18n.md). It is also the single source for + _method_key_from_label(), which maps the displayed label back to the key -- + the two must never be able to drift. + """ + return { + "seg": _("Segmentation (seg)"), + "det": _("Detection (det)"), + } + + +def _recommended_suffix() -> str: + """Marker appended to the perspective-recommended weight in the dropdown. + + This string is BOTH displayed and parsed back off again: the combobox shows + "", and _strip_annotation() removes it before the name is + validated and stored. Translating the display without the parser would glue + the marker onto the weight name, so validate() would reject a weight the + user picked from the list. One definition, three call sites. + """ + return _(" ⭐ Recommended") class ModelSelectionStep(WizardStep): @@ -155,11 +177,12 @@ def _load_weight_catalog(self) -> None: def _method_display(self, method_key: str | None) -> str: """Format method labels for display.""" - if method_key in _METHOD_OPTIONS: - return _METHOD_OPTIONS[method_key] + options = _method_options() + if method_key in options: + return options[method_key] if method_key: return method_key - return _METHOD_OPTIONS["seg"] + return options["seg"] def _recommended_use_bytetrack(self, animal_method: str) -> bool: """Return recommended ByteTrack default for current wizard context.""" @@ -282,17 +305,17 @@ def _prefill_from_wizard_data(self) -> None: def _default_weight_for_method(self, method_key: str) -> str: perspective = self._get_active_perspective() if perspective: - name, _ = self.weight_manager.get_weight_by_perspective_and_type( + name, _details = self.weight_manager.get_weight_by_perspective_and_type( perspective, method_key, ) if name: return name if method_key == "seg": - name, _ = self.weight_manager.get_default_seg_weight() + name, _details = self.weight_manager.get_default_seg_weight() return name or (self.seg_weight_names[0] if self.seg_weight_names else "") if method_key == "det": - name, _ = self.weight_manager.get_default_det_weight() + name, _details = self.weight_manager.get_default_det_weight() return name or (self.det_weight_names[0] if self.det_weight_names else "") return "" @@ -324,14 +347,14 @@ def _normalize_openvino_device(self, device: str | None) -> str: def build_ui(self) -> None: """Build the UI for this step with model selection controls.""" title_font = tkfont.Font(size=14, weight="bold") - title = Label(self, text="Modelos e Pesos", font=title_font) + title = Label(self, text=_("Models and Weights"), font=title_font) title.pack(pady=(0, 10)) subtitle = Label( self, - text=( - "Ajuste como o ZebTrack utilizará cada modelo de detecção.\n" - "Se preferir, mantenha os padrões recomendados e avance." + text=_( + "Adjust how ZebTrack will use each detection model.\n" + "If you prefer, keep the recommended defaults and move on." ), fg="gray", wraplength=560, @@ -374,7 +397,7 @@ def build_ui(self) -> None: methods_frame = LabelFrame( left_column, - text="Métodos e Pesos por Função", + text=_("Methods and Weights per Role"), padx=10, pady=5, ) @@ -384,7 +407,7 @@ def build_ui(self) -> None: self._build_method_row( parent=methods_frame, row=0, - title="Aquário (detecção de arena)", + title=_("Aquarium (arena detection)"), method_var=self.aquarium_method_var, weight_var=self.aquarium_weight_var, combo_attr="_aquarium_weight_combo", @@ -393,7 +416,7 @@ def build_ui(self) -> None: self._build_method_row( parent=methods_frame, row=1, - title="Animais (rastreamento)", + title=_("Animals (tracking)"), method_var=self.animal_method_var, weight_var=self.animal_weight_var, combo_attr="_animal_weight_combo", @@ -411,7 +434,7 @@ def build_ui(self) -> None: acceleration_frame = LabelFrame( left_column, - text="Aceleração / OpenVINO", + text=_("Acceleration / OpenVINO"), padx=10, pady=5, ) @@ -419,19 +442,21 @@ def build_ui(self) -> None: openvino_check = ttk.Checkbutton( acceleration_frame, - text="Usar OpenVINO (requer conversão do peso)", + text=_("Use OpenVINO (requires converting the weight)"), variable=self.use_openvino_var, ) openvino_check.pack(anchor="w") ToolTip( openvino_check, - "Ative quando o modelo OpenVINO correspondente já foi convertido." - " Permite inferência mais rápida em CPUs compatíveis.", + _( + "Enable this once the matching OpenVINO model has been converted." + " It allows faster inference on compatible CPUs." + ), ) device_row = ttk.Frame(acceleration_frame) device_row.pack(fill="x", pady=(6, 0)) - ttk.Label(device_row, text="Dispositivo OpenVINO:").pack(side="left") + ttk.Label(device_row, text=_("OpenVINO device:")).pack(side="left") device_combo = ttk.Combobox( device_row, @@ -443,13 +468,15 @@ def build_ui(self) -> None: device_combo.pack(side="left", padx=(8, 0)) ToolTip( device_combo, - "AUTO usa seleção automática do OpenVINO.\n" - "Escolha CPU/GPU/NPU para forçar o alvo, quando disponível.", + _( + "AUTO lets OpenVINO choose the target automatically.\n" + "Pick CPU/GPU/NPU to force the target, when available." + ), ) detector_frame = LabelFrame( left_column, - text="Parâmetros de Detecção (YOLO)", + text=_("Detection Parameters (YOLO)"), padx=10, pady=5, ) @@ -457,37 +484,37 @@ def build_ui(self) -> None: self._build_detector_param_row( detector_frame, - label="Confiança mínima (0-1):", + label=_("Minimum confidence (0-1):"), var=self.confidence_var, column=0, - tooltip=( - "🎯 Confiança Mínima (Confidence Threshold)\n\n" - "Filtra 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" + tooltip=_( + "🎯 Minimum Confidence (Confidence Threshold)\n\n" + "Filters 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" ), param_key="confidence", ) self._build_detector_param_row( detector_frame, - label="NMS (sobreposição, 0-1):", + label=_("NMS (overlap, 0-1):"), var=self.nms_var, column=1, - tooltip=( + tooltip=_( "🔲 NMS - Non-Maximum Suppression\n\n" - "Elimina 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" + "Removes 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" ), param_key="nms", ) @@ -495,7 +522,7 @@ def build_ui(self) -> None: # ByteTrack Section - positioned in right column to use the guide space self._bytetrack_frame = LabelFrame( right_column, - text="Parâmetros de Rastreamento (ByteTrack)", + text=_("Tracking Parameters (ByteTrack)"), padx=10, pady=8, ) @@ -508,15 +535,17 @@ def build_ui(self) -> None: bytetrack_check = ttk.Checkbutton( self._bytetrack_frame, - text="Usar ByteTrack (Recomendado)", + text=_("Use ByteTrack (Recommended)"), variable=self.use_bytetrack_var, command=self._toggle_bytetrack_options, ) bytetrack_check.grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 5)) ToolTip( bytetrack_check, - "Ativa o algoritmo ByteTrack para rastreamento robusto com Filtro de Kalman.\n" - "Recomendado para a maioria dos experimentos.", + _( + "Enables the ByteTrack algorithm for robust tracking with a Kalman filter.\n" + "Recommended for most experiments." + ), ) self.bytetrack_hint_var = StringVar() @@ -532,73 +561,73 @@ def build_ui(self) -> None: self._build_detector_param_row( self._bytetrack_frame, - label="Track Threshold (0-1):", + label=_("Track Threshold (0-1):"), var=self.track_var, column=0, row=2, - tooltip=( + tooltip=_( "🛤️ Track Threshold\n\n" - "Confiança mínima para INICIAR ou MANTER uma trajetória.\n" - "Valores baixos ajudam a manter o rastro de animais difíceis de detectar.\n\n" - "💡 Padrão: 0.25" + "Minimum confidence to START or KEEP a trajectory.\n" + "Low values help keep the trail of animals that are hard to detect.\n\n" + "💡 Default: 0.25" ), param_key="track", ) self._build_detector_param_row( self._bytetrack_frame, - label="Match Threshold (0-1):", + label=_("Match Threshold (0-1):"), var=self.match_var, column=1, row=2, - tooltip=( + tooltip=_( "🔗 Match Threshold\n\n" - "Tolerância para associação de caixas.\n" - "Valores ALTOS (perto de 1.0) são mais permissivos para movimento rápido.\n\n" - "💡 Padrão: 0.95 (para zebrafish rápidos)" + "Tolerance when associating boxes.\n" + "HIGH values (close to 1.0) are more permissive for fast movement.\n\n" + "💡 Default: 0.95 (for fast zebrafish)" ), param_key="match", ) self._build_detector_param_row( self._bytetrack_frame, - label="Track Buffer (frames):", + label=_("Track Buffer (frames):"), var=self.track_buffer_var, column=0, row=3, - tooltip=( + tooltip=_( "🧠 Track Buffer\n\n" - "Memó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)" + "The 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)" ), param_key="track_buffer", ) self._build_detector_param_row( self._bytetrack_frame, - label="Distância Máx (px):", + label=_("Max distance (px):"), var=self.max_center_dist_var, column=1, row=3, - tooltip=( - "📏 Distância Máxima de Centro\n\n" - "Distâ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" + tooltip=_( + "📏 Maximum Centre Distance\n\n" + "The 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" ), param_key="max_center_dist", ) self._build_detector_param_row( self._bytetrack_frame, - label="IoU Threshold (0-1):", + label=_("IoU Threshold (0-1):"), var=self.iou_thresh_var, column=0, row=4, - tooltip=( + tooltip=_( "🔳 IoU Threshold\n\n" - "Sobreposição mínima para preferir 'Match por Caixa' em vez de distância.\n" - "Para peixes pequenos e rápidos, valores baixos são melhores.\n\n" - "💡 Padrão: 0.1" + "Minimum overlap to prefer a box match over a distance match.\n" + "For small, fast fish, low values work better.\n\n" + "💡 Default: 0.1" ), param_key="iou_thresh", ) @@ -607,12 +636,12 @@ def build_ui(self) -> None: guide_separator = ttk.Separator(self._bytetrack_frame, orient="horizontal") guide_separator.grid(row=5, column=0, columnspan=2, sticky="ew", pady=(12, 8)) - guide_text = ( - "📊 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" + guide_text = _( + "📊 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" ) guide_label = Label( @@ -628,7 +657,7 @@ def build_ui(self) -> None: # Footer Tip tip_label = Label( self._bytetrack_frame, - text="💡 Dica: Ajuste UM parâmetro por vez (±0.05) e teste!", + text=_("💡 Tip: adjust ONE parameter at a time (±0.05) and test!"), fg="#006600", font=("TkDefaultFont", 9, "bold"), ) @@ -643,8 +672,9 @@ def build_ui(self) -> None: defaults_label = Label( left_column, - text=( - f"Padrões atuais YOLO: confiança {display_confidence:.2f}, NMS {display_nms:.2f}." + text=_("Current YOLO defaults: confidence {confidence}, NMS {nms}.").format( + confidence=f"{display_confidence:.2f}", + nms=f"{display_nms:.2f}", ), fg="#555555", wraplength=560, @@ -658,7 +688,7 @@ def build_ui(self) -> None: restore_btn = Button( left_column, - text="🔄 Restaurar Padrões Recomendados", + text=_("🔄 Restore Recommended Defaults"), command=self._restore_default_thresholds, bg="#E3F2FD", fg="#1565C0", @@ -669,17 +699,17 @@ def build_ui(self) -> None: restore_btn.pack(fill="x", padx=10, pady=(5, 0)) ToolTip( restore_btn, - ( - "Restaura todos os thresholds para os valores padrão recomendados.\n\n" - "Útil se você fez ajustes e quer voltar ao ponto de partida." + _( + "Restores every threshold to its recommended default value.\n\n" + "Useful if you have made adjustments and want to start over." ), ) footer = Label( left_column, - text=( - "Dica: mantenha os padrões se ainda estiver configurando os vídeos." - " Você pode revisar esses valores depois nas configurações do projeto." + text=_( + "Tip: keep the defaults if you are still setting up the videos." + " You can review these values later in the project settings." ), fg="#555555", wraplength=560, @@ -719,9 +749,14 @@ def _toggle_bytetrack_options(self) -> None: if not enabled: self.bytetrack_hint_var.set( - "ℹ️ ByteTrack desativado. O sistema usará um rastreador híbrido simplificado " - "que utiliza apenas 'Distância Máxima' e 'IoU Threshold' para manter o ID estável. " - "Ideal para 1 animal/aquário." + _( + "ℹ️ 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." + ).format( + distance=_("Max distance (px):").rstrip(":"), + iou=_("IoU Threshold (0-1):").rstrip(":"), + ) ) # Re-enable distance and iou for the simple tracker for key in ["max_center_dist", "iou_thresh"]: @@ -730,8 +765,10 @@ def _toggle_bytetrack_options(self) -> None: entry.configure(state="normal") else: self.bytetrack_hint_var.set( - "💡 O ByteTrack usa Filtro de Kalman para prever posições mesmo quando o peixe " - "some brevemente. Ajuste os campos abaixo para maior estabilidade." + _( + "💡 ByteTrack uses a Kalman filter to predict positions even when the " + "fish briefly disappears. Adjust the fields below for more stability." + ) ) def _build_method_row( @@ -748,15 +785,17 @@ def _build_method_row( method_combo = ttk.Combobox( parent, textvariable=method_var, - values=[label for label in _METHOD_OPTIONS.values()], + values=list(_method_options().values()), state="readonly", width=24, ) method_combo.grid(row=row, column=1, padx=(10, 10), pady=5, sticky="w") ToolTip( method_combo, - "Segmentação suporta múltiplos animais por aquário.\n" - "Detecção é otimizada para um animal por aquário e usa ByteTrack.", + _( + "Segmentation supports several animals per aquarium.\n" + "Detection is optimized for one animal per aquarium and uses ByteTrack." + ), ) weight_combo = ttk.Combobox( @@ -769,7 +808,7 @@ def _build_method_row( weight_combo.grid(row=row, column=2, pady=5, sticky="w") ToolTip( weight_combo, - "Selecione o arquivo de peso carregado para esta função.", + _("Select the weight file loaded for this role."), ) setattr(self, combo_attr, weight_combo) @@ -836,17 +875,20 @@ def _validate_threshold_field(self, param_key: str) -> bool: bool: True if valid, False otherwise """ # Get the StringVar and Entry widget + # One finished sentence per field instead of splicing a noun into a + # template: capitalising a translated word (the old .capitalize() call) + # is not safe across languages, and the Portuguese needs its own wording. var_map = { - "confidence": (self.confidence_var, "confiança"), - "nms": (self.nms_var, "NMS"), - "track": (self.track_var, "track"), - "match": (self.match_var, "associação"), + "confidence": (self.confidence_var, _("❌ Confidence must be between 0 and 1")), + "nms": (self.nms_var, _("❌ NMS must be between 0 and 1")), + "track": (self.track_var, _("❌ Track must be between 0 and 1")), + "match": (self.match_var, _("❌ Match must be between 0 and 1")), } if param_key not in var_map: return True - var, label = var_map[param_key] + var, range_error = var_map[param_key] entry = self._threshold_entries.get(param_key) error_label = self._threshold_error_labels.get(param_key) @@ -870,7 +912,7 @@ def _validate_threshold_field(self, param_key: str) -> bool: entry.configure(background="#FFE0E0") # Light red except TclError: log.debug("model_selection.entry_highlight.error", exc_info=True) - error_label.configure(text="❌ Valor deve ser decimal (ex: 0.25)") + error_label.configure(text=_("❌ Value must be a decimal (e.g. 0.25)")) return False # Check range (0, 1) exclusive @@ -879,7 +921,7 @@ def _validate_threshold_field(self, param_key: str) -> bool: entry.configure(background="#FFE0E0") # Light red except TclError: log.debug("model_selection.entry_highlight_range.error", exc_info=True) - error_label.configure(text=f"❌ {label.capitalize()} deve estar entre 0 e 1") + error_label.configure(text=range_error) return False # Valid - clear error @@ -940,7 +982,7 @@ def _refresh_weight_dropdowns(self, role: str | None = None) -> None: # Annotate perspective-recommended weights recommended_name: str | None = None if perspective: - rec, _ = self.weight_manager.get_weight_by_perspective_and_type( + rec, _details = self.weight_manager.get_weight_by_perspective_and_type( perspective, method_key, ) @@ -949,7 +991,7 @@ def _refresh_weight_dropdowns(self, role: str | None = None) -> None: display_options: list[str] = [] for opt in raw_options: if opt == recommended_name: - display_options.insert(0, f"{opt} ⭐ Recomendado") + display_options.insert(0, f"{opt}{_recommended_suffix()}") else: display_options.append(opt) @@ -959,7 +1001,7 @@ def _refresh_weight_dropdowns(self, role: str | None = None) -> None: current_weight = weight_var.get() # Check if current value is still valid (strip annotation) valid = any( - current_weight == opt or opt.startswith(current_weight + " ⭐") + current_weight == opt or opt == current_weight + _recommended_suffix() for opt in display_options ) if not valid: @@ -971,7 +1013,7 @@ def _refresh_weight_dropdowns(self, role: str | None = None) -> None: combo.configure(state="disabled") def _method_key_from_label(self, label_value: str) -> str: - for key, label in _METHOD_OPTIONS.items(): + for key, label in _method_options().items(): if label_value == label or label_value == key: return key return label_value or "seg" @@ -982,8 +1024,10 @@ def _update_animal_method_hint(self) -> None: if method_key == "det" and animals_per_aquarium > 1: self.animal_method_hint_var.set( - "⚠️ Detecção (det) é recomendada para apenas 1 animal por aquário." - " Considere segmentação (seg) para múltiplos animais." + _( + "⚠️ Detection (det) is recommended for only 1 animal per aquarium." + " Consider segmentation (seg) for several animals." + ) ) else: self.animal_method_hint_var.set("") @@ -1124,8 +1168,8 @@ def _restore_default_thresholds(self) -> None: # ------------------------------------------------------------------ @staticmethod def _strip_annotation(value: str) -> str: - """Remove the '⭐ Recomendado' suffix from a weight name.""" - marker = " ⭐ Recomendado" + """Remove the recommended-weight suffix from a weight name.""" + marker = _recommended_suffix() if value.endswith(marker): return value[: -len(marker)] return value @@ -1142,20 +1186,28 @@ def validate(self) -> tuple[bool, str]: track = float(self.track_var.get()) match = float(self.match_var.get()) except ValueError: - return False, "Informe valores decimais entre 0 e 1 para os parâmetros." + return False, _("Enter decimal values between 0 and 1 for the parameters.") - for label, value in ( - ("confiança", confidence), - ("NMS", nms), - ("track", track), - ("associação", match), + for message, value in ( + (_("The confidence parameter must be between 0 and 1."), confidence), + (_("The NMS parameter must be between 0 and 1."), nms), + (_("The track parameter must be between 0 and 1."), track), + (_("The match parameter must be between 0 and 1."), match), ): if not 0.0 < value < 1.0: - return False, f"O parâmetro de {label} deve estar entre 0 e 1." + return False, message - for role, method_var, weight_var in ( - ("aquário", self.aquarium_method_var, self.aquarium_weight_var), - ("animais", self.animal_method_var, self.animal_weight_var), + for role_message, method_var, weight_var in ( + ( + _("Select a valid weight for the aquarium. The file must match the chosen method."), + self.aquarium_method_var, + self.aquarium_weight_var, + ), + ( + _("Select a valid weight for the animals. The file must match the chosen method."), + self.animal_method_var, + self.animal_weight_var, + ), ): method_key = self._method_key_from_label(method_var.get()) if method_key == "seg": @@ -1164,10 +1216,7 @@ def validate(self) -> tuple[bool, str]: options = self.det_weight_names weight_name = self._strip_annotation(weight_var.get()) if options and weight_name not in options: - return False, ( - f"Selecione um peso válido para {role}." - " O arquivo precisa corresponder ao método escolhido." - ) + return False, role_message return True, "" diff --git a/tests/ui/wizard/test_model_selection_step.py b/tests/ui/wizard/test_model_selection_step.py index 72cf254c..ea9d12d4 100644 --- a/tests/ui/wizard/test_model_selection_step.py +++ b/tests/ui/wizard/test_model_selection_step.py @@ -151,7 +151,7 @@ def test_validation_fails_with_invalid_confidence_format(self, mock_weight_manag is_valid, error_message = step.validate() assert not is_valid - assert "decimais" in error_message.lower() or "valor" in error_message.lower() + assert "decimal values" in error_message.lower() def test_validation_fails_with_threshold_out_of_range(self, mock_weight_manager): """Validation should fail when threshold is outside (0, 1) range.""" @@ -168,7 +168,7 @@ def test_validation_fails_with_threshold_out_of_range(self, mock_weight_manager) is_valid, error_message = step.validate() assert not is_valid - assert "entre 0 e 1" in error_message.lower() + assert "between 0 and 1" in error_message.lower() def test_validation_fails_with_threshold_at_boundary(self, mock_weight_manager): """Validation should fail with threshold exactly at 0 or 1.""" @@ -279,7 +279,7 @@ def test_weight_dropdown_updates_on_method_change(self, mock_weight_manager): assert "-seg" in initial_animal_weight or initial_animal_weight == "yolov8n-seg.pt" # Change to det method - step.animal_method_var.set("Detecção (det)") + step.animal_method_var.set(step._method_display("det")) step._on_animal_method_change() # Should now have a det weight @@ -294,13 +294,13 @@ def test_validation_fails_with_mismatched_weight(self, mock_weight_manager): step.build_ui() # Force invalid state: seg method with det weight - step.aquarium_method_var.set("Segmentação (seg)") + step.aquarium_method_var.set(step._method_display("seg")) step.aquarium_weight_var.set("invalid_weight_not_in_catalog.pt") is_valid, error_message = step.validate() assert not is_valid - assert "peso válido" in error_message.lower() + assert "valid weight" in error_message.lower() def test_animal_method_hint_for_multiple_animals(self, mock_weight_manager): """Hint should appear when using det method with multiple animals.""" @@ -311,14 +311,14 @@ def test_animal_method_hint_for_multiple_animals(self, mock_weight_manager): step.build_ui() # Set to det method - step.animal_method_var.set("Detecção (det)") + step.animal_method_var.set(step._method_display("det")) step._update_animal_method_hint() hint = step.animal_method_hint_var.get() # Should show warning about det with multiple animals assert "⚠️" in hint or "det" in hint.lower() - assert "múltiplos animais" in hint.lower() or "1 animal" in hint.lower() + assert "several animals" in hint.lower() or "1 animal" in hint.lower() def test_animal_method_hint_cleared_for_seg(self, mock_weight_manager): """Hint should be cleared when using seg method.""" @@ -329,7 +329,7 @@ def test_animal_method_hint_cleared_for_seg(self, mock_weight_manager): step.build_ui() # Set to seg method - step.animal_method_var.set("Segmentação (seg)") + step.animal_method_var.set(step._method_display("seg")) step._update_animal_method_hint() hint = step.animal_method_hint_var.get() diff --git a/tests/ui/wizard/test_wizard_detection.py b/tests/ui/wizard/test_wizard_detection.py index 95362c9e..febc5cfd 100644 --- a/tests/ui/wizard/test_wizard_detection.py +++ b/tests/ui/wizard/test_wizard_detection.py @@ -128,7 +128,7 @@ def test_detection_step_validate_fails_when_no_videos(self, mock_scan): is_valid, error_message = step.validate() assert not is_valid - assert "nenhum vídeo" in error_message.lower() + assert "no video was found" in error_message.lower() @patch("zebtrack.ui.wizard.detection_step.ProjectManager.scan_input_paths") def test_detection_step_validate_succeeds_with_videos(self, mock_scan): @@ -276,7 +276,7 @@ def test_custom_regex_from_editor_recalculates_design(self, mock_detect): assert result == new_design assert step.detected_design == new_design assert step.custom_regex_patterns is not None - assert "regex personalizado aplicado" in step.status_var.get().lower() + assert "custom regex applied" in step.status_var.get().lower() display_text = step.results_text.get("1.0", "end-1c") assert "Regex Group" in display_text From a0260306bbb255d0eb9f2724bb9ce4f06d33534e Mon Sep 17 00:00:00 2001 From: MarkSant Date: Fri, 14 Aug 2026 16:19:43 -0300 Subject: [PATCH 12/21] feat(i18n): fechar ui/wizard (lote 4d) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Os seis arquivos restantes de ui/wizard: discovery_step, custom_regex_dialog, file_selection_step, import_config_step, calibration_step e design_editor_dialog. O pacote ui/wizard vai a ZERO literais acentuados (561 -> 395 no pacote todo; o que sobra e ui/dialogs). Tres achados estruturais: 1. custom_regex_dialog guardava CINCO copias do mesmo {"group": "Grupo", "day": "Dia", "subject": "Sujeito"}: o cabecalho da arvore, a linha de resultado por campo, o placeholder "aguardando", o erro de validacao e a pre-visualizacao. Cada copia precisaria do seu proprio _(), e elas TEM de concordar entre si na tela (o mesmo rotulo aparece nos cinco lugares). Colapsaram em _field_labels(). 2. file_selection_step montava o resumo colando "N arquivo(s)" + " + " + " selecionado(s)". O participio final tem de concordar com um sujeito que pode ser masculino (arquivo), feminino (pasta) ou os dois ao mesmo tempo -- e era por isso que carregava o "(s)". O portugues agora comeca por rotulo ("Seleção: 1 arquivo + 2 pastas"), entao nada precisa concordar, e as contagens usam os pares singular/plural que ja existiam. 3. import_config_step e confirmation_step descrevem as mesmas estrategias de ROI com portugues diferente ("Merge (manter ambos)" no radio, "Mesclar (manter ambos)" no resumo; "Full (do zero)" vs "Full (processar do zero)"). Mantidos msgids distintos em vez de colidir -- colidir faria update_translations descartar um dos pares em silencio. Nos testes, "1 arquivo(s)" virou "1 file": a assercao agora confere a forma singular de verdade, que era justamente o que o "(s)" escondia. Junto vao cinco anotacoes `str` -> `Path | str` em custom_regex_dialog e file_selection_step. Sao violacoes PRE-EXISTENTES da regra do proprio repo: o hook check-path-consistency so inspeciona arquivos tocados pelo commit, e este lote e a primeira mudanca a alcancar esses arquivos. Os corpos sao orientados a string (re.search, fatiamento, os.path.basename), entao cada um normaliza com str(path) no topo -- a anotacao alarga, o comportamento nao muda. Verificacao: ruff limpo, mypy limpo em 698 arquivos, 4210 fast, 1079 gui. Co-Authored-By: Claude Opus 5 --- docs/tasks/active/ROLLING_TASK_LOG.md | 15 +- .../locales/_pairs/pr3-wizard-4d.json | 181 ++ .../locales/pt_BR/LC_MESSAGES/zebtrack.mo | Bin 194544 -> 217719 bytes .../locales/pt_BR/LC_MESSAGES/zebtrack.po | 2372 ++++++++++++----- src/zebtrack/locales/zebtrack.pot | 1122 +++++++- src/zebtrack/ui/wizard/calibration_step.py | 197 +- src/zebtrack/ui/wizard/custom_regex_dialog.py | 149 +- .../ui/wizard/design_editor_dialog.py | 83 +- src/zebtrack/ui/wizard/discovery_step.py | 117 +- src/zebtrack/ui/wizard/file_selection_step.py | 102 +- src/zebtrack/ui/wizard/import_config_step.py | 76 +- tests/ui/wizard/test_wizard_file_selection.py | 16 +- tests/ui/wizard/test_wizard_import_config.py | 2 +- 13 files changed, 3364 insertions(+), 1068 deletions(-) create mode 100644 src/zebtrack/locales/_pairs/pr3-wizard-4d.json diff --git a/docs/tasks/active/ROLLING_TASK_LOG.md b/docs/tasks/active/ROLLING_TASK_LOG.md index 718c1450..dfdd7cc9 100644 --- a/docs/tasks/active/ROLLING_TASK_LOG.md +++ b/docs/tasks/active/ROLLING_TASK_LOG.md @@ -99,9 +99,22 @@ no topo do ratchet; depois, atualizar a seção "A migração" em 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()`. - - [ ] 4d: `discovery_step` (20), `custom_regex_dialog` (19), + - [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. - [ ] Lote 5: `ui/dialogs/**` (26 arquivos). - [ ] Fechamento: colapsar MIGRATED_PATHS, atualizar guia e CHANGELOG. 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/pt_BR/LC_MESSAGES/zebtrack.mo b/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.mo index 2c0df80c53697fd8952cff02d2abdea10a9b8248..28b84d6a2c051f964ebf394fe8721a386678974f 100644 GIT binary patch delta 50133 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

1L@wZ` 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\n" -"Language-Team: pt_BR \n" "Language: pt_BR\n" +"Language-Team: pt_BR \n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"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." +"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." +"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 @@ -96,19 +98,17 @@ 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 " +"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 " +"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." +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 @@ -117,11 +117,11 @@ 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." +"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." +"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." @@ -189,8 +189,8 @@ msgid "" "Aquarium {number} has no subject defined. Configure the aquariums before " "processing." msgstr "" -"Aquário {number} não tem sujeito definido. Configure os aquários antes de " -"processar." +"Aquário {number} não tem sujeito definido. Configure os aquários antes de" +" processar." # | msgid "Analyze Single Video" #: src/zebtrack/coordinators/_single_video_mixin.py:220 @@ -440,8 +440,8 @@ msgid "" "A video analysis is already running. Please wait or cancel the current " "analysis." msgstr "" -"Uma análise de vídeo já está em andamento. Por favor, aguarde ou cancele a " -"análise atual." +"Uma análise de vídeo já está em andamento. Por favor, aguarde ou cancele " +"a análise atual." # | msgid "Project Loaded" # | msgid "No Project Loaded" @@ -470,7 +470,7 @@ 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/wizard/detection_step.py:223 +#: src/zebtrack/ui/wizard/detection_step.py:221 msgid "No video selected." msgstr "Nenhum vídeo selecionado." @@ -527,19 +527,19 @@ msgstr "Escopo: Projeto" #: src/zebtrack/coordinators/calibration_coordinator.py:113 msgid "" -"This project uses saved overrides. Changes in this window are persisted only" -" in this project." +"This project uses saved overrides. Changes in this window are persisted " +"only in this project." msgstr "" "Este projeto usa overrides salvos. Ajustes nesta janela são persistidos " "apenas neste projeto." #: src/zebtrack/coordinators/calibration_coordinator.py:118 msgid "" -"This project inherits the global defaults. Saving here turns the values into" -" project-specific overrides." +"This project inherits the global defaults. Saving here turns the values " +"into project-specific overrides." msgstr "" -"Este projeto está herdando os padrões globais. Ao salvar aqui, os valores se" -" tornam overrides específicos." +"Este projeto está herdando os padrões globais. Ao salvar aqui, os valores" +" se tornam overrides específicos." # | msgid "Global Model Configuration..." # | msgid "Model Configuration" @@ -550,11 +550,11 @@ msgstr "Escopo: Configuração Global" #: src/zebtrack/coordinators/calibration_coordinator.py:125 msgid "" -"Changes update the global default. Use the copy action to pin these values " -"to the current project." +"Changes update the global default. Use the copy action to pin these " +"values to the current project." msgstr "" -"Alterações atualizam o padrão global. Use a ação de cópia para fixar estes " -"valores no projeto atual." +"Alterações atualizam o padrão global. Use a ação de cópia para fixar " +"estes valores no projeto atual." #: src/zebtrack/coordinators/calibration_coordinator.py:129 msgid "No project loaded; changes update the global defaults." @@ -597,8 +597,8 @@ msgid "" "Every selected video already has analysis data. Do you want to reprocess " "them all?" msgstr "" -"Todos os vídeos selecionados já possuem dados de análise. Deseja reprocessá-" -"los todos?" +"Todos os vídeos selecionados já possuem dados de análise. Deseja " +"reprocessá-los todos?" # | msgid "Video not found" # | msgid "Main arena not found." @@ -751,6 +751,7 @@ 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/wizard/design_editor_dialog.py:509 msgid "Validation Error" msgstr "Erro de Validação" @@ -763,21 +764,30 @@ msgstr "Ajustar Polígono" #: src/zebtrack/coordinators/live_calibration_coordinator.py:352 msgid "" -"Polygon reused. When you close this notice, the Zone Configuration tab opens with ALL vertices already SELECTED (in red).\n" +"Polygon reused. When you close this notice, the Zone Configuration tab " +"opens with ALL vertices already SELECTED (in red).\n" "\n" -"To move the WHOLE polygon at once, click and DRAG over the area or over any of its lines (or over one of the red vertices). The whole polygon moves together.\n" +"To move the WHOLE polygon at once, click and DRAG over the area or over " +"any of its lines (or over one of the red vertices). The whole polygon " +"moves together.\n" "\n" -"If the selection disappears, drag the mouse to form a rectangle over the whole polygon area to select every vertex again.\n" +"If the selection disappears, drag the mouse to form a rectangle over the " +"whole polygon area to select every vertex again.\n" "\n" "Then click 'Save Edit' and 'Start Recording' when you are ready." msgstr "" -"Polígono reutilizado. Ao fechar este aviso, a aba de Configuração de Zonas será aberta com TODOS os vértices já SELECIONADOS (em vermelho).\n" +"Polígono reutilizado. Ao fechar este aviso, a aba de Configuração de " +"Zonas será aberta com TODOS os vértices já SELECIONADOS (em vermelho).\n" "\n" -"Para mover o polígono INTEIRO de uma vez, clique e ARRASTE sobre a área ou sobre qualquer linha do polígono (ou sobre um dos vértices em vermelho). Todo o polígono se move junto.\n" +"Para mover o polígono INTEIRO de uma vez, clique e ARRASTE sobre a área " +"ou sobre qualquer linha do polígono (ou sobre um dos vértices em " +"vermelho). Todo o polígono se move junto.\n" "\n" -"Se a seleção sumir, arraste o mouse formando um retângulo sobre toda a área do polígono para selecionar todos os vértices de novo.\n" +"Se a seleção sumir, arraste o mouse formando um retângulo sobre toda a " +"área do polígono para selecionar todos os vértices de novo.\n" "\n" -"Depois clique em 'Salvar Edição' e em 'Iniciar Gravação' quando estiver pronto." +"Depois clique em 'Salvar Edição' e em 'Iniciar Gravação' quando estiver " +"pronto." # | msgid "🐠 Aquarium (Detection)" #: src/zebtrack/coordinators/live_calibration_coordinator.py:629 @@ -972,8 +982,7 @@ msgstr "Análise ao vivo em andamento." #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1885 #, python-brace-format msgid "Analysing camera {camera} (analysis: {analysis}f, display: {display}f)" -msgstr "" -"Analisando câmera {camera} (análise: {analysis}f, exibição: {display}f)" +msgstr "Analisando câmera {camera} (análise: {analysis}f, exibição: {display}f)" #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1900 #, python-brace-format @@ -989,27 +998,38 @@ msgstr " (porta {port})" #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1970 #, python-brace-format msgid "" -"This project uses External Trigger Mode, but the Arduino is not connected{port}.\n" +"This project uses External Trigger Mode, but the Arduino is not " +"connected{port}.\n" "\n" -"The recording was NOT started: it would wait forever for a signal that has no way to arrive.\n" +"The recording was NOT started: it would wait forever for a signal that " +"has no way to arrive.\n" "\n" -"Check the cable, whether the port is in use by another program (Arduino IDE, serial monitor), and reopen the project to reconnect." +"Check the cable, whether the port is in use by another program (Arduino " +"IDE, serial monitor), and reopen the project to reconnect." msgstr "" -"Este projeto usa Modo de Gatilho Externo, mas o Arduino não está conectado{port}.\n" +"Este projeto usa Modo de Gatilho Externo, mas o Arduino não está " +"conectado{port}.\n" "\n" -"A gravação NÃO foi iniciada: ela ficaria esperando para sempre um sinal que não tem por onde chegar.\n" +"A gravação NÃO foi iniciada: ela ficaria esperando para sempre um sinal " +"que não tem por onde chegar.\n" "\n" -"Verifique o cabo, se a porta não está em uso por outro programa (IDE do Arduino, monitor serial) e reabra o projeto para reconectar." +"Verifique o cabo, se a porta não está em uso por outro programa (IDE do " +"Arduino, monitor serial) e reabra o projeto para reconectar." #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1980 msgid "" "This project has External Trigger Mode on, but no Arduino configured.\n" "\n" -"The recording was NOT started: starting now would ignore the external signal the protocol expects. Configure the Arduino or turn the external trigger off in the project settings." +"The recording was NOT started: starting now would ignore the external " +"signal the protocol expects. Configure the Arduino or turn the external " +"trigger off in the project settings." msgstr "" -"Este projeto está com o Modo de Gatilho Externo ligado, mas sem Arduino configurado.\n" +"Este projeto está com o Modo de Gatilho Externo ligado, mas sem Arduino " +"configurado.\n" "\n" -"A gravação NÃO foi iniciada: começar agora ignoraria o sinal externo que o protocolo espera. Configure o Arduino ou desligue o gatilho externo nas configurações do projeto." +"A gravação NÃO foi iniciada: começar agora ignoraria o sinal externo que " +"o protocolo espera. Configure o Arduino ou desligue o gatilho externo nas" +" configurações do projeto." # | msgid "Action unavailable" #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1990 @@ -1035,11 +1055,13 @@ msgstr "Câmera não encontrada" msgid "" "The camera saved in the project ('{name}') was not detected.\n" "\n" -"Connect the right device or use 'Change camera' in the recording dialog to pick another one." +"Connect the right device or use 'Change camera' in the recording dialog " +"to pick another one." msgstr "" "A câmera salva no projeto ('{name}') não foi detectada.\n" "\n" -"Conecte o dispositivo correto ou use 'Trocar câmera' no diálogo de gravação para escolher outra." +"Conecte o dispositivo correto ou use 'Trocar câmera' no diálogo de " +"gravação para escolher outra." # | msgid "Loading settings..." # | msgid "Global Diagnostics..." @@ -1058,11 +1080,11 @@ msgstr "Converter Modelo?" #: src/zebtrack/coordinators/model_diagnostics_coordinator.py:270 msgid "" -"The OpenVINO model was not found or is incomplete. Do you want to convert it" -" now?" +"The OpenVINO model was not found or is incomplete. Do you want to convert" +" it now?" msgstr "" -"O modelo OpenVINO não foi encontrado ou está incompleto. Deseja convertê-lo " -"agora?" +"O modelo OpenVINO não foi encontrado ou está incompleto. Deseja " +"convertê-lo agora?" #: src/zebtrack/coordinators/model_diagnostics_coordinator.py:300 msgid "Conversion to OpenVINO failed." @@ -1214,7 +1236,8 @@ msgstr ", Máscara: {count} pontos" #: src/zebtrack/coordinators/model_diagnostics_coordinator.py:863 #, python-brace-format msgid "" -" - Class {class_id} ('{class_name}'), Conf: {conf}, BBox: {bbox}{mask_info}" +" - Class {class_id} ('{class_name}'), Conf: {conf}, BBox: " +"{bbox}{mask_info}" msgstr "" " - Classe {class_id} ('{class_name}'), Conf: {conf}, BBox: " "{bbox}{mask_info}" @@ -1222,8 +1245,8 @@ msgstr "" #: src/zebtrack/coordinators/model_diagnostics_coordinator.py:835 #, python-brace-format msgid "" -" - [MASK WITHOUT BOX] Probable Aquarium, approx. BBox: [{x_min}, {y_min}, " -"{x_max}, {y_max}], Area: {area}, Points: {points}" +" - [MASK WITHOUT BOX] Probable Aquarium, approx. BBox: [{x_min}, " +"{y_min}, {x_max}, {y_max}], Area: {area}, Points: {points}" msgstr "" " - [MÁSCARA SEM BOX] Provável Aquário, BBox aprox: [{x_min}, {y_min}, " "{x_max}, {y_max}], Área: {area}, Pontos: {points}" @@ -1357,10 +1380,8 @@ msgstr "vídeo" #: src/zebtrack/coordinators/progress_tracking_coordinator.py:718 #, python-brace-format -msgid "" -"Processing {name}: {done}/{total} ({pct}%) - {detected} detections{eta}" -msgstr "" -"Processando {name}: {done}/{total} ({pct}%) - {detected} detecções{eta}" +msgid "Processing {name}: {done}/{total} ({pct}%) - {detected} detections{eta}" +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 @@ -1384,11 +1405,11 @@ msgstr "Iniciando gravação..." #: src/zebtrack/coordinators/recording_session_coordinator.py:519 msgid "" -"The Arduino is not connected — check the cable and whether the port is in " -"use by another program." +"The Arduino is not connected — check the cable and whether the port is in" +" use by another program." msgstr "" -"O Arduino não está conectado — verifique o cabo e se a porta não está em uso" -" por outro programa." +"O Arduino não está conectado — verifique o cabo e se a porta não está em " +"uso por outro programa." #: src/zebtrack/coordinators/recording_session_coordinator.py:523 msgid "External trigger mode requires a configured Arduino." @@ -1651,6 +1672,7 @@ 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/wizard/file_selection_step.py:239 msgid "Video files" msgstr "Arquivos de vídeo" @@ -1662,8 +1684,7 @@ msgstr "Pastas" #: src/zebtrack/coordinators/video_processing_coordinator.py:397 #: src/zebtrack/coordinators/video_processing_coordinator.py:502 msgid "No new video file was found in the selected paths." -msgstr "" -"Nenhum novo arquivo de vídeo foi encontrado nos caminhos selecionados." +msgstr "Nenhum novo arquivo de vídeo foi encontrado nos caminhos selecionados." # | msgid "No videos listed" #: src/zebtrack/coordinators/video_processing_coordinator.py:408 @@ -1826,10 +1847,8 @@ msgid "Finalizing startup..." msgstr "Finalizando inicialização..." #: src/zebtrack/core/application_bootstrapper.py:425 -msgid "" -"Recommended, but the model is not converted. Use 'Diagnostics' to convert." -msgstr "" -"Recomendado mas modelo não convertido. Use 'Diagnóstico' para converter." +msgid "Recommended, but the model is not converted. Use 'Diagnostics' to convert." +msgstr "Recomendado mas modelo não convertido. Use 'Diagnóstico' para converter." # | msgid "Select the Video for Diagnostics" # | msgid "📹 Select Video for Drawing" @@ -1861,8 +1880,7 @@ msgstr "Selecione ao menos arena ou ROIs para salvar." #: 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 "" -"Não é possível salvar o template no projeto atual: projeto não carregado." +msgstr "Não é possível salvar o template no projeto atual: projeto não carregado." # | msgid "Template '{name}' applied to the video being edited." #: src/zebtrack/core/project/asset_manager.py:259 @@ -1903,10 +1921,11 @@ msgstr "Template de ROI '{name}' não encontrado para o contexto solicitado." #: src/zebtrack/core/project/asset_manager.py:718 msgid "" -"Remove the reports and summaries before deleting the arena, the ROIs or the " -"trajectory." +"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." +"Remova os relatórios e sumários antes de apagar arena, ROIs ou " +"trajetórias." # | msgid "Remove the trajectory generated for this video?" #: src/zebtrack/core/project/asset_manager.py:728 @@ -1940,18 +1959,20 @@ msgstr "Remova relatórios e sumários antes de excluir o vídeo." #: src/zebtrack/core/project/asset_manager.py:749 msgid "" -"Remove the arena, the ROIs and the trajectory before deleting the video from" -" the project." +"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/core/project/project_lifecycle_manager.py:73 #, python-brace-format msgid "" -"Project configuration file '{filename}' not found in the selected directory: {path}\n" +"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" +"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." @@ -1964,7 +1985,8 @@ msgid "" "\n" "Error: {error}" msgstr "" -"Falha ao carregar ou analisar o arquivo de configuração do projeto: {path}\n" +"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" @@ -2061,15 +2083,19 @@ msgstr "Vídeo não encontrado no projeto." #: src/zebtrack/core/viewmodels/analysis_control_view_model.py:92 #, python-brace-format msgid "" -"The detection mode (det) for animals is only compatible with 1 animal per aquarium.\n" +"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." +"To use multiple animals per aquarium, change the animal detection method " +"to 'seg' (segmentation) in the settings." msgstr "" -"O modo de detecção (det) para animais só é compatível com 1 animal por aquário.\n" +"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." +"Para usar múltiplos animais por aquário, altere o método de detecção de " +"animais para 'seg' (segmentação) nas configurações." # | msgid "Default?" #: src/zebtrack/core/project/project_workflow_service.py:1184 @@ -2239,8 +2265,7 @@ msgstr " • Use a busca para localizar vídeos rapidamente" #: 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" +msgstr " • Os símbolos de status indicam arenas, ROIs e trajetórias disponíveis" #: src/zebtrack/core/project/project_workflow_service.py:1375 msgid " • Adjust the zones before processing if necessary" @@ -2403,8 +2428,8 @@ msgstr "Gravação Concluída" #: src/zebtrack/core/recording/live_camera_mode.py:132 #, python-brace-format msgid "" -"The system supports {count} aquariums simultaneously. Real-time processing " -"enabled." +"The system supports {count} aquariums simultaneously. Real-time " +"processing enabled." msgstr "" "Sistema suporta {count} aquários simultaneamente. Processamento em tempo " "real habilitado." @@ -2416,11 +2441,11 @@ msgstr "Gravar sem detecção (melhor qualidade, processar depois)" #: src/zebtrack/core/recording/live_camera_mode.py:167 #, python-brace-format msgid "" -"The system supports only {supported} of the {requested} aquariums requested " -"for simultaneous processing. " +"The system supports only {supported} of the {requested} aquariums " +"requested for simultaneous processing. " msgstr "" -"O sistema suporta apenas {supported} dos {requested} aquários solicitados " -"para processamento simultâneo. " +"O sistema suporta apenas {supported} dos {requested} aquários solicitados" +" para processamento simultâneo. " #: src/zebtrack/core/recording/live_camera_mode.py:172 #, python-brace-format @@ -2467,11 +2492,11 @@ msgstr "Máximo suportado: {count} aquários." # | video " "only and process offline." #: src/zebtrack/core/recording/live_camera_mode.py:228 msgid "" -"The system is not sufficient for real-time processing. Recommended: record " -"the video and process it offline." +"The system is not sufficient for real-time processing. Recommended: " +"record the video and process it offline." msgstr "" -"Sistema insuficiente para processamento em tempo real. Recomendado: gravar " -"vídeo e processar offline." +"Sistema insuficiente para processamento em tempo real. Recomendado: " +"gravar vídeo e processar offline." #: src/zebtrack/core/recording/live_camera_mode.py:233 msgid "⚠️ INSUFFICIENT HARDWARE for real-time detection." @@ -2579,14 +2604,14 @@ msgid "" "⚠️ Video: 1 frame dropped — check the disk / OneDrive (the recording may " "have gaps)" msgstr "" -"⚠️ Vídeo: 1 frame descartado — verifique o disco / OneDrive (gravação pode " -"ter falhas)" +"⚠️ Vídeo: 1 frame descartado — verifique o disco / OneDrive (gravação " +"pode ter falhas)" #: src/zebtrack/core/recording/live_session_manager.py:1123 #, python-brace-format msgid "" -"⚠️ Video: {count} frames dropped — check the disk / OneDrive (the recording " -"may have gaps)" +"⚠️ Video: {count} frames dropped — check the disk / OneDrive (the " +"recording may have gaps)" msgstr "" "⚠️ Vídeo: {count} frames descartados — verifique o disco / OneDrive " "(gravação pode ter falhas)" @@ -2603,10 +2628,8 @@ msgstr "⏳ Análise atrasada ({seconds:.1f}s) - Gravação continua normalmente #: src/zebtrack/core/recording/live_session_manager.py:1157 #, python-brace-format -msgid "" -"⚠️ Analysis is far behind ({seconds:.1f}s) - Recording OK, analysis queued" -msgstr "" -"⚠️ Análise muito atrasada ({seconds:.1f}s) - Gravação OK, análise em fila" +msgid "⚠️ Analysis is far behind ({seconds:.1f}s) - Recording OK, analysis queued" +msgstr "⚠️ Análise muito atrasada ({seconds:.1f}s) - Gravação OK, análise em fila" #: src/zebtrack/core/recording/recording_service.py:155 msgid "Camera configuration unavailable to start the recording." @@ -2621,8 +2644,8 @@ msgstr "Não foi possível iniciar a gravação." #: src/zebtrack/core/services/arduino_ack_semantics.py:84 #, python-brace-format msgid "" -"{roi} enter → token {token} → the firmware replied \"{ack}\", but an enter " -"should switch it ON" +"{roi} enter → token {token} → the firmware replied \"{ack}\", but an " +"enter should switch it ON" msgstr "" "{roi} entrada → token {token} → o firmware respondeu \"{ack}\", mas uma " "entrada deveria ligar" @@ -2633,8 +2656,8 @@ msgid "" "{roi} exit → token {token} → the firmware replied \"{ack}\", but an exit " "should switch it OFF" msgstr "" -"{roi} saída → token {token} → o firmware respondeu \"{ack}\", mas uma saída " -"deveria desligar" +"{roi} saída → token {token} → o firmware respondeu \"{ack}\", mas uma " +"saída deveria desligar" #: src/zebtrack/core/services/arduino_bindings.py:54 #, python-brace-format @@ -2656,11 +2679,10 @@ msgstr "Não foi possível encontrar o peso correspondente ao caminho: {path}" # | msgid "" "The OpenVINO model directory does not contain the required .xml # | files. " "Please reconvert the model." #: src/zebtrack/core/services/detector_service.py:156 -msgid "" -"OpenVINO model path not found or invalid. Please convert the model first." +msgid "OpenVINO model path not found or invalid. Please convert the model first." msgstr "" -"Caminho do modelo OpenVINO não encontrado ou inválido. Por favor, converta o" -" modelo primeiro." +"Caminho do modelo OpenVINO não encontrado ou inválido. Por favor, " +"converta o modelo primeiro." #: src/zebtrack/core/services/detector_service.py:164 msgid "YOLO .pt model path not found or invalid." @@ -2937,11 +2959,11 @@ msgstr "Exatamente 2 aquários devem ser configurados" #: src/zebtrack/core/services/wizard_service.py:920 #, python-brace-format msgid "" -"Aquarium {index} has a very small area ({area} px²). It may affect detection" -" accuracy." +"Aquarium {index} has a very small area ({area} px²). It may affect " +"detection accuracy." msgstr "" -"Aquário {index} tem área muito pequena ({area} px²). Pode afetar precisão da" -" detecção." +"Aquário {index} tem área muito pequena ({area} px²). Pode afetar precisão" +" da detecção." #: src/zebtrack/core/services/wizard_service.py:949 msgid "" @@ -3074,8 +3096,7 @@ msgid "Critical Error" msgstr "Erro Crítico" #: src/zebtrack/core/viewmodels/main_view_model_runtime.py:527 -msgid "" -"The camera thread did not shut down correctly. The application will close." +msgid "The camera thread did not shut down correctly. The application will close." msgstr "" "A thread da câmera não foi finalizada corretamente. O aplicativo será " "encerrado." @@ -3155,8 +3176,7 @@ msgstr "Valores restaurados para refletir as configurações atuais." #: src/zebtrack/ui/gui.py:809 msgid "This project is not configured for live experimental tracking." -msgstr "" -"Este projeto não está configurado para rastreamento experimental ao vivo." +msgstr "Este projeto não está configurado para rastreamento experimental ao vivo." # | msgid "Failed to open folder" #: src/zebtrack/ui/components/dialog_manager.py:824 src/zebtrack/ui/gui.py:840 @@ -3217,6 +3237,7 @@ msgstr "Não informado" #: src/zebtrack/ui/components/dialog_manager.py:284 #: src/zebtrack/ui/components/zone_controls.py:26 #: src/zebtrack/ui/sentinels.py:57 +#: src/zebtrack/ui/wizard/custom_regex_dialog.py:38 msgid "Day" msgstr "Dia" @@ -3369,10 +3390,9 @@ msgstr "Aviso" #: src/zebtrack/ui/builders/analysis_widgets.py:559 #, python-brace-format msgid "" -"Global settings were saved, but the current project could not be updated: " -"{error}" -msgstr "" -"Configuração global salva, mas erro ao atualizar projeto atual: {error}" +"Global settings were saved, but the current project could not be updated:" +" {error}" +msgstr "Configuração global salva, mas erro ao atualizar projeto atual: {error}" #: src/zebtrack/ui/builders/button_factory.py:36 msgid "Project Actions" @@ -3413,11 +3433,14 @@ msgstr "↷ Refazer (Ctrl+Y)" #: src/zebtrack/ui/builders/common_widgets.py:49 #: src/zebtrack/ui/components/validation_manager.py:1007 +#: src/zebtrack/ui/wizard/import_config_step.py:160 msgid "Trajectory" msgstr "Trajetória" #: src/zebtrack/ui/builders/common_widgets.py:52 #: src/zebtrack/ui/components/validation_manager.py:1009 +#: src/zebtrack/ui/wizard/file_selection_step.py:204 +#: src/zebtrack/ui/wizard/import_config_step.py:215 msgid "Summary" msgstr "Sumário" @@ -3434,8 +3457,8 @@ 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." +"indica vídeos prontos para gerar trajetórias (arena e ROIs salvos). O " +"valor mostra quantos ainda aguardam processamento." #: src/zebtrack/ui/builders/common_widgets.py:180 msgid "Weight Type" @@ -3454,6 +3477,8 @@ msgid "Detection (for fast bounding boxes)" msgstr "Detecção (para caixas delimitadoras rápidas)" #: src/zebtrack/ui/builders/common_widgets.py:223 +#: src/zebtrack/ui/wizard/custom_regex_dialog.py:472 +#: src/zebtrack/ui/wizard/design_editor_dialog.py:269 #: src/zebtrack/ui/wizard/wizard_dialog.py:207 msgid "Cancel" msgstr "Cancelar" @@ -3522,8 +3547,9 @@ msgstr "" #: 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." +"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." @@ -3538,13 +3564,13 @@ msgstr "" #: 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." +"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." +"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/ui/builders/zone_control_builder.py:118 msgid "" @@ -3556,19 +3582,20 @@ msgstr "" #: src/zebtrack/ui/builders/zone_control_builder.py:124 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: " +"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_

stP)oEOwf5&xp}miqY36*ki?X94ln1qh zWl+go(Y;r_++n@}M< ziv95l4#1eA_MmmB&wYg2HJ5P}W-n$D+luPvOAP+^e_p4c&DAE@n_uCzs07- zGipN1a1vfA&;BpO17a(L1izDwt7xyu5Abhp@Kg#3{$UuTQ06L(a8%U<05ifN;fI5(V zK_%H8)OO2U%Q`IMS{Jp?+oN{FC{!eupa!%ZefTA+|36U?%~RXjYa$0w!0ACj4;+N0 zSi4l@pL00N>sSPq)U%myMRo9%>)&{s`wG;z&tGa_k^2+-bDyW74LBAxk&&nf%tam1 zTY?7oRS1RT5IV--$<^388id*|<54qOiVErbt|zb^^&fE&mTzL)>JaL!c^AjvQ`B}& zZ5k5%tKK_s81<&jLY#p--`Pz;S)9MQ`z6%%b=1h0phms}b(DUA8rZLH`wLX)i?pyv zR72%TBh>xzs3Ur!TVH_T)VE^r-~XPXpqI!s)Qt~N56Ijy#Q6zJVN>+BvH`U~bua)G z$|}AQzQt^f_5R!A==M%wEh=&V*e}KcXqN4kD+FA9X0dks0S46 zY*}9p^?>%M0rW@Ba5Cy`_7>_S-Ga)Mv+ngrsEFk0VxOyl8eoqu?0I||?RFgX^TD5}0Y5?|ZT7CVZHu6`RTNgn&Zs4ug4u92rr}l$o*&&TV&ei7G=urL zl5F0ByQ#P6VcY5n>U!aqEcvRT_IC@no{E~;Obo6mYAH^kK6e$hl>ed*utGg8naiRg z)Veu^T@;3*_Ic4>AeJ^A}(q@3CjT-0hO%{fWM&3IF-jo(I21XGo0q`>OgzCB*xai6E0o zPD&h0bbPTX{-juStf3O~IQmXV8XW1gN{UMy$*0E0C67!>onW6G@&EPVJ&}K`$*||s zyW=u@>h7uiG-t+&gXkmP_sgWk|Fuwh+JE92T&2_$#xpoJAt|kn*YiAQTID>RuhQl5 zjgC!7jf{+RIwtyKNBHYzsk&@IK~IUa5(PbnGNyfA-1AZPw22ixy)xGylHgB?O^F+l z5Eq;1OH3dcwDxhfvccIUr22=A@C9=yHYrtE)6O zeodFPQHnp7RMBdt4Xx){kuAf^3B!l``D2!rp4#aOrlcxv47E#YZ0g7nE_KrywDm-I z%K648jT|=6pE9AYFP>a^jYUsip)Cuz~=^TYBuzpPy3Ycw)7`9JxZwraa)bZKu?^|S)tda@Vqr9Bjz;v>aa z+vLQf+u21V?x^Irqg#B0IzE=>cz?>#b-q~bpQGz4B_^|jMh16KLaILNi}#Q4Ck>%! z+h>@ctc_k4e%G@htkEkYkFHB$FW6p+jZgmX%@mswz3k{+&xL~j*-yG0mpqKc8I};6 zk~Zsa&(4s-onyxx-H_~KBu6JJzW>=(i*_x}m)7=yC!c58ol+tBb8|=Rf987h-J=`Q z`aSdv&sL$?2sYRdF4A|3Z+L7(m_y?Cvs>C+$>-_x(~uo5$*YvOc)Y)m8F; zHr%pFWxWRq2Y1iWJ=%21KL4=)Y`WyMdgZ)ZJ=qycVtH@7lEwbF&$LKP z9!MH8;b1Z)r?^NeW)`gm7mA;Go|@@C3Z+@HiANlnq>1ox-%!0$_T zS83VgT;BX?Zzp-D6v3J)t@xN_sU4it)V2Lojb89kxpm-sDu&zWeZMw z^DWD}+51U&voZd-k*VxuvP_5Ce=W;@$M)OJ-j3+fdgF3rYQ?#!d8Lgx z=$(oBq;)##T~wcN tUV7pEr(FKIzGiGwMe~1bmt|j<3mLQQ;ayKy-v4vcYJ5$~hg2{7zW|z`WxN0Y delta 30636 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 F{{g(aJPZH; diff --git a/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.po b/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.po index 724ce761..6145ff94 100644 --- a/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.po +++ b/src/zebtrack/locales/pt_BR/LC_MESSAGES/zebtrack.po @@ -8,35 +8,33 @@ msgid "" msgstr "" "Project-Id-Version: DRerio LogAI VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-08-14 16:09-0300\n" +"POT-Creation-Date: 2026-08-14 16:24-0300\n" "PO-Revision-Date: 2026-08-08 21:41-0300\n" "Last-Translator: FULL NAME \n" -"Language: pt_BR\n" "Language-Team: pt_BR \n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" +"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." +"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." +"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 @@ -98,17 +96,19 @@ 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 " +"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 " +"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." +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 @@ -117,11 +117,11 @@ 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." +"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." +"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." @@ -189,8 +189,8 @@ msgid "" "Aquarium {number} has no subject defined. Configure the aquariums before " "processing." msgstr "" -"Aquário {number} não tem sujeito definido. Configure os aquários antes de" -" processar." +"Aquário {number} não tem sujeito definido. Configure os aquários antes de " +"processar." # | msgid "Analyze Single Video" #: src/zebtrack/coordinators/_single_video_mixin.py:220 @@ -440,8 +440,8 @@ msgid "" "A video analysis is already running. Please wait or cancel the current " "analysis." msgstr "" -"Uma análise de vídeo já está em andamento. Por favor, aguarde ou cancele " -"a análise atual." +"Uma análise de vídeo já está em andamento. Por favor, aguarde ou cancele a " +"análise atual." # | msgid "Project Loaded" # | msgid "No Project Loaded" @@ -527,19 +527,19 @@ msgstr "Escopo: Projeto" #: src/zebtrack/coordinators/calibration_coordinator.py:113 msgid "" -"This project uses saved overrides. Changes in this window are persisted " -"only in this project." +"This project uses saved overrides. Changes in this window are persisted only" +" in this project." msgstr "" "Este projeto usa overrides salvos. Ajustes nesta janela são persistidos " "apenas neste projeto." #: src/zebtrack/coordinators/calibration_coordinator.py:118 msgid "" -"This project inherits the global defaults. Saving here turns the values " -"into project-specific overrides." +"This project inherits the global defaults. Saving here turns the values into" +" project-specific overrides." msgstr "" -"Este projeto está herdando os padrões globais. Ao salvar aqui, os valores" -" se tornam overrides específicos." +"Este projeto está herdando os padrões globais. Ao salvar aqui, os valores se" +" tornam overrides específicos." # | msgid "Global Model Configuration..." # | msgid "Model Configuration" @@ -550,11 +550,11 @@ msgstr "Escopo: Configuração Global" #: src/zebtrack/coordinators/calibration_coordinator.py:125 msgid "" -"Changes update the global default. Use the copy action to pin these " -"values to the current project." +"Changes update the global default. Use the copy action to pin these values " +"to the current project." msgstr "" -"Alterações atualizam o padrão global. Use a ação de cópia para fixar " -"estes valores no projeto atual." +"Alterações atualizam o padrão global. Use a ação de cópia para fixar estes " +"valores no projeto atual." #: src/zebtrack/coordinators/calibration_coordinator.py:129 msgid "No project loaded; changes update the global defaults." @@ -597,8 +597,8 @@ msgid "" "Every selected video already has analysis data. Do you want to reprocess " "them all?" msgstr "" -"Todos os vídeos selecionados já possuem dados de análise. Deseja " -"reprocessá-los todos?" +"Todos os vídeos selecionados já possuem dados de análise. Deseja reprocessá-" +"los todos?" # | msgid "Video not found" # | msgid "Main arena not found." @@ -764,30 +764,21 @@ msgstr "Ajustar Polígono" #: src/zebtrack/coordinators/live_calibration_coordinator.py:352 msgid "" -"Polygon reused. When you close this notice, the Zone Configuration tab " -"opens with ALL vertices already SELECTED (in red).\n" +"Polygon reused. When you close this notice, the Zone Configuration tab opens with ALL vertices already SELECTED (in red).\n" "\n" -"To move the WHOLE polygon at once, click and DRAG over the area or over " -"any of its lines (or over one of the red vertices). The whole polygon " -"moves together.\n" +"To move the WHOLE polygon at once, click and DRAG over the area or over any of its lines (or over one of the red vertices). The whole polygon moves together.\n" "\n" -"If the selection disappears, drag the mouse to form a rectangle over the " -"whole polygon area to select every vertex again.\n" +"If the selection disappears, drag the mouse to form a rectangle over the whole polygon area to select every vertex again.\n" "\n" "Then click 'Save Edit' and 'Start Recording' when you are ready." msgstr "" -"Polígono reutilizado. Ao fechar este aviso, a aba de Configuração de " -"Zonas será aberta com TODOS os vértices já SELECIONADOS (em vermelho).\n" +"Polígono reutilizado. Ao fechar este aviso, a aba de Configuração de Zonas será aberta com TODOS os vértices já SELECIONADOS (em vermelho).\n" "\n" -"Para mover o polígono INTEIRO de uma vez, clique e ARRASTE sobre a área " -"ou sobre qualquer linha do polígono (ou sobre um dos vértices em " -"vermelho). Todo o polígono se move junto.\n" +"Para mover o polígono INTEIRO de uma vez, clique e ARRASTE sobre a área ou sobre qualquer linha do polígono (ou sobre um dos vértices em vermelho). Todo o polígono se move junto.\n" "\n" -"Se a seleção sumir, arraste o mouse formando um retângulo sobre toda a " -"área do polígono para selecionar todos os vértices de novo.\n" +"Se a seleção sumir, arraste o mouse formando um retângulo sobre toda a área do polígono para selecionar todos os vértices de novo.\n" "\n" -"Depois clique em 'Salvar Edição' e em 'Iniciar Gravação' quando estiver " -"pronto." +"Depois clique em 'Salvar Edição' e em 'Iniciar Gravação' quando estiver pronto." # | msgid "🐠 Aquarium (Detection)" #: src/zebtrack/coordinators/live_calibration_coordinator.py:629 @@ -982,7 +973,8 @@ msgstr "Análise ao vivo em andamento." #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1885 #, python-brace-format msgid "Analysing camera {camera} (analysis: {analysis}f, display: {display}f)" -msgstr "Analisando câmera {camera} (análise: {analysis}f, exibição: {display}f)" +msgstr "" +"Analisando câmera {camera} (análise: {analysis}f, exibição: {display}f)" #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1900 #, python-brace-format @@ -998,38 +990,27 @@ msgstr " (porta {port})" #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1970 #, python-brace-format msgid "" -"This project uses External Trigger Mode, but the Arduino is not " -"connected{port}.\n" +"This project uses External Trigger Mode, but the Arduino is not connected{port}.\n" "\n" -"The recording was NOT started: it would wait forever for a signal that " -"has no way to arrive.\n" +"The recording was NOT started: it would wait forever for a signal that has no way to arrive.\n" "\n" -"Check the cable, whether the port is in use by another program (Arduino " -"IDE, serial monitor), and reopen the project to reconnect." +"Check the cable, whether the port is in use by another program (Arduino IDE, serial monitor), and reopen the project to reconnect." msgstr "" -"Este projeto usa Modo de Gatilho Externo, mas o Arduino não está " -"conectado{port}.\n" +"Este projeto usa Modo de Gatilho Externo, mas o Arduino não está conectado{port}.\n" "\n" -"A gravação NÃO foi iniciada: ela ficaria esperando para sempre um sinal " -"que não tem por onde chegar.\n" +"A gravação NÃO foi iniciada: ela ficaria esperando para sempre um sinal que não tem por onde chegar.\n" "\n" -"Verifique o cabo, se a porta não está em uso por outro programa (IDE do " -"Arduino, monitor serial) e reabra o projeto para reconectar." +"Verifique o cabo, se a porta não está em uso por outro programa (IDE do Arduino, monitor serial) e reabra o projeto para reconectar." #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1980 msgid "" "This project has External Trigger Mode on, but no Arduino configured.\n" "\n" -"The recording was NOT started: starting now would ignore the external " -"signal the protocol expects. Configure the Arduino or turn the external " -"trigger off in the project settings." +"The recording was NOT started: starting now would ignore the external signal the protocol expects. Configure the Arduino or turn the external trigger off in the project settings." msgstr "" -"Este projeto está com o Modo de Gatilho Externo ligado, mas sem Arduino " -"configurado.\n" +"Este projeto está com o Modo de Gatilho Externo ligado, mas sem Arduino configurado.\n" "\n" -"A gravação NÃO foi iniciada: começar agora ignoraria o sinal externo que " -"o protocolo espera. Configure o Arduino ou desligue o gatilho externo nas" -" configurações do projeto." +"A gravação NÃO foi iniciada: começar agora ignoraria o sinal externo que o protocolo espera. Configure o Arduino ou desligue o gatilho externo nas configurações do projeto." # | msgid "Action unavailable" #: src/zebtrack/coordinators/live_camera_session_coordinator.py:1990 @@ -1055,13 +1036,11 @@ msgstr "Câmera não encontrada" msgid "" "The camera saved in the project ('{name}') was not detected.\n" "\n" -"Connect the right device or use 'Change camera' in the recording dialog " -"to pick another one." +"Connect the right device or use 'Change camera' in the recording dialog to pick another one." msgstr "" "A câmera salva no projeto ('{name}') não foi detectada.\n" "\n" -"Conecte o dispositivo correto ou use 'Trocar câmera' no diálogo de " -"gravação para escolher outra." +"Conecte o dispositivo correto ou use 'Trocar câmera' no diálogo de gravação para escolher outra." # | msgid "Loading settings..." # | msgid "Global Diagnostics..." @@ -1080,11 +1059,11 @@ msgstr "Converter Modelo?" #: src/zebtrack/coordinators/model_diagnostics_coordinator.py:270 msgid "" -"The OpenVINO model was not found or is incomplete. Do you want to convert" -" it now?" +"The OpenVINO model was not found or is incomplete. Do you want to convert it" +" now?" msgstr "" -"O modelo OpenVINO não foi encontrado ou está incompleto. Deseja " -"convertê-lo agora?" +"O modelo OpenVINO não foi encontrado ou está incompleto. Deseja convertê-lo " +"agora?" #: src/zebtrack/coordinators/model_diagnostics_coordinator.py:300 msgid "Conversion to OpenVINO failed." @@ -1236,8 +1215,7 @@ msgstr ", Máscara: {count} pontos" #: src/zebtrack/coordinators/model_diagnostics_coordinator.py:863 #, python-brace-format msgid "" -" - Class {class_id} ('{class_name}'), Conf: {conf}, BBox: " -"{bbox}{mask_info}" +" - Class {class_id} ('{class_name}'), Conf: {conf}, BBox: {bbox}{mask_info}" msgstr "" " - Classe {class_id} ('{class_name}'), Conf: {conf}, BBox: " "{bbox}{mask_info}" @@ -1245,8 +1223,8 @@ msgstr "" #: src/zebtrack/coordinators/model_diagnostics_coordinator.py:835 #, python-brace-format msgid "" -" - [MASK WITHOUT BOX] Probable Aquarium, approx. BBox: [{x_min}, " -"{y_min}, {x_max}, {y_max}], Area: {area}, Points: {points}" +" - [MASK WITHOUT BOX] Probable Aquarium, approx. BBox: [{x_min}, {y_min}, " +"{x_max}, {y_max}], Area: {area}, Points: {points}" msgstr "" " - [MÁSCARA SEM BOX] Provável Aquário, BBox aprox: [{x_min}, {y_min}, " "{x_max}, {y_max}], Área: {area}, Pontos: {points}" @@ -1380,8 +1358,10 @@ msgstr "vídeo" #: src/zebtrack/coordinators/progress_tracking_coordinator.py:718 #, python-brace-format -msgid "Processing {name}: {done}/{total} ({pct}%) - {detected} detections{eta}" -msgstr "Processando {name}: {done}/{total} ({pct}%) - {detected} detecções{eta}" +msgid "" +"Processing {name}: {done}/{total} ({pct}%) - {detected} detections{eta}" +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 @@ -1405,11 +1385,11 @@ msgstr "Iniciando gravação..." #: src/zebtrack/coordinators/recording_session_coordinator.py:519 msgid "" -"The Arduino is not connected — check the cable and whether the port is in" -" use by another program." +"The Arduino is not connected — check the cable and whether the port is in " +"use by another program." msgstr "" -"O Arduino não está conectado — verifique o cabo e se a porta não está em " -"uso por outro programa." +"O Arduino não está conectado — verifique o cabo e se a porta não está em uso" +" por outro programa." #: src/zebtrack/coordinators/recording_session_coordinator.py:523 msgid "External trigger mode requires a configured Arduino." @@ -1672,7 +1652,7 @@ 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/wizard/file_selection_step.py:239 +#: src/zebtrack/ui/wizard/file_selection_step.py:240 msgid "Video files" msgstr "Arquivos de vídeo" @@ -1684,7 +1664,8 @@ msgstr "Pastas" #: src/zebtrack/coordinators/video_processing_coordinator.py:397 #: src/zebtrack/coordinators/video_processing_coordinator.py:502 msgid "No new video file was found in the selected paths." -msgstr "Nenhum novo arquivo de vídeo foi encontrado nos caminhos selecionados." +msgstr "" +"Nenhum novo arquivo de vídeo foi encontrado nos caminhos selecionados." # | msgid "No videos listed" #: src/zebtrack/coordinators/video_processing_coordinator.py:408 @@ -1847,8 +1828,10 @@ msgid "Finalizing startup..." msgstr "Finalizando inicialização..." #: src/zebtrack/core/application_bootstrapper.py:425 -msgid "Recommended, but the model is not converted. Use 'Diagnostics' to convert." -msgstr "Recomendado mas modelo não convertido. Use 'Diagnóstico' para converter." +msgid "" +"Recommended, but the model is not converted. Use 'Diagnostics' to convert." +msgstr "" +"Recomendado mas modelo não convertido. Use 'Diagnóstico' para converter." # | msgid "Select the Video for Diagnostics" # | msgid "📹 Select Video for Drawing" @@ -1880,7 +1863,8 @@ msgstr "Selecione ao menos arena ou ROIs para salvar." #: 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 "Não é possível salvar o template no projeto atual: projeto não carregado." +msgstr "" +"Não é possível salvar o template no projeto atual: projeto não carregado." # | msgid "Template '{name}' applied to the video being edited." #: src/zebtrack/core/project/asset_manager.py:259 @@ -1921,11 +1905,10 @@ msgstr "Template de ROI '{name}' não encontrado para o contexto solicitado." #: src/zebtrack/core/project/asset_manager.py:718 msgid "" -"Remove the reports and summaries before deleting the arena, the ROIs or " -"the trajectory." +"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." +"Remova os relatórios e sumários antes de apagar arena, ROIs ou trajetórias." # | msgid "Remove the trajectory generated for this video?" #: src/zebtrack/core/project/asset_manager.py:728 @@ -1959,20 +1942,18 @@ msgstr "Remova relatórios e sumários antes de excluir o vídeo." #: src/zebtrack/core/project/asset_manager.py:749 msgid "" -"Remove the arena, the ROIs and the trajectory before deleting the video " -"from the project." +"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/core/project/project_lifecycle_manager.py:73 #, python-brace-format msgid "" -"Project configuration file '{filename}' not found in the selected " -"directory: {path}\n" +"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" +"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." @@ -1985,8 +1966,7 @@ msgid "" "\n" "Error: {error}" msgstr "" -"Falha ao carregar ou analisar o arquivo de configuração do projeto: " -"{path}\n" +"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" @@ -2083,19 +2063,15 @@ msgstr "Vídeo não encontrado no projeto." #: src/zebtrack/core/viewmodels/analysis_control_view_model.py:92 #, python-brace-format msgid "" -"The detection mode (det) for animals is only compatible with 1 animal per" -" aquarium.\n" +"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." +"To use multiple animals per aquarium, change the animal detection method to 'seg' (segmentation) in the settings." msgstr "" -"O modo de detecção (det) para animais só é compatível com 1 animal por " -"aquário.\n" +"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." +"Para usar múltiplos animais por aquário, altere o método de detecção de animais para 'seg' (segmentação) nas configurações." # | msgid "Default?" #: src/zebtrack/core/project/project_workflow_service.py:1184 @@ -2265,7 +2241,8 @@ msgstr " • Use a busca para localizar vídeos rapidamente" #: 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" +msgstr "" +" • Os símbolos de status indicam arenas, ROIs e trajetórias disponíveis" #: src/zebtrack/core/project/project_workflow_service.py:1375 msgid " • Adjust the zones before processing if necessary" @@ -2428,8 +2405,8 @@ msgstr "Gravação Concluída" #: src/zebtrack/core/recording/live_camera_mode.py:132 #, python-brace-format msgid "" -"The system supports {count} aquariums simultaneously. Real-time " -"processing enabled." +"The system supports {count} aquariums simultaneously. Real-time processing " +"enabled." msgstr "" "Sistema suporta {count} aquários simultaneamente. Processamento em tempo " "real habilitado." @@ -2441,11 +2418,11 @@ msgstr "Gravar sem detecção (melhor qualidade, processar depois)" #: src/zebtrack/core/recording/live_camera_mode.py:167 #, python-brace-format msgid "" -"The system supports only {supported} of the {requested} aquariums " -"requested for simultaneous processing. " +"The system supports only {supported} of the {requested} aquariums requested " +"for simultaneous processing. " msgstr "" -"O sistema suporta apenas {supported} dos {requested} aquários solicitados" -" para processamento simultâneo. " +"O sistema suporta apenas {supported} dos {requested} aquários solicitados " +"para processamento simultâneo. " #: src/zebtrack/core/recording/live_camera_mode.py:172 #, python-brace-format @@ -2492,11 +2469,11 @@ msgstr "Máximo suportado: {count} aquários." # | video " "only and process offline." #: src/zebtrack/core/recording/live_camera_mode.py:228 msgid "" -"The system is not sufficient for real-time processing. Recommended: " -"record the video and process it offline." +"The system is not sufficient for real-time processing. Recommended: record " +"the video and process it offline." msgstr "" -"Sistema insuficiente para processamento em tempo real. Recomendado: " -"gravar vídeo e processar offline." +"Sistema insuficiente para processamento em tempo real. Recomendado: gravar " +"vídeo e processar offline." #: src/zebtrack/core/recording/live_camera_mode.py:233 msgid "⚠️ INSUFFICIENT HARDWARE for real-time detection." @@ -2604,14 +2581,14 @@ msgid "" "⚠️ Video: 1 frame dropped — check the disk / OneDrive (the recording may " "have gaps)" msgstr "" -"⚠️ Vídeo: 1 frame descartado — verifique o disco / OneDrive (gravação " -"pode ter falhas)" +"⚠️ Vídeo: 1 frame descartado — verifique o disco / OneDrive (gravação pode " +"ter falhas)" #: src/zebtrack/core/recording/live_session_manager.py:1123 #, python-brace-format msgid "" -"⚠️ Video: {count} frames dropped — check the disk / OneDrive (the " -"recording may have gaps)" +"⚠️ Video: {count} frames dropped — check the disk / OneDrive (the recording " +"may have gaps)" msgstr "" "⚠️ Vídeo: {count} frames descartados — verifique o disco / OneDrive " "(gravação pode ter falhas)" @@ -2628,8 +2605,10 @@ msgstr "⏳ Análise atrasada ({seconds:.1f}s) - Gravação continua normalmente #: src/zebtrack/core/recording/live_session_manager.py:1157 #, python-brace-format -msgid "⚠️ Analysis is far behind ({seconds:.1f}s) - Recording OK, analysis queued" -msgstr "⚠️ Análise muito atrasada ({seconds:.1f}s) - Gravação OK, análise em fila" +msgid "" +"⚠️ Analysis is far behind ({seconds:.1f}s) - Recording OK, analysis queued" +msgstr "" +"⚠️ Análise muito atrasada ({seconds:.1f}s) - Gravação OK, análise em fila" #: src/zebtrack/core/recording/recording_service.py:155 msgid "Camera configuration unavailable to start the recording." @@ -2644,8 +2623,8 @@ msgstr "Não foi possível iniciar a gravação." #: src/zebtrack/core/services/arduino_ack_semantics.py:84 #, python-brace-format msgid "" -"{roi} enter → token {token} → the firmware replied \"{ack}\", but an " -"enter should switch it ON" +"{roi} enter → token {token} → the firmware replied \"{ack}\", but an enter " +"should switch it ON" msgstr "" "{roi} entrada → token {token} → o firmware respondeu \"{ack}\", mas uma " "entrada deveria ligar" @@ -2656,8 +2635,8 @@ msgid "" "{roi} exit → token {token} → the firmware replied \"{ack}\", but an exit " "should switch it OFF" msgstr "" -"{roi} saída → token {token} → o firmware respondeu \"{ack}\", mas uma " -"saída deveria desligar" +"{roi} saída → token {token} → o firmware respondeu \"{ack}\", mas uma saída " +"deveria desligar" #: src/zebtrack/core/services/arduino_bindings.py:54 #, python-brace-format @@ -2679,10 +2658,11 @@ msgstr "Não foi possível encontrar o peso correspondente ao caminho: {path}" # | msgid "" "The OpenVINO model directory does not contain the required .xml # | files. " "Please reconvert the model." #: src/zebtrack/core/services/detector_service.py:156 -msgid "OpenVINO model path not found or invalid. Please convert the model first." +msgid "" +"OpenVINO model path not found or invalid. Please convert the model first." msgstr "" -"Caminho do modelo OpenVINO não encontrado ou inválido. Por favor, " -"converta o modelo primeiro." +"Caminho do modelo OpenVINO não encontrado ou inválido. Por favor, converta o" +" modelo primeiro." #: src/zebtrack/core/services/detector_service.py:164 msgid "YOLO .pt model path not found or invalid." @@ -2959,11 +2939,11 @@ msgstr "Exatamente 2 aquários devem ser configurados" #: src/zebtrack/core/services/wizard_service.py:920 #, python-brace-format msgid "" -"Aquarium {index} has a very small area ({area} px²). It may affect " -"detection accuracy." +"Aquarium {index} has a very small area ({area} px²). It may affect detection" +" accuracy." msgstr "" -"Aquário {index} tem área muito pequena ({area} px²). Pode afetar precisão" -" da detecção." +"Aquário {index} tem área muito pequena ({area} px²). Pode afetar precisão da" +" detecção." #: src/zebtrack/core/services/wizard_service.py:949 msgid "" @@ -3096,7 +3076,8 @@ msgid "Critical Error" msgstr "Erro Crítico" #: src/zebtrack/core/viewmodels/main_view_model_runtime.py:527 -msgid "The camera thread did not shut down correctly. The application will close." +msgid "" +"The camera thread did not shut down correctly. The application will close." msgstr "" "A thread da câmera não foi finalizada corretamente. O aplicativo será " "encerrado." @@ -3176,7 +3157,8 @@ msgstr "Valores restaurados para refletir as configurações atuais." #: src/zebtrack/ui/gui.py:809 msgid "This project is not configured for live experimental tracking." -msgstr "Este projeto não está configurado para rastreamento experimental ao vivo." +msgstr "" +"Este projeto não está configurado para rastreamento experimental ao vivo." # | msgid "Failed to open folder" #: src/zebtrack/ui/components/dialog_manager.py:824 src/zebtrack/ui/gui.py:840 @@ -3237,7 +3219,7 @@ msgstr "Não informado" #: src/zebtrack/ui/components/dialog_manager.py:284 #: src/zebtrack/ui/components/zone_controls.py:26 #: src/zebtrack/ui/sentinels.py:57 -#: src/zebtrack/ui/wizard/custom_regex_dialog.py:38 +#: src/zebtrack/ui/wizard/custom_regex_dialog.py:39 msgid "Day" msgstr "Dia" @@ -3390,9 +3372,10 @@ msgstr "Aviso" #: src/zebtrack/ui/builders/analysis_widgets.py:559 #, python-brace-format msgid "" -"Global settings were saved, but the current project could not be updated:" -" {error}" -msgstr "Configuração global salva, mas erro ao atualizar projeto atual: {error}" +"Global settings were saved, but the current project could not be updated: " +"{error}" +msgstr "" +"Configuração global salva, mas erro ao atualizar projeto atual: {error}" #: src/zebtrack/ui/builders/button_factory.py:36 msgid "Project Actions" @@ -3439,7 +3422,7 @@ msgstr "Trajetória" #: src/zebtrack/ui/builders/common_widgets.py:52 #: src/zebtrack/ui/components/validation_manager.py:1009 -#: src/zebtrack/ui/wizard/file_selection_step.py:204 +#: src/zebtrack/ui/wizard/file_selection_step.py:205 #: src/zebtrack/ui/wizard/import_config_step.py:215 msgid "Summary" msgstr "Sumário" @@ -3457,8 +3440,8 @@ 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." +"indica vídeos prontos para gerar trajetórias (arena e ROIs salvos). O valor " +"mostra quantos ainda aguardam processamento." #: src/zebtrack/ui/builders/common_widgets.py:180 msgid "Weight Type" @@ -3477,7 +3460,7 @@ msgid "Detection (for fast bounding boxes)" msgstr "Detecção (para caixas delimitadoras rápidas)" #: src/zebtrack/ui/builders/common_widgets.py:223 -#: src/zebtrack/ui/wizard/custom_regex_dialog.py:472 +#: src/zebtrack/ui/wizard/custom_regex_dialog.py:473 #: src/zebtrack/ui/wizard/design_editor_dialog.py:269 #: src/zebtrack/ui/wizard/wizard_dialog.py:207 msgid "Cancel" @@ -3547,9 +3530,8 @@ msgstr "" #: 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." +"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." @@ -3564,13 +3546,13 @@ msgstr "" #: 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." +"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." +"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/ui/builders/zone_control_builder.py:118 msgid "" @@ -3582,20 +3564,19 @@ msgstr "" #: src/zebtrack/ui/builders/zone_control_builder.py:124 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: " +"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_