feat(gui): sidebar sections + non-technical tool labels

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 19:36:01 +00:00
parent 624f99653e
commit 93e43fc0d9
19 changed files with 356 additions and 199 deletions

View File

@@ -2,6 +2,14 @@
Launch: Launch:
streamlit run src/gui/app.py 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 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 ( def _home_page() -> None:
findings_count_for_tool, """Render the home page — upload section + tool grid + footer."""
hide_streamlit_chrome, from src.gui.components import (
upload_and_analyze_section, findings_count_for_tool,
) hide_streamlit_chrome,
from src.i18n import t 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( st.set_page_config(
page_title=t("home.page_title"), page_title=t("home.page_title"),
page_icon="🧹", page_icon="🧹",
layout="wide", layout="wide",
) )
hide_streamlit_chrome()
# ``hide_streamlit_chrome`` also renders the sidebar language selector, st.title(t("home.title"))
# so every page that hides chrome picks up the same picker. st.caption(t("home.caption"))
hide_streamlit_chrome()
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")) from src.gui.tools_registry import TOOLS, section_label # noqa: E402
st.caption(t("home.caption")) from src.i18n import t as _t # noqa: E402
st.divider()
# --------------------------------------------------------------------------- def _page_for(tool_id: str, *, page_slug: str, icon: str, title: str) -> "st.Page":
# Upload & analyze (optional onboarding step) return st.Page(
# --------------------------------------------------------------------------- f"pages/{page_slug}.py",
title=title,
icon=icon,
url_path=tool_id,
)
upload_and_analyze_section()
st.divider() def _build_navigation() -> dict[str, list]:
by_section: dict[str, list] = {
# --------------------------------------------------------------------------- "review": [],
# Tool cards "cleaners": [],
# --------------------------------------------------------------------------- "transformations": [],
"automations": [],
from src.gui.tools_registry import TOOLS, tool_description, tool_name }
from src.license import get_manager for tool in TOOLS:
by_section[tool.section].append(
# Per-tier feature set for badging — Ready tools that the user's _page_for(
# tier doesn't unlock get a red "🔒 Locked" pill instead of the green tool.tool_id,
# "Ready" so they see at a glance which tools an upgrade would page_slug=tool.page_slug,
# unlock. Dev-mode skips the lock check so the home grid renders the icon=tool.icon,
# full SKU during development. title=tool.name,
_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
) )
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 = "" # The Review gate has no entry in the registry (it isn't a "tool")
n = findings_count_for_tool(tool.tool_id) # so register it by hand at the top of its section.
if n: review_page = st.Page(
badge_key = "home.findings_badge_one" if n == 1 else "home.findings_badge_other" "pages/0_Review.py",
badge = f" :red-background[**{t(badge_key, n=n)}**]" title=_t("nav.review_page_title") or "Review",
lock_glyph = "🔒 " if tool_locked else "" icon="🛡️",
st.markdown( url_path="review",
f"### {tool.icon} {tool_name(tool.tool_id)}{badge}\n\n" )
f"{tool_description(tool.tool_id)}\n\n" by_section["review"].insert(0, review_page)
f":{status_color}[**{lock_glyph}{t(status_key)}**]"
) 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],
}
# --------------------------------------------------------------------------- pg = st.navigation(_build_navigation())
# Footer pg.run()
# ---------------------------------------------------------------------------
st.divider()
st.caption(t("chrome.footer"))

View File

@@ -54,7 +54,7 @@ for key, default in _DEFAULTS.items():
# Header # Header
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
st.title("🔍 Deduplicator") st.title("🔍 Find Duplicates")
st.caption("Find and remove duplicate rows in CSV, delimited text, and Excel files.") st.caption("Find and remove duplicate rows in CSV, delimited text, and Excel files.")
@@ -363,5 +363,5 @@ else:
st.divider() st.divider()
st.caption( st.caption(
"Runs locally. Your data never leaves this computer. " "Runs locally. Your data never leaves this computer. "
"| DataTools Deduplicator v3.0" "| DataTools v3.0"
) )

View File

@@ -39,7 +39,7 @@ require_normalization_gate()
# Header # Header
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
st.title("✂️ Text Cleaner") st.title("✂️ Clean Text")
st.caption( st.caption(
"Trim whitespace, fold smart quotes, strip invisible characters, and " "Trim whitespace, fold smart quotes, strip invisible characters, and "
"normalize line endings. Runs locally — your data never leaves this computer." "normalize line endings. Runs locally — your data never leaves this computer."

View File

@@ -37,7 +37,7 @@ require_normalization_gate()
# Header # Header
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
st.title("📐 Format Standardizer") st.title("📐 Standardize Formats")
st.caption( st.caption(
"Canonicalize dates, phone numbers, currency, names, addresses, and " "Canonicalize dates, phone numbers, currency, names, addresses, and "
"booleans on a per-column basis. Runs locally — your data never leaves " "booleans on a per-column basis. Runs locally — your data never leaves "

View File

@@ -38,7 +38,7 @@ require_normalization_gate()
# Header # Header
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
st.title("🕳️ Missing Value Handler") st.title("🕳️ Fix Missing Values")
st.caption( st.caption(
"Detect disguised nulls, profile missingness, and apply imputation or " "Detect disguised nulls, profile missingness, and apply imputation or "
"drop strategies. Runs locally — your data never leaves this computer." "drop strategies. Runs locally — your data never leaves this computer."

View File

@@ -39,7 +39,7 @@ require_normalization_gate()
# Header # Header
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
st.title("🗂️ Column Mapper") st.title("🗂️ Map Columns")
st.caption( st.caption(
"Rename columns, enforce a target schema, and coerce types. Runs locally — " "Rename columns, enforce a target schema, and coerce types. Runs locally — "
"your data never leaves this computer." "your data never leaves this computer."

View File

@@ -26,7 +26,7 @@ require_normalization_gate()
# Header # Header
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
st.title("📊 Outlier Detector") st.title("📊 Find Unusual Values")
st.caption("Detect and handle outliers in numeric columns.") st.caption("Detect and handle outliers in numeric columns.")
st.info("This tool is under development.") st.info("This tool is under development.")

View File

@@ -26,7 +26,7 @@ require_normalization_gate()
# Header # Header
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
st.title("📎 Multi-File Merger") st.title("📎 Combine Files")
st.caption("Combine multiple CSV and Excel files into one dataset.") st.caption("Combine multiple CSV and Excel files into one dataset.")
st.info("This tool is under development.") st.info("This tool is under development.")

View File

@@ -26,7 +26,7 @@ require_normalization_gate()
# Header # Header
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
st.title("Validator & Reporter") st.title("Quality Check")
st.caption("Validate data against rules and generate quality reports.") st.caption("Validate data against rules and generate quality reports.")
st.info("This tool is under development.") st.info("This tool is under development.")

View File

@@ -40,7 +40,7 @@ require_normalization_gate()
# Header # Header
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
st.title("⚙️ Pipeline Runner") st.title("⚙️ Automated Workflows")
st.caption( st.caption(
"Chain DataTools cleaning steps into one repeatable workflow. The " "Chain DataTools cleaning steps into one repeatable workflow. The "
"pipeline recommends an order; you stay in control." "pipeline recommends an order; you stay in control."

View File

@@ -1,9 +1,9 @@
"""Per-tool manifest registry. """Per-tool manifest registry.
Single source of truth for what tools exist, their display strings, and Single source of truth for what tools exist, their display strings, the
the tier (which controls whether a tool ships in a given build SKU). The left-nav section they belong to, and the tier (which controls whether a
home-page sidebar consumes this list; future per-tool packaging will tool ships in a given build SKU). The home-page sidebar consumes this
filter it via the ``tier`` field. list; future per-tool packaging will filter it via the ``tier`` field.
Adding a tool: append one ``Tool`` entry. Page filenames must match the Adding a tool: append one ``Tool`` entry. Page filenames must match the
``page_slug`` so Streamlit's automatic page discovery picks them up. ``page_slug`` so Streamlit's automatic page discovery picks them up.
@@ -22,6 +22,10 @@ from typing import Literal
Tier = Literal["core", "pro", "enterprise"] Tier = Literal["core", "pro", "enterprise"]
Status = Literal["Ready", "Coming Soon"] 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) @dataclass(frozen=True)
@@ -34,102 +38,127 @@ class Tool:
description: str # One-sentence card body. description: str # One-sentence card body.
page_slug: str # Streamlit page filename without ".py" (e.g. "1_Deduplicator"). page_slug: str # Streamlit page filename without ".py" (e.g. "1_Deduplicator").
status: Status # "Ready" or "Coming Soon" — drives the card badge color. 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. 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] = [ 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(
tool_id="04_missing_handler", tool_id="04_missing_handler",
icon="🕳️", icon="🕳️",
name="Missing Value Handler", name="Fix Missing Values",
description=( description=(
"Detect disguised nulls, missingness analysis, and imputation strategies." "Detect disguised nulls, missingness analysis, and imputation strategies."
), ),
page_slug="4_Missing_Values", page_slug="4_Missing_Values",
status="Ready", status="Ready",
), section="cleaners",
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",
), ),
Tool( Tool(
tool_id="06_outlier_detector", tool_id="06_outlier_detector",
icon="📊", icon="📊",
name="Outlier Detector", name="Find Unusual Values",
description=( description=(
"Z-score, IQR, and MAD detection with domain-rule violations and " "Z-score, IQR, and MAD detection with domain-rule violations and "
"winsorization." "winsorization."
), ),
page_slug="6_Outlier_Detector", page_slug="6_Outlier_Detector",
status="Coming Soon", status="Coming Soon",
section="cleaners",
), ),
Tool( Tool(
tool_id="07_multi_file_merger", tool_id="02_text_cleaner",
icon="📎", icon="✂️",
name="Multi-File Merger", name="Clean Text",
description="Combine multiple CSV/Excel files with schema alignment.", description=(
page_slug="7_Multi_File_Merger", "Whitespace trim, multi-space collapse, Unicode normalization, "
status="Coming Soon", "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(
tool_id="08_validator_reporter", tool_id="08_validator_reporter",
icon="", icon="",
name="Validator & Reporter", name="Quality Check",
description=( description=(
"Validate against rules and generate PDF/Excel quality reports." "Validate against rules and generate PDF/Excel quality reports."
), ),
page_slug="8_Validator_Reporter", page_slug="8_Validator_Reporter",
status="Coming Soon", 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(
tool_id="09_pipeline_runner", tool_id="09_pipeline_runner",
icon="⚙️", icon="⚙️",
name="Pipeline Runner", name="Automated Workflows",
description=( description=(
"Chain tools in recommended order and pass output between steps." "Chain tools in recommended order and pass output between steps."
), ),
page_slug="9_Pipeline_Runner", page_slug="9_Pipeline_Runner",
status="Ready", 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]: def tools_for_tier(*tiers: Tier) -> list[Tool]:
"""Subset filter for build-time slicing. """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] 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: def tool_by_id(tool_id: str) -> Tool | None:
return next((t for t in TOOLS if t.tool_id == tool_id), 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 "" fallback = tool.description if tool else ""
translated = _t(f"tools.{tool_id}.description") translated = _t(f"tools.{tool_id}.description")
return translated if translated != f"tools.{tool_id}.description" else fallback 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

View File

@@ -99,40 +99,51 @@
}, },
"tools": { "tools": {
"01_deduplicator": { "01_deduplicator": {
"name": "Deduplicator", "name": "Find Duplicates",
"description": "Fuzzy matching, normalization, survivor selection, and interactive review." "description": "Fuzzy matching, normalization, survivor selection, and interactive review."
}, },
"02_text_cleaner": { "02_text_cleaner": {
"name": "Text Cleaner", "name": "Clean Text",
"description": "Whitespace trim, multi-space collapse, Unicode normalization, BOM and line-ending handling." "description": "Whitespace trim, multi-space collapse, Unicode normalization, BOM and line-ending handling."
}, },
"03_format_standardizer": { "03_format_standardizer": {
"name": "Format Standardizer", "name": "Standardize Formats",
"description": "Standardize dates, currencies, names, phone numbers, and addresses." "description": "Standardize dates, currencies, names, phone numbers, and addresses."
}, },
"04_missing_handler": { "04_missing_handler": {
"name": "Missing Value Handler", "name": "Fix Missing Values",
"description": "Detect disguised nulls, missingness analysis, and imputation strategies." "description": "Detect disguised nulls, missingness analysis, and imputation strategies."
}, },
"05_column_mapper": { "05_column_mapper": {
"name": "Column Mapper", "name": "Map Columns",
"description": "Rename columns, enforce a target schema, and coerce types." "description": "Rename columns, enforce a target schema, and coerce types."
}, },
"06_outlier_detector": { "06_outlier_detector": {
"name": "Outlier Detector", "name": "Find Unusual Values",
"description": "Z-score, IQR, and MAD detection with domain-rule violations and winsorization." "description": "Z-score, IQR, and MAD detection with domain-rule violations and winsorization."
}, },
"07_multi_file_merger": { "07_multi_file_merger": {
"name": "Multi-File Merger", "name": "Combine Files",
"description": "Combine multiple CSV/Excel files with schema alignment." "description": "Combine multiple CSV/Excel files with schema alignment."
}, },
"08_validator_reporter": { "08_validator_reporter": {
"name": "Validator & Reporter", "name": "Quality Check",
"description": "Validate against rules and generate PDF/Excel quality reports." "description": "Validate against rules and generate PDF/Excel quality reports."
}, },
"09_pipeline_runner": { "09_pipeline_runner": {
"name": "Pipeline Runner", "name": "Automated Workflows",
"description": "Chain tools in recommended order and pass output between steps." "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"
} }
} }

View File

@@ -99,40 +99,51 @@
}, },
"tools": { "tools": {
"01_deduplicator": { "01_deduplicator": {
"name": "Eliminador de duplicados", "name": "Buscar duplicados",
"description": "Coincidencia difusa, normalización, selección de superviviente y revisión interactiva." "description": "Coincidencia difusa, normalización, selección de superviviente y revisión interactiva."
}, },
"02_text_cleaner": { "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." "description": "Recorte de espacios, colapso de espacios múltiples, normalización Unicode, manejo de BOM y de finales de línea."
}, },
"03_format_standardizer": { "03_format_standardizer": {
"name": "Estandarizador de formatos", "name": "Estandarizar formatos",
"description": "Estandariza fechas, monedas, nombres, números de teléfono y direcciones." "description": "Estandariza fechas, monedas, nombres, números de teléfono y direcciones."
}, },
"04_missing_handler": { "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." "description": "Detecta nulos disfrazados, analiza la ausencia de datos y aplica estrategias de imputación."
}, },
"05_column_mapper": { "05_column_mapper": {
"name": "Mapeador de columnas", "name": "Mapear columnas",
"description": "Renombra columnas, aplica un esquema objetivo y fuerza tipos de datos." "description": "Renombra columnas, aplica un esquema objetivo y fuerza tipos de datos."
}, },
"06_outlier_detector": { "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." "description": "Detección por Z-score, IQR y MAD con reglas de dominio y winsorización."
}, },
"07_multi_file_merger": { "07_multi_file_merger": {
"name": "Combinador de varios archivos", "name": "Combinar archivos",
"description": "Combina varios archivos CSV/Excel alineando sus esquemas." "description": "Combina varios archivos CSV/Excel alineando sus esquemas."
}, },
"08_validator_reporter": { "08_validator_reporter": {
"name": "Validador e informes", "name": "Verificación de calidad",
"description": "Valida contra reglas y genera informes de calidad en PDF/Excel." "description": "Valida contra reglas y genera informes de calidad en PDF/Excel."
}, },
"09_pipeline_runner": { "09_pipeline_runner": {
"name": "Ejecutor de canalizaciones", "name": "Flujos automatizados",
"description": "Encadena herramientas en el orden recomendado y pasa la salida entre pasos." "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"
} }
} }

View File

@@ -168,11 +168,11 @@ class TestHomeToolGridLocalization:
English names. Pin a few representative ones.""" English names. Pin a few representative ones."""
@pytest.mark.parametrize("needle", [ @pytest.mark.parametrize("needle", [
"Eliminador de duplicados", "Buscar duplicados",
"Limpiador de texto", "Limpiar texto",
"Estandarizador de formatos", "Estandarizar formatos",
"Gestor de valores faltantes", "Corregir valores faltantes",
"Mapeador de columnas", "Mapear columnas",
]) ])
def test_es_tool_name_on_home_grid(self, home_app, needle): def test_es_tool_name_on_home_grid(self, home_app, needle):
with_language(home_app, "es") with_language(home_app, "es")

View File

@@ -113,13 +113,13 @@ class TestGrouping:
labels = [e.label for e in app.expander] labels = [e.label for e in app.expander]
# Two unique tools → two expanders. Each label carries the # Two unique tools → two expanders. Each label carries the
# tool's display name + finding count. # tool's display name + finding count.
text_cleaner_expanders = [lbl for lbl in labels if "Text Cleaner" in lbl] text_cleaner_expanders = [lbl for lbl in labels if "Clean Text" in lbl]
format_expanders = [lbl for lbl in labels if "Format Standardizer" in lbl] format_expanders = [lbl for lbl in labels if "Standardize Formats" in lbl]
assert len(text_cleaner_expanders) == 1, ( 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, ( 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): def test_tool_names_localize_in_spanish(self):
@@ -127,7 +127,7 @@ class TestGrouping:
app = _harness(findings, lang="es") app = _harness(findings, lang="es")
app.run() app.run()
labels = [e.label for e in app.expander] 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}" f"Spanish tool name missing; expanders: {labels}"
) )
@@ -140,7 +140,7 @@ class TestGrouping:
app.run() app.run()
labels = [e.label for e in app.expander] labels = [e.label for e in app.expander]
# Pack template: "{tool} — {n} finding(s)" # 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, ( assert "3" in text_cleaner_label, (
f"expected count '3' in expander label; got {text_cleaner_label!r}" f"expected count '3' in expander label; got {text_cleaner_label!r}"
) )
@@ -163,7 +163,7 @@ class TestOpenToolButton:
# raw markdown. We probe both. # raw markdown. We probe both.
text = collected_text(app) text = collected_text(app)
# Pack template: "Open {tool} →" # Pack template: "Open {tool} →"
assert "Open Text Cleaner" in text assert "Open Clean Text" in text
def test_open_tool_label_spanish(self): def test_open_tool_label_spanish(self):
findings = [_make_finding(tool="02_text_cleaner")] findings = [_make_finding(tool="02_text_cleaner")]
@@ -171,7 +171,7 @@ class TestOpenToolButton:
app.run() app.run()
text = collected_text(app) text = collected_text(app)
# Pack template: "Abrir {tool} →" # Pack template: "Abrir {tool} →"
assert "Abrir Limpiador de texto" in text assert "Abrir Limpiar texto" in text
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@@ -45,7 +45,7 @@ class TestGateNoUpload:
text = collected_text(app) text = collected_text(app)
# The dedup page title is the unambiguous signal that the gate # The dedup page title is the unambiguous signal that the gate
# didn't short-circuit. # didn't short-circuit.
assert "Deduplicator" in text assert "Find Duplicates" in text
def test_no_upload_no_gate_warning(self, app_factory): def test_no_upload_no_gate_warning(self, app_factory):
app = app_factory(GATED_PAGE) app = app_factory(GATED_PAGE)

View File

@@ -40,9 +40,9 @@ def lite_license(monkeypatch, tmp_path):
class TestLiteUnlockedPages: class TestLiteUnlockedPages:
@pytest.mark.parametrize("slug,signal", [ @pytest.mark.parametrize("slug,signal", [
("1_Deduplicator", "Deduplicator"), ("1_Deduplicator", "Find Duplicates"),
("2_Text_Cleaner", "Text Cleaner"), ("2_Text_Cleaner", "Clean Text"),
("3_Format_Standardizer", "Format Standardizer"), ("3_Format_Standardizer", "Standardize Formats"),
]) ])
def test_unlocked_pages_render_body( def test_unlocked_pages_render_body(
self, lite_license, app_factory, slug, signal, small_csv_bytes, self, lite_license, app_factory, slug, signal, small_csv_bytes,

View File

@@ -53,17 +53,17 @@ PAGE_SLUGS = [
# When a page gains real Spanish translation, flip its 'es' entry to # When a page gains real Spanish translation, flip its 'es' entry to
# the localized substring — the test surface stays the same. # the localized substring — the test surface stays the same.
EXPECTED_SUBSTRINGS: dict[str, dict[str, str]] = { EXPECTED_SUBSTRINGS: dict[str, dict[str, str]] = {
"0_Review": {"en": "Review", "es": "Review"}, "0_Review": {"en": "Review", "es": "Review"},
"1_Deduplicator": {"en": "Deduplicator", "es": "Deduplicator"}, "1_Deduplicator": {"en": "Find Duplicates", "es": "Find Duplicates"},
"2_Text_Cleaner": {"en": "Text Cleaner", "es": "Text Cleaner"}, "2_Text_Cleaner": {"en": "Clean Text", "es": "Clean Text"},
"3_Format_Standardizer": {"en": "Format", "es": "Format"}, "3_Format_Standardizer": {"en": "Standardize", "es": "Standardize"},
"4_Missing_Values": {"en": "Missing", "es": "Missing"}, "4_Missing_Values": {"en": "Fix Missing", "es": "Fix Missing"},
"5_Column_Mapper": {"en": "Column", "es": "Column"}, "5_Column_Mapper": {"en": "Map Columns", "es": "Map Columns"},
"6_Outlier_Detector": {"en": "Outlier", "es": "Outlier"}, "6_Outlier_Detector": {"en": "Unusual", "es": "Unusual"},
"7_Multi_File_Merger": {"en": "Merger", "es": "Merger"}, "7_Multi_File_Merger": {"en": "Combine Files", "es": "Combine Files"},
"8_Validator_Reporter": {"en": "Validator", "es": "Validator"}, "8_Validator_Reporter": {"en": "Quality Check", "es": "Quality Check"},
"9_Pipeline_Runner": {"en": "Pipeline", "es": "Pipeline"}, "9_Pipeline_Runner": {"en": "Automated", "es": "Automated"},
"99_Close": {"en": "Close DataTools", "es": "Cerrar DataTools"}, "99_Close": {"en": "Close DataTools", "es": "Cerrar DataTools"},
} }
@@ -95,7 +95,7 @@ class TestHomePageRenders:
with_language(home_app, "es") with_language(home_app, "es")
home_app.run() home_app.run()
text = collected_text(home_app) text = collected_text(home_app)
assert "Eliminador de duplicados" in text assert "Buscar duplicados" in text
class TestEveryPageRenders: class TestEveryPageRenders:

View File

@@ -78,7 +78,7 @@ class TestTextCleanerWorkflow:
app.run() app.run()
assert not app.exception assert not app.exception
text = collected_text(app) 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): def test_preview_or_clean_button_present(self, app_factory, small_csv_bytes):
"""The text cleaner ships a primary action (label varies by """The text cleaner ships a primary action (label varies by
@@ -106,7 +106,7 @@ class TestFormatStandardizerWorkflow:
app.run() app.run()
assert not app.exception assert not app.exception
text = collected_text(app) text = collected_text(app)
assert "Format Standardizer" in text assert "Standardize Formats" in text
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -148,7 +148,7 @@ class TestPipelineRunnerWorkflow:
app.run() app.run()
assert not app.exception assert not app.exception
text = collected_text(app) text = collected_text(app)
assert "Pipeline" in text assert "Automated Workflows" in text
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -194,9 +194,9 @@ class TestReviewWorkflow:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@pytest.mark.parametrize("slug,name", [ @pytest.mark.parametrize("slug,name", [
("6_Outlier_Detector", "Outlier"), ("6_Outlier_Detector", "Unusual Values"),
("7_Multi_File_Merger", "Merger"), ("7_Multi_File_Merger", "Combine Files"),
("8_Validator_Reporter", "Validator"), ("8_Validator_Reporter", "Quality Check"),
]) ])
class TestComingSoonStubs: class TestComingSoonStubs:
def test_stub_renders(self, app_factory, slug, name): def test_stub_renders(self, app_factory, slug, name):