From 93e43fc0d9121f9d02b026f04c1c1809496c657a Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 16 May 2026 19:36:01 +0000 Subject: [PATCH] feat(gui): sidebar sections + non-technical tool labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sidebar nav now groups tools under Data Review / Data Cleaners / Transformations / Automations via st.navigation, replacing the flat auto-discovered list. Tool display names switch to action-first phrasing (Find Duplicates, Fix Missing Values, Find Unusual Values, Standardize Formats, Clean Text, Quality Check, Map Columns, Combine Files, Automated Workflows) in EN + ES packs and on each page's H1. The Data Cleaners section follows the requested order: Missing Values → Outliers → Text Cleaner → Format Standardizer → Deduplicator → Quality Check. (Text Cleaner kept inside cleaners since the request didn't list it but the tool still ships.) Registry now carries a section field; helpers added: tools_in_section(), section_label(). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/gui/app.py | 256 +++++++++++++++++-------- src/gui/pages/1_Deduplicator.py | 4 +- src/gui/pages/2_Text_Cleaner.py | 2 +- src/gui/pages/3_Format_Standardizer.py | 2 +- src/gui/pages/4_Missing_Values.py | 2 +- src/gui/pages/5_Column_Mapper.py | 2 +- src/gui/pages/6_Outlier_Detector.py | 2 +- src/gui/pages/7_Multi_File_Merger.py | 2 +- src/gui/pages/8_Validator_Reporter.py | 2 +- src/gui/pages/9_Pipeline_Runner.py | 2 +- src/gui/tools_registry.py | 151 +++++++++------ src/i18n/packs/en.json | 29 ++- src/i18n/packs/es.json | 29 ++- tests/gui/test_chrome.py | 10 +- tests/gui/test_findings_panel.py | 16 +- tests/gui/test_gate.py | 2 +- tests/gui/test_lite_tier.py | 6 +- tests/gui/test_smoke.py | 24 +-- tests/gui/test_workflows.py | 12 +- 19 files changed, 356 insertions(+), 199 deletions(-) diff --git a/src/gui/app.py b/src/gui/app.py index a7014b7..873ddc4 100644 --- a/src/gui/app.py +++ b/src/gui/app.py @@ -2,6 +2,14 @@ Launch: streamlit run src/gui/app.py + +This module is the navigation manager for the full GUI: it registers +every tool page with ``st.navigation`` so the sidebar can render +section headers ("Data Review", "Data Cleaners", "Transformations", +"Automations") instead of the flat numbered list Streamlit's +auto-page-discovery would produce. The Home page itself is registered +as a callable defined below so the entry script remains the single +file users invoke. """ from __future__ import annotations @@ -18,102 +26,186 @@ if str(_project_root) not in sys.path: # --------------------------------------------------------------------------- -# Page config +# Home page (rendered when the user selects the default nav entry) # --------------------------------------------------------------------------- -from src.gui.components import ( - findings_count_for_tool, - hide_streamlit_chrome, - upload_and_analyze_section, -) -from src.i18n import t +def _home_page() -> None: + """Render the home page — upload section + tool grid + footer.""" + from src.gui.components import ( + findings_count_for_tool, + hide_streamlit_chrome, + upload_and_analyze_section, + ) + from src.gui.tools_registry import ( + TOOLS, + section_label, + tool_description, + tool_name, + ) + from src.i18n import t + from src.license import get_manager -st.set_page_config( - page_title=t("home.page_title"), - page_icon="🧹", - layout="wide", -) + st.set_page_config( + page_title=t("home.page_title"), + page_icon="🧹", + layout="wide", + ) + hide_streamlit_chrome() -# ``hide_streamlit_chrome`` also renders the sidebar language selector, -# so every page that hides chrome picks up the same picker. -hide_streamlit_chrome() + st.title(t("home.title")) + st.caption(t("home.caption")) + + st.divider() + + upload_and_analyze_section() + + st.divider() + + # Per-tier feature set for badging — Ready tools that the user's + # tier doesn't unlock get a red "🔒 Locked" pill instead of the green + # "Ready" so they see at a glance which tools an upgrade would + # unlock. Dev-mode skips the lock check so the home grid renders the + # full SKU during development. + _license_state = get_manager().current_state() + _unlocked_features = ( + set(_license_state.features) if _license_state.valid else set() + ) + _dev_mode = get_manager().dev_mode + + # Group tool cards by sidebar section so the home grid mirrors the + # left-nav layout — same vocabulary, same ordering. + sections: list[tuple[str, list]] = [] + for section in ("review", "cleaners", "transformations", "automations"): + tools = [tool for tool in TOOLS if tool.section == section] + if not tools: + continue + sections.append((section_label(section), tools)) + + for header, tools in sections: + st.subheader(header) + for row_start in range(0, len(tools), 3): + cols = st.columns(3) + for i, col in enumerate(cols): + idx = row_start + i + if idx >= len(tools): + break + tool = tools[idx] + with col: + tool_locked = ( + tool.status == "Ready" + and not _dev_mode + and tool.tool_id not in _unlocked_features + ) + if tool_locked: + status_key = "license.status_locked" + status_color = "red" + elif tool.status == "Ready": + status_key = "status.ready" + status_color = "green" + else: + status_key = "status.coming_soon" + status_color = "orange" + + badge = "" + n = findings_count_for_tool(tool.tool_id) + if n: + badge_key = ( + "home.findings_badge_one" + if n == 1 + else "home.findings_badge_other" + ) + badge = f" :red-background[**{t(badge_key, n=n)}**]" + lock_glyph = "🔒 " if tool_locked else "" + st.markdown( + f"### {tool.icon} {tool_name(tool.tool_id)}{badge}\n\n" + f"{tool_description(tool.tool_id)}\n\n" + f":{status_color}[**{lock_glyph}{t(status_key)}**]" + ) + + st.divider() + st.caption(t("chrome.footer")) # --------------------------------------------------------------------------- -# Home page +# Navigation registration # --------------------------------------------------------------------------- +# +# ``st.navigation`` overrides Streamlit's auto-discovery of the +# ``pages/`` directory, so every page we want in the sidebar must be +# listed here. The dict key becomes the section header rendered above +# the entries; an empty-string key suppresses the header so the Home +# entry sits at the top without a label above it. -st.title(t("home.title")) -st.caption(t("home.caption")) +from src.gui.tools_registry import TOOLS, section_label # noqa: E402 +from src.i18n import t as _t # noqa: E402 -st.divider() -# --------------------------------------------------------------------------- -# Upload & analyze (optional onboarding step) -# --------------------------------------------------------------------------- +def _page_for(tool_id: str, *, page_slug: str, icon: str, title: str) -> "st.Page": + return st.Page( + f"pages/{page_slug}.py", + title=title, + icon=icon, + url_path=tool_id, + ) -upload_and_analyze_section() -st.divider() - -# --------------------------------------------------------------------------- -# Tool cards -# --------------------------------------------------------------------------- - -from src.gui.tools_registry import TOOLS, tool_description, tool_name -from src.license import get_manager - -# Per-tier feature set for badging — Ready tools that the user's -# tier doesn't unlock get a red "🔒 Locked" pill instead of the green -# "Ready" so they see at a glance which tools an upgrade would -# unlock. Dev-mode skips the lock check so the home grid renders the -# full SKU during development. -_license_state = get_manager().current_state() -_unlocked_features = set(_license_state.features) if _license_state.valid else set() -_dev_mode = get_manager().dev_mode - -# Render tool cards in a 3-column grid. Cards picked up by the analyzer get a -# coloured "N findings" badge so the user can see at a glance which tools -# would help with the just-uploaded file. -for row_start in range(0, len(TOOLS), 3): - cols = st.columns(3) - for i, col in enumerate(cols): - idx = row_start + i - if idx >= len(TOOLS): - break - tool = TOOLS[idx] - with col: - tool_locked = ( - tool.status == "Ready" - and not _dev_mode - and tool.tool_id not in _unlocked_features +def _build_navigation() -> dict[str, list]: + by_section: dict[str, list] = { + "review": [], + "cleaners": [], + "transformations": [], + "automations": [], + } + for tool in TOOLS: + by_section[tool.section].append( + _page_for( + tool.tool_id, + page_slug=tool.page_slug, + icon=tool.icon, + title=tool.name, ) - if tool_locked: - status_key = "license.status_locked" - status_color = "red" - elif tool.status == "Ready": - status_key = "status.ready" - status_color = "green" - else: - status_key = "status.coming_soon" - status_color = "orange" + ) - badge = "" - n = findings_count_for_tool(tool.tool_id) - if n: - badge_key = "home.findings_badge_one" if n == 1 else "home.findings_badge_other" - badge = f" :red-background[**{t(badge_key, n=n)}**]" - lock_glyph = "🔒 " if tool_locked else "" - st.markdown( - f"### {tool.icon} {tool_name(tool.tool_id)}{badge}\n\n" - f"{tool_description(tool.tool_id)}\n\n" - f":{status_color}[**{lock_glyph}{t(status_key)}**]" - ) + # The Review gate has no entry in the registry (it isn't a "tool") + # so register it by hand at the top of its section. + review_page = st.Page( + "pages/0_Review.py", + title=_t("nav.review_page_title") or "Review", + icon="🛡️", + url_path="review", + ) + by_section["review"].insert(0, review_page) + + home = st.Page( + _home_page, + title=_t("nav.home_page_title") or "Home", + icon="🧹", + default=True, + url_path="home", + ) + activate = st.Page( + "pages/_Activate.py", + title=_t("nav.activate_title") or "Activate", + icon="🔑", + url_path="activate", + ) + close = st.Page( + "pages/99_Close.py", + title=_t("nav.close_title") or "Close", + icon="🛑", + url_path="close", + ) + + account_header = _t("nav.section_account") or "Account" + return { + "": [home], + section_label("review"): by_section["review"], + section_label("cleaners"): by_section["cleaners"], + section_label("transformations"): by_section["transformations"], + section_label("automations"): by_section["automations"], + account_header: [activate, close], + } -# --------------------------------------------------------------------------- -# Footer -# --------------------------------------------------------------------------- - -st.divider() -st.caption(t("chrome.footer")) +pg = st.navigation(_build_navigation()) +pg.run() diff --git a/src/gui/pages/1_Deduplicator.py b/src/gui/pages/1_Deduplicator.py index c4a2499..48b98f6 100644 --- a/src/gui/pages/1_Deduplicator.py +++ b/src/gui/pages/1_Deduplicator.py @@ -54,7 +54,7 @@ for key, default in _DEFAULTS.items(): # Header # --------------------------------------------------------------------------- -st.title("🔍 Deduplicator") +st.title("🔍 Find Duplicates") st.caption("Find and remove duplicate rows in CSV, delimited text, and Excel files.") @@ -363,5 +363,5 @@ else: st.divider() st.caption( "Runs locally. Your data never leaves this computer. " - "| DataTools Deduplicator v3.0" + "| DataTools v3.0" ) diff --git a/src/gui/pages/2_Text_Cleaner.py b/src/gui/pages/2_Text_Cleaner.py index f2c016e..dae734f 100644 --- a/src/gui/pages/2_Text_Cleaner.py +++ b/src/gui/pages/2_Text_Cleaner.py @@ -39,7 +39,7 @@ require_normalization_gate() # Header # --------------------------------------------------------------------------- -st.title("✂️ Text Cleaner") +st.title("✂️ Clean Text") st.caption( "Trim whitespace, fold smart quotes, strip invisible characters, and " "normalize line endings. Runs locally — your data never leaves this computer." diff --git a/src/gui/pages/3_Format_Standardizer.py b/src/gui/pages/3_Format_Standardizer.py index ed2a519..46f91b8 100644 --- a/src/gui/pages/3_Format_Standardizer.py +++ b/src/gui/pages/3_Format_Standardizer.py @@ -37,7 +37,7 @@ require_normalization_gate() # Header # --------------------------------------------------------------------------- -st.title("📐 Format Standardizer") +st.title("📐 Standardize Formats") st.caption( "Canonicalize dates, phone numbers, currency, names, addresses, and " "booleans on a per-column basis. Runs locally — your data never leaves " diff --git a/src/gui/pages/4_Missing_Values.py b/src/gui/pages/4_Missing_Values.py index c53ae0f..6b5de24 100644 --- a/src/gui/pages/4_Missing_Values.py +++ b/src/gui/pages/4_Missing_Values.py @@ -38,7 +38,7 @@ require_normalization_gate() # Header # --------------------------------------------------------------------------- -st.title("🕳️ Missing Value Handler") +st.title("🕳️ Fix Missing Values") st.caption( "Detect disguised nulls, profile missingness, and apply imputation or " "drop strategies. Runs locally — your data never leaves this computer." diff --git a/src/gui/pages/5_Column_Mapper.py b/src/gui/pages/5_Column_Mapper.py index 2c83e80..818332d 100644 --- a/src/gui/pages/5_Column_Mapper.py +++ b/src/gui/pages/5_Column_Mapper.py @@ -39,7 +39,7 @@ require_normalization_gate() # Header # --------------------------------------------------------------------------- -st.title("🗂️ Column Mapper") +st.title("🗂️ Map Columns") st.caption( "Rename columns, enforce a target schema, and coerce types. Runs locally — " "your data never leaves this computer." diff --git a/src/gui/pages/6_Outlier_Detector.py b/src/gui/pages/6_Outlier_Detector.py index e3e5cee..b26cf4b 100644 --- a/src/gui/pages/6_Outlier_Detector.py +++ b/src/gui/pages/6_Outlier_Detector.py @@ -26,7 +26,7 @@ require_normalization_gate() # Header # --------------------------------------------------------------------------- -st.title("📊 Outlier Detector") +st.title("📊 Find Unusual Values") st.caption("Detect and handle outliers in numeric columns.") st.info("This tool is under development.") diff --git a/src/gui/pages/7_Multi_File_Merger.py b/src/gui/pages/7_Multi_File_Merger.py index 3151e80..c58870f 100644 --- a/src/gui/pages/7_Multi_File_Merger.py +++ b/src/gui/pages/7_Multi_File_Merger.py @@ -26,7 +26,7 @@ require_normalization_gate() # Header # --------------------------------------------------------------------------- -st.title("📎 Multi-File Merger") +st.title("📎 Combine Files") st.caption("Combine multiple CSV and Excel files into one dataset.") st.info("This tool is under development.") diff --git a/src/gui/pages/8_Validator_Reporter.py b/src/gui/pages/8_Validator_Reporter.py index 576a948..f3d2333 100644 --- a/src/gui/pages/8_Validator_Reporter.py +++ b/src/gui/pages/8_Validator_Reporter.py @@ -26,7 +26,7 @@ require_normalization_gate() # Header # --------------------------------------------------------------------------- -st.title("✅ Validator & Reporter") +st.title("✅ Quality Check") st.caption("Validate data against rules and generate quality reports.") st.info("This tool is under development.") diff --git a/src/gui/pages/9_Pipeline_Runner.py b/src/gui/pages/9_Pipeline_Runner.py index a9f2e73..95b78fe 100644 --- a/src/gui/pages/9_Pipeline_Runner.py +++ b/src/gui/pages/9_Pipeline_Runner.py @@ -40,7 +40,7 @@ require_normalization_gate() # Header # --------------------------------------------------------------------------- -st.title("⚙️ Pipeline Runner") +st.title("⚙️ Automated Workflows") st.caption( "Chain DataTools cleaning steps into one repeatable workflow. The " "pipeline recommends an order; you stay in control." diff --git a/src/gui/tools_registry.py b/src/gui/tools_registry.py index e1dac00..5e7dc89 100644 --- a/src/gui/tools_registry.py +++ b/src/gui/tools_registry.py @@ -1,9 +1,9 @@ """Per-tool manifest registry. -Single source of truth for what tools exist, their display strings, and -the tier (which controls whether a tool ships in a given build SKU). The -home-page sidebar consumes this list; future per-tool packaging will -filter it via the ``tier`` field. +Single source of truth for what tools exist, their display strings, the +left-nav section they belong to, and the tier (which controls whether a +tool ships in a given build SKU). The home-page sidebar consumes this +list; future per-tool packaging will filter it via the ``tier`` field. Adding a tool: append one ``Tool`` entry. Page filenames must match the ``page_slug`` so Streamlit's automatic page discovery picks them up. @@ -22,6 +22,10 @@ from typing import Literal Tier = Literal["core", "pro", "enterprise"] Status = Literal["Ready", "Coming Soon"] +# Sidebar grouping. The Review gate is its own section; cleaners, +# transformations, and automations group the tools by what the user is +# trying to accomplish rather than by implementation detail. +Section = Literal["review", "cleaners", "transformations", "automations"] @dataclass(frozen=True) @@ -34,102 +38,127 @@ class Tool: description: str # One-sentence card body. page_slug: str # Streamlit page filename without ".py" (e.g. "1_Deduplicator"). status: Status # "Ready" or "Coming Soon" — drives the card badge color. + section: Section # Sidebar group this tool belongs to. tier: Tier = "core" # Build-time gating hook; every tool is "core" today. +# Order in this list IS the order shown in each sidebar section, so +# arranging it carefully matters: within "cleaners" we lead with the +# operations a non-technical user is most likely to need (filling +# blanks, flagging outliers) before progressing to format cleanup, +# dedup, and the final quality report. TOOLS: list[Tool] = [ - Tool( - tool_id="01_deduplicator", - icon="🔍", - name="Deduplicator", - description=( - "Fuzzy matching, normalization, survivor selection, and " - "interactive review." - ), - page_slug="1_Deduplicator", - status="Ready", - ), - Tool( - tool_id="02_text_cleaner", - icon="✂️", - name="Text Cleaner", - description=( - "Whitespace trim, multi-space collapse, Unicode normalization, " - "BOM and line-ending handling." - ), - page_slug="2_Text_Cleaner", - status="Ready", - ), - Tool( - tool_id="03_format_standardizer", - icon="📐", - name="Format Standardizer", - description=( - "Standardize dates, currencies, names, phone numbers, and addresses." - ), - page_slug="3_Format_Standardizer", - status="Ready", - ), Tool( tool_id="04_missing_handler", icon="🕳️", - name="Missing Value Handler", + name="Fix Missing Values", description=( "Detect disguised nulls, missingness analysis, and imputation strategies." ), page_slug="4_Missing_Values", status="Ready", - ), - Tool( - tool_id="05_column_mapper", - icon="🗂️", - name="Column Mapper", - description="Rename columns, enforce a target schema, and coerce types.", - page_slug="5_Column_Mapper", - status="Ready", + section="cleaners", ), Tool( tool_id="06_outlier_detector", icon="📊", - name="Outlier Detector", + name="Find Unusual Values", description=( "Z-score, IQR, and MAD detection with domain-rule violations and " "winsorization." ), page_slug="6_Outlier_Detector", status="Coming Soon", + section="cleaners", ), Tool( - tool_id="07_multi_file_merger", - icon="📎", - name="Multi-File Merger", - description="Combine multiple CSV/Excel files with schema alignment.", - page_slug="7_Multi_File_Merger", - status="Coming Soon", + tool_id="02_text_cleaner", + icon="✂️", + name="Clean Text", + description=( + "Whitespace trim, multi-space collapse, Unicode normalization, " + "BOM and line-ending handling." + ), + page_slug="2_Text_Cleaner", + status="Ready", + section="cleaners", + ), + Tool( + tool_id="03_format_standardizer", + icon="📐", + name="Standardize Formats", + description=( + "Standardize dates, currencies, names, phone numbers, and addresses." + ), + page_slug="3_Format_Standardizer", + status="Ready", + section="cleaners", + ), + Tool( + tool_id="01_deduplicator", + icon="🔍", + name="Find Duplicates", + description=( + "Fuzzy matching, normalization, survivor selection, and " + "interactive review." + ), + page_slug="1_Deduplicator", + status="Ready", + section="cleaners", ), Tool( tool_id="08_validator_reporter", icon="✅", - name="Validator & Reporter", + name="Quality Check", description=( "Validate against rules and generate PDF/Excel quality reports." ), page_slug="8_Validator_Reporter", status="Coming Soon", + section="cleaners", + ), + Tool( + tool_id="05_column_mapper", + icon="🗂️", + name="Map Columns", + description="Rename columns, enforce a target schema, and coerce types.", + page_slug="5_Column_Mapper", + status="Ready", + section="transformations", + ), + Tool( + tool_id="07_multi_file_merger", + icon="📎", + name="Combine Files", + description="Combine multiple CSV/Excel files with schema alignment.", + page_slug="7_Multi_File_Merger", + status="Coming Soon", + section="transformations", ), Tool( tool_id="09_pipeline_runner", icon="⚙️", - name="Pipeline Runner", + name="Automated Workflows", description=( "Chain tools in recommended order and pass output between steps." ), page_slug="9_Pipeline_Runner", status="Ready", + section="automations", ), ] +# Display labels for each sidebar section. Kept here so i18n falls back +# to a sensible English string if a translation pack is missing the key. +SECTION_LABELS: dict[Section, str] = { + "review": "Data Review", + "cleaners": "Data Cleaners", + "transformations": "Transformations", + "automations": "Automations", +} + + def tools_for_tier(*tiers: Tier) -> list[Tool]: """Subset filter for build-time slicing. @@ -142,6 +171,11 @@ def tools_for_tier(*tiers: Tier) -> list[Tool]: return [t for t in TOOLS if t.tier in keep] +def tools_in_section(section: Section) -> list[Tool]: + """Return the tools in *section*, preserving registry order.""" + return [t for t in TOOLS if t.section == section] + + def tool_by_id(tool_id: str) -> Tool | None: return next((t for t in TOOLS if t.tool_id == tool_id), None) @@ -167,3 +201,12 @@ def tool_description(tool_id: str) -> str: fallback = tool.description if tool else "" translated = _t(f"tools.{tool_id}.description") return translated if translated != f"tools.{tool_id}.description" else fallback + + +def section_label(section: Section) -> str: + """Return the localized sidebar section header.""" + from src.i18n import t as _t + fallback = SECTION_LABELS[section] + key = f"nav.section_{section}" + translated = _t(key) + return translated if translated != key else fallback diff --git a/src/i18n/packs/en.json b/src/i18n/packs/en.json index 73e22a5..e0b3acd 100644 --- a/src/i18n/packs/en.json +++ b/src/i18n/packs/en.json @@ -99,40 +99,51 @@ }, "tools": { "01_deduplicator": { - "name": "Deduplicator", + "name": "Find Duplicates", "description": "Fuzzy matching, normalization, survivor selection, and interactive review." }, "02_text_cleaner": { - "name": "Text Cleaner", + "name": "Clean Text", "description": "Whitespace trim, multi-space collapse, Unicode normalization, BOM and line-ending handling." }, "03_format_standardizer": { - "name": "Format Standardizer", + "name": "Standardize Formats", "description": "Standardize dates, currencies, names, phone numbers, and addresses." }, "04_missing_handler": { - "name": "Missing Value Handler", + "name": "Fix Missing Values", "description": "Detect disguised nulls, missingness analysis, and imputation strategies." }, "05_column_mapper": { - "name": "Column Mapper", + "name": "Map Columns", "description": "Rename columns, enforce a target schema, and coerce types." }, "06_outlier_detector": { - "name": "Outlier Detector", + "name": "Find Unusual Values", "description": "Z-score, IQR, and MAD detection with domain-rule violations and winsorization." }, "07_multi_file_merger": { - "name": "Multi-File Merger", + "name": "Combine Files", "description": "Combine multiple CSV/Excel files with schema alignment." }, "08_validator_reporter": { - "name": "Validator & Reporter", + "name": "Quality Check", "description": "Validate against rules and generate PDF/Excel quality reports." }, "09_pipeline_runner": { - "name": "Pipeline Runner", + "name": "Automated Workflows", "description": "Chain tools in recommended order and pass output between steps." } + }, + "nav": { + "section_review": "Data Review", + "section_cleaners": "Data Cleaners", + "section_transformations": "Transformations", + "section_automations": "Automations", + "review_page_title": "Review", + "home_page_title": "Home", + "section_account": "Account", + "activate_title": "Activate", + "close_title": "Close" } } diff --git a/src/i18n/packs/es.json b/src/i18n/packs/es.json index e028d6c..fed3af6 100644 --- a/src/i18n/packs/es.json +++ b/src/i18n/packs/es.json @@ -99,40 +99,51 @@ }, "tools": { "01_deduplicator": { - "name": "Eliminador de duplicados", + "name": "Buscar duplicados", "description": "Coincidencia difusa, normalización, selección de superviviente y revisión interactiva." }, "02_text_cleaner": { - "name": "Limpiador de texto", + "name": "Limpiar texto", "description": "Recorte de espacios, colapso de espacios múltiples, normalización Unicode, manejo de BOM y de finales de línea." }, "03_format_standardizer": { - "name": "Estandarizador de formatos", + "name": "Estandarizar formatos", "description": "Estandariza fechas, monedas, nombres, números de teléfono y direcciones." }, "04_missing_handler": { - "name": "Gestor de valores faltantes", + "name": "Corregir valores faltantes", "description": "Detecta nulos disfrazados, analiza la ausencia de datos y aplica estrategias de imputación." }, "05_column_mapper": { - "name": "Mapeador de columnas", + "name": "Mapear columnas", "description": "Renombra columnas, aplica un esquema objetivo y fuerza tipos de datos." }, "06_outlier_detector": { - "name": "Detector de valores atípicos", + "name": "Detectar valores atípicos", "description": "Detección por Z-score, IQR y MAD con reglas de dominio y winsorización." }, "07_multi_file_merger": { - "name": "Combinador de varios archivos", + "name": "Combinar archivos", "description": "Combina varios archivos CSV/Excel alineando sus esquemas." }, "08_validator_reporter": { - "name": "Validador e informes", + "name": "Verificación de calidad", "description": "Valida contra reglas y genera informes de calidad en PDF/Excel." }, "09_pipeline_runner": { - "name": "Ejecutor de canalizaciones", + "name": "Flujos automatizados", "description": "Encadena herramientas en el orden recomendado y pasa la salida entre pasos." } + }, + "nav": { + "section_review": "Revisión de datos", + "section_cleaners": "Limpiadores de datos", + "section_transformations": "Transformaciones", + "section_automations": "Automatizaciones", + "review_page_title": "Revisión", + "home_page_title": "Inicio", + "section_account": "Cuenta", + "activate_title": "Activar", + "close_title": "Cerrar" } } diff --git a/tests/gui/test_chrome.py b/tests/gui/test_chrome.py index 537f3ea..4340d15 100644 --- a/tests/gui/test_chrome.py +++ b/tests/gui/test_chrome.py @@ -168,11 +168,11 @@ class TestHomeToolGridLocalization: English names. Pin a few representative ones.""" @pytest.mark.parametrize("needle", [ - "Eliminador de duplicados", - "Limpiador de texto", - "Estandarizador de formatos", - "Gestor de valores faltantes", - "Mapeador de columnas", + "Buscar duplicados", + "Limpiar texto", + "Estandarizar formatos", + "Corregir valores faltantes", + "Mapear columnas", ]) def test_es_tool_name_on_home_grid(self, home_app, needle): with_language(home_app, "es") diff --git a/tests/gui/test_findings_panel.py b/tests/gui/test_findings_panel.py index 7a82751..cfa060a 100644 --- a/tests/gui/test_findings_panel.py +++ b/tests/gui/test_findings_panel.py @@ -113,13 +113,13 @@ class TestGrouping: labels = [e.label for e in app.expander] # Two unique tools → two expanders. Each label carries the # tool's display name + finding count. - text_cleaner_expanders = [lbl for lbl in labels if "Text Cleaner" in lbl] - format_expanders = [lbl for lbl in labels if "Format Standardizer" in lbl] + text_cleaner_expanders = [lbl for lbl in labels if "Clean Text" in lbl] + format_expanders = [lbl for lbl in labels if "Standardize Formats" in lbl] assert len(text_cleaner_expanders) == 1, ( - f"expected one Text Cleaner expander; got: {labels}" + f"expected one Clean Text expander; got: {labels}" ) assert len(format_expanders) == 1, ( - f"expected one Format Standardizer expander; got: {labels}" + f"expected one Standardize Formats expander; got: {labels}" ) def test_tool_names_localize_in_spanish(self): @@ -127,7 +127,7 @@ class TestGrouping: app = _harness(findings, lang="es") app.run() labels = [e.label for e in app.expander] - assert any("Limpiador de texto" in lbl for lbl in labels), ( + assert any("Limpiar texto" in lbl for lbl in labels), ( f"Spanish tool name missing; expanders: {labels}" ) @@ -140,7 +140,7 @@ class TestGrouping: app.run() labels = [e.label for e in app.expander] # Pack template: "{tool} — {n} finding(s)" - text_cleaner_label = next(l for l in labels if "Text Cleaner" in l) + text_cleaner_label = next(l for l in labels if "Clean Text" in l) assert "3" in text_cleaner_label, ( f"expected count '3' in expander label; got {text_cleaner_label!r}" ) @@ -163,7 +163,7 @@ class TestOpenToolButton: # raw markdown. We probe both. text = collected_text(app) # Pack template: "Open {tool} →" - assert "Open Text Cleaner" in text + assert "Open Clean Text" in text def test_open_tool_label_spanish(self): findings = [_make_finding(tool="02_text_cleaner")] @@ -171,7 +171,7 @@ class TestOpenToolButton: app.run() text = collected_text(app) # Pack template: "Abrir {tool} →" - assert "Abrir Limpiador de texto" in text + assert "Abrir Limpiar texto" in text # --------------------------------------------------------------------------- diff --git a/tests/gui/test_gate.py b/tests/gui/test_gate.py index 27cbe77..3aeac14 100644 --- a/tests/gui/test_gate.py +++ b/tests/gui/test_gate.py @@ -45,7 +45,7 @@ class TestGateNoUpload: text = collected_text(app) # The dedup page title is the unambiguous signal that the gate # didn't short-circuit. - assert "Deduplicator" in text + assert "Find Duplicates" in text def test_no_upload_no_gate_warning(self, app_factory): app = app_factory(GATED_PAGE) diff --git a/tests/gui/test_lite_tier.py b/tests/gui/test_lite_tier.py index 5fb62d0..f05dc24 100644 --- a/tests/gui/test_lite_tier.py +++ b/tests/gui/test_lite_tier.py @@ -40,9 +40,9 @@ def lite_license(monkeypatch, tmp_path): class TestLiteUnlockedPages: @pytest.mark.parametrize("slug,signal", [ - ("1_Deduplicator", "Deduplicator"), - ("2_Text_Cleaner", "Text Cleaner"), - ("3_Format_Standardizer", "Format Standardizer"), + ("1_Deduplicator", "Find Duplicates"), + ("2_Text_Cleaner", "Clean Text"), + ("3_Format_Standardizer", "Standardize Formats"), ]) def test_unlocked_pages_render_body( self, lite_license, app_factory, slug, signal, small_csv_bytes, diff --git a/tests/gui/test_smoke.py b/tests/gui/test_smoke.py index 0f6d79f..26458dc 100644 --- a/tests/gui/test_smoke.py +++ b/tests/gui/test_smoke.py @@ -53,17 +53,17 @@ PAGE_SLUGS = [ # When a page gains real Spanish translation, flip its 'es' entry to # the localized substring — the test surface stays the same. EXPECTED_SUBSTRINGS: dict[str, dict[str, str]] = { - "0_Review": {"en": "Review", "es": "Review"}, - "1_Deduplicator": {"en": "Deduplicator", "es": "Deduplicator"}, - "2_Text_Cleaner": {"en": "Text Cleaner", "es": "Text Cleaner"}, - "3_Format_Standardizer": {"en": "Format", "es": "Format"}, - "4_Missing_Values": {"en": "Missing", "es": "Missing"}, - "5_Column_Mapper": {"en": "Column", "es": "Column"}, - "6_Outlier_Detector": {"en": "Outlier", "es": "Outlier"}, - "7_Multi_File_Merger": {"en": "Merger", "es": "Merger"}, - "8_Validator_Reporter": {"en": "Validator", "es": "Validator"}, - "9_Pipeline_Runner": {"en": "Pipeline", "es": "Pipeline"}, - "99_Close": {"en": "Close DataTools", "es": "Cerrar DataTools"}, + "0_Review": {"en": "Review", "es": "Review"}, + "1_Deduplicator": {"en": "Find Duplicates", "es": "Find Duplicates"}, + "2_Text_Cleaner": {"en": "Clean Text", "es": "Clean Text"}, + "3_Format_Standardizer": {"en": "Standardize", "es": "Standardize"}, + "4_Missing_Values": {"en": "Fix Missing", "es": "Fix Missing"}, + "5_Column_Mapper": {"en": "Map Columns", "es": "Map Columns"}, + "6_Outlier_Detector": {"en": "Unusual", "es": "Unusual"}, + "7_Multi_File_Merger": {"en": "Combine Files", "es": "Combine Files"}, + "8_Validator_Reporter": {"en": "Quality Check", "es": "Quality Check"}, + "9_Pipeline_Runner": {"en": "Automated", "es": "Automated"}, + "99_Close": {"en": "Close DataTools", "es": "Cerrar DataTools"}, } @@ -95,7 +95,7 @@ class TestHomePageRenders: with_language(home_app, "es") home_app.run() text = collected_text(home_app) - assert "Eliminador de duplicados" in text + assert "Buscar duplicados" in text class TestEveryPageRenders: diff --git a/tests/gui/test_workflows.py b/tests/gui/test_workflows.py index a67c41a..c8e91d0 100644 --- a/tests/gui/test_workflows.py +++ b/tests/gui/test_workflows.py @@ -78,7 +78,7 @@ class TestTextCleanerWorkflow: app.run() assert not app.exception text = collected_text(app) - assert "Text Cleaner" in text + assert "Clean Text" in text def test_preview_or_clean_button_present(self, app_factory, small_csv_bytes): """The text cleaner ships a primary action (label varies by @@ -106,7 +106,7 @@ class TestFormatStandardizerWorkflow: app.run() assert not app.exception text = collected_text(app) - assert "Format Standardizer" in text + assert "Standardize Formats" in text # --------------------------------------------------------------------------- @@ -148,7 +148,7 @@ class TestPipelineRunnerWorkflow: app.run() assert not app.exception text = collected_text(app) - assert "Pipeline" in text + assert "Automated Workflows" in text # --------------------------------------------------------------------------- @@ -194,9 +194,9 @@ class TestReviewWorkflow: # --------------------------------------------------------------------------- @pytest.mark.parametrize("slug,name", [ - ("6_Outlier_Detector", "Outlier"), - ("7_Multi_File_Merger", "Merger"), - ("8_Validator_Reporter", "Validator"), + ("6_Outlier_Detector", "Unusual Values"), + ("7_Multi_File_Merger", "Combine Files"), + ("8_Validator_Reporter", "Quality Check"), ]) class TestComingSoonStubs: def test_stub_renders(self, app_factory, slug, name):