Compare commits

..

3 Commits

Author SHA1 Message Date
4d8513b1a3 docs: cover help popover, +/- nav indicators, render_tool_header
User-facing docs (USER-GUIDE en+es, README en+es):
- New short paragraph under §3.1 GUI noting the in-tool Help button
  on every detail page, what it contains (When to use / Steps /
  Examples / Tip), and that content lives in tools.<id>.help_md.
- One-line note in the README tool tables pointing at the same.
- Mention the sidebar +/- nav indicators replacing Streamlit's
  default Material Symbols chevron.

Developer docs:
- DEVELOPER: new "Tool page header" subsection documenting
  render_tool_header(tool_id), the help_md markdown skeleton, and
  the fallback to help.missing_body when a tool's help is absent.
  Update i18n authoring rules to list help.* keys and the per-tool
  help_md field alongside name/description/page_title/page_caption.
- TECHNICAL: new §10c documenting the sidebar nav indicator swap —
  CSS in _HIDE_CHROME_CSS plus _SWAP_NAV_SECTION_INDICATOR_JS
  injected through the hide_streamlit_chrome() iframe bundle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 18:08:01 +00:00
ac94208d8f chore: production-readiness sweep on the help-popover wave
- Drop unused 'from src.i18n import t' from pages 1-9 (the swap to
  render_tool_header(tool_id) means no page calls t() directly anymore).
  Pages 10, 11 and the underscore-prefixed pages were already clean or
  legitimately use t().

- Rewrite PDF Extractor help_md (en + es). The original prose described
  features the tool does NOT have — template drawing, per-source saved
  templates, automatic reuse. The actual tool is a heuristic batch
  scanner (per its own docstring: "No templates, no per-bank
  configuration"). New copy: scan → uncheck → pick date format → enable
  OCR if needed → download. Spanish version tagged with
  '<!-- TODO: review Spanish -->' since the prose is best-effort.

- Document why both stSidebarNavSectionHeader (legacy, streamlit~=1.35)
  and stNavSectionHeader (current, 1.57) testids appear in the chrome
  CSS — requirements floor is streamlit>=1.35,<2 so dropping the legacy
  selector would silently break the lower bound.

- Pin the t()-returns-key-on-miss contract that render_tool_header's
  fallback path depends on, with a comment at the call site.

- Pin the demo's intentional skip of hide_streamlit_chrome (so the
  +/- sidebar swap JS doesn't ever try to load there) with a load-
  bearing comment in app_demo.py.

- Confirmed i18n parity: every tool id has page_title / page_caption /
  description / name / help_md in BOTH packs; help.button_label and
  help.missing_body in both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 18:07:33 +00:00
4955fb239b test: cover help_md keys, header smoke, and bilingual ES smoke
Two stale Spanish smoke assertions still expected English page titles
for PDF Extractor and Reconciler — the i18n work landed real
translations ("PDF a CSV", "Reconciliar dos archivos"), so refresh the
expected substrings and the surrounding comment.

Add new coverage for the help-popover feature:
- TestHelpPopoverKeys (test_lang_packs): every tool_id resolves a
  non-empty tools.<id>.help_md in BOTH packs; help.button_label and
  help.missing_body resolve in both.
- TestDescriptionCopy (test_tools_registry): every Tool.description
  non-empty and under 120 chars — pins the post-jargon-scrub copy
  so future drift back into multi-clause prose is loud.
- TestRenderToolHeaderSmoke: render_tool_header is callable, listed
  in components.__all__, and every i18n key it touches resolves in
  both packs. Runs without a Streamlit script context.

Suite: 2427 passed (+9 new), 91 skipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 18:07:19 +00:00
22 changed files with 187 additions and 19 deletions

View File

@@ -18,6 +18,8 @@ Limpieza local de CSV / Excel. CLI + GUI en el navegador, sin nube, sin ceremoni
| 08 | Verificación de calidad | Próximamente | | 08 | Verificación de calidad | Próximamente |
| 09 | **Flujos automatizados** — encadena herramientas en un orden recomendado (no forzado), guarda/carga JSON, automatiza limpiezas semanales | Listo | | 09 | **Flujos automatizados** — encadena herramientas en un orden recomendado (no forzado), guarda/carga JSON, automatiza limpiezas semanales | Listo |
Cada página de herramienta incluye una ventana emergente de **Help** (a la derecha del título) con una guía compacta de Cuándo usarla / Pasos / Ejemplos / Consejo. El texto vive en los paquetes de idioma (`tools.<id>.help_md`).
## Descarga (usuarios no técnicos) ## Descarga (usuarios no técnicos)
Paquetes precompilados — sin instalar Python, sin permisos de administrador, sin internet en ejecución. Cada versión ofrece dos formatos por sistema operativo: un **instalador** que crea accesos directos en el escritorio + menú Inicio / Launchpad, y un **.zip portable** que descomprimes y haces doble clic. Elige el que te permita tu política de TI. Paquetes precompilados — sin instalar Python, sin permisos de administrador, sin internet en ejecución. Cada versión ofrece dos formatos por sistema operativo: un **instalador** que crea accesos directos en el escritorio + menú Inicio / Launchpad, y un **.zip portable** que descomprimes y haces doble clic. Elige el que te permita tu política de TI.

View File

@@ -18,6 +18,8 @@ Local CSV / Excel cleaning. CLI + browser GUI, no cloud, no install ceremony. GU
| 08 | Quality Check | Coming Soon | | 08 | Quality Check | Coming Soon |
| 09 | **Automated Workflows** — chain tools with recommended (not forced) order, save/load JSON, automate weekly cleanups | Ready | | 09 | **Automated Workflows** — chain tools with recommended (not forced) order, save/load JSON, automate weekly cleanups | Ready |
Every tool page has an in-tool **Help** popover (right of the title) with a compact When-to-use / Steps / Examples / Tip card. Copy lives in the language packs (`tools.<id>.help_md`).
## Download (non-technical users) ## Download (non-technical users)
Pre-built bundles — no Python install, no admin rights, no internet at runtime. Each release ships two flavors per OS: an **installer** that wires up Desktop + Start Menu / Launchpad shortcuts, and a **portable .zip** you unzip and double-click. Pick whichever your IT policy allows. Pre-built bundles — no Python install, no admin rights, no internet at runtime. Each release ships two flavors per OS: an **installer** that wires up Desktop + Start Menu / Launchpad shortcuts, and a **portable .zip** you unzip and double-click. Pick whichever your IT policy allows.

View File

@@ -96,6 +96,36 @@ DeduplicationResult # deduplicated_df, removed_df, match_groups, l
No other call sites change. Gate auto-discovers it via the registry. No other call sites change. Gate auto-discovers it via the registry.
### Tool page header — `render_tool_header(tool_id)`
Every tool page renders its title block via `render_tool_header(tool_id)` in `src/gui/components/_legacy.py` — do not call `st.title()` + `st.caption()` directly. The helper renders:
- `tools.<id>.page_title` as the page title (left column).
- A **Help** popover button right of the title (icon `:material/help_outline:`, label from `help.button_label`). Clicking opens an `st.popover` containing the markdown body.
- `tools.<id>.page_caption` as the caption below.
All copy is i18n-driven; editors can tweak help text without touching Python. If a tool is missing its `help_md` key, the popover falls back to `help.missing_body`.
**`help_md` structure** (markdown, stored as a single string with `\n` line breaks in JSON):
```
**When to use**
- bullet 1
- bullet 2
**Steps**
1. numbered step
2. numbered step
**Examples**
- example 1
- example 2
**Tip** one-sentence pro tip.
```
Keep it short — the popover is intentionally compact. Mirror the structure across every tool so the muscle memory transfers.
### i18n — language packs ### i18n — language packs
The GUI's user-facing strings live in `src/i18n/packs/<code>.json`, keyed by ISO-639-1 code. English (`en.json`) is canonical; missing keys in other packs fall back to English, and missing keys in English fall back to the literal dotted key so a typo is visible rather than silent. The GUI's user-facing strings live in `src/i18n/packs/<code>.json`, keyed by ISO-639-1 code. English (`en.json`) is canonical; missing keys in other packs fall back to English, and missing keys in English fall back to the literal dotted key so a typo is visible rather than silent.
@@ -120,7 +150,8 @@ st.warning(t("gate.warning", name=filename)) # {name} interpolated via str.for
3. Use the dotted key at the call site: `t("section.subsection.key")` or `t("section.key", name=value)` for placeholder interpolation. 3. Use the dotted key at the call site: `t("section.subsection.key")` or `t("section.key", name=value)` for placeholder interpolation.
**Authoring rules:** **Authoring rules:**
- Keys live under semantic sections (`home.*`, `upload.*`, `findings.*`, `tools.<id>.name`). Don't nest by language or by tool unless the string is genuinely tool-specific. - Keys live under semantic sections (`home.*`, `upload.*`, `findings.*`, `help.*`, `tools.<id>.name`). Don't nest by language or by tool unless the string is genuinely tool-specific.
- Per-tool header copy lives under `tools.<id>.{page_title, page_caption, help_md}`. `page_caption` is the one-line subtitle under the title; `help_md` is the popover body (see *Tool page header* above). Top-level `help.button_label` / `help.missing_body` are shared across every tool.
- Use `{named}` placeholders (not positional `{0}`) so translators see what's being interpolated. - Use `{named}` placeholders (not positional `{0}`) so translators see what's being interpolated.
- Strings can contain Streamlit markdown (`**bold**`) — pass through `st.markdown` / `st.caption` as usual. - Strings can contain Streamlit markdown (`**bold**`) — pass through `st.markdown` / `st.caption` as usual.
- Do **not** put strings inside the farewell-overlay JS payload without going through `_js_html_safe()` in `src/gui/components/_legacy.py`; the helper escapes both the JS string terminator and HTML special chars. The test `TestFarewellEscape` pins that contract. - Do **not** put strings inside the farewell-overlay JS payload without going through `_js_html_safe()` in `src/gui/components/_legacy.py`; the helper escapes both the JS string terminator and HTML special chars. The test `TestFarewellEscape` pins that contract.

View File

@@ -242,6 +242,15 @@ The GUI uses an in-house, JSON-backed translation layer at `src/i18n/`. **No** `
**Why not gettext**: zero compiled artifacts in the PyInstaller bundle, no build step before tests run, no `.po`/`.mo` round-trip for translators (anyone can edit JSON), and the same lookup works in unit tests without process state. Locked in because the surface won't grow large enough to need the alternative, and the alternative breaks the "drop a file, run pytest, ship" loop. **Why not gettext**: zero compiled artifacts in the PyInstaller bundle, no build step before tests run, no `.po`/`.mo` round-trip for translators (anyone can edit JSON), and the same lookup works in unit tests without process state. Locked in because the surface won't grow large enough to need the alternative, and the alternative breaks the "drop a file, run pytest, ship" loop.
## 10c. GUI chrome — sidebar nav indicator swap
Streamlit's `st.Page`-driven sidebar renders section headers with a Material Symbols ligature (`expand_more` / `expand_less`). The header element is not a button and carries no `aria-expanded`, so a pure-CSS swap can't follow open/closed state. We replace the glyph with plain typographic `+` / `` (U+2212) via JS:
- **CSS** (`components/_legacy.py`, `_HIDE_CHROME_CSS`) drops the Material Symbols font on `[data-testid="stIconMaterial"]` inside `[data-testid="stNavSectionHeader"]` so the rewritten character renders as normal text rather than re-resolving as an icon name.
- **JS** (`_SWAP_NAV_SECTION_INDICATOR_JS`) walks each section header, reads the icon's text node, and rewrites `expand_more``+` / `expand_less```. A MutationObserver re-runs the swap when Streamlit re-renders the sidebar (RAF-throttled so a burst of mutations is one swap).
The script ships through the same component-iframe bundle as the brand injector and upload-button rename inside `hide_streamlit_chrome()` — one iframe per page, three DOM mutations.
## 11. Per-script functional specs ## 11. Per-script functional specs
Specs live in this section as scripts enter active build. Each follows the Tier 1/2/3 structure with explicit strategic framing (what's the market gap given some of this is free elsewhere). Specs live in this section as scripts enter active build. Each follows the Tier 1/2/3 structure with explicit strategic framing (what's the market gap given some of this is free elsewhere).

View File

@@ -135,6 +135,10 @@ Matriz de soporte completa: [REQUIREMENTS.md](REQUIREMENTS.md) (solo en inglés)
Las opciones avanzadas se encuentran en paneles desplegables. El archivo original nunca se modifica. Las opciones avanzadas se encuentran en paneles desplegables. El archivo original nunca se modifica.
**Ayuda en la herramienta**: cada página tiene un botón **Help** a la derecha del título. Al pulsarlo se abre una ventana emergente con una guía compacta (Cuándo usarla · Pasos · Ejemplos · Consejo). Úsala como recordatorio a media tarea — la ventana se cierra al hacer clic fuera y tus datos no se ven afectados.
**Navegación lateral**: la barra lateral agrupa las herramientas en secciones (Análisis, Limpiadores de datos, Transformaciones, Automatizaciones). Cada cabecera muestra `+` cuando está plegada y `` cuando está desplegada — pulsa la cabecera para alternar.
### 3.2 CLI ### 3.2 CLI
```bash ```bash

View File

@@ -135,6 +135,10 @@ Full numbered support matrix: [REQUIREMENTS.md](REQUIREMENTS.md).
Advanced options are tucked in expander panes. The original file is never modified. Advanced options are tucked in expander panes. The original file is never modified.
**In-tool Help**: every tool page has a **Help** button right of the title. Click it to open a popover with a compact how-to (When to use · Steps · Examples · Tip). Use it as a refresher mid-task — the popover closes when you click outside, your inputs are untouched.
**Sidebar nav**: the sidebar groups tools into sections (Analysis, Data Cleaners, Transformations, Automations). Each section header shows `+` when collapsed and `` when expanded — click the header to toggle.
### 3.2 CLI ### 3.2 CLI
```bash ```bash

View File

@@ -132,6 +132,15 @@ st.set_page_config(
# Strip Streamlit chrome that breaks the iframe-embed look on the # Strip Streamlit chrome that breaks the iframe-embed look on the
# landing pages. # landing pages.
#
# We deliberately do NOT call ``hide_streamlit_chrome()`` from the
# paid GUI here — that helper drags in the license gate, the sidebar
# brand block, language selector, and the +/- nav-section indicator
# script. The demo has no sidebar (we hide it below), no licensing
# (it's the marketing surface), and a different visual palette (dark
# theme vs. the paid app's cream paper). Keep this hand-rolled chrome
# in sync with the demo's own dark palette; do NOT replace it with
# the paid GUI's chrome helper.
st.markdown(""" st.markdown("""
<style> <style>
#MainMenu, footer, header { visibility: hidden; } #MainMenu, footer, header { visibility: hidden; }

View File

@@ -279,7 +279,13 @@ body, .stApp {
with class ``st-emotion-cache-…`` inside ``stSidebarNav`` — class with class ``st-emotion-cache-…`` inside ``stSidebarNav`` — class
hashes are unstable across versions, so we lean on the structural hashes are unstable across versions, so we lean on the structural
position (the bare span / h2 directly inside the nav list) rather position (the bare span / h2 directly inside the nav list) rather
than emotion classes. */ than emotion classes.
``stSidebarNavSectionHeader`` is the LEGACY testid used by
Streamlit ~1.35; current Streamlit emits ``stNavSectionHeader``
(handled by the dedicated block further down). Both are kept in
the selector list because the requirements floor is
``streamlit>=1.35,<2`` — dropping the legacy testid would break
the visual treatment on the lower bound. */
[data-testid="stSidebarNav"] h2, [data-testid="stSidebarNav"] h2,
[data-testid="stSidebarNav"] h3, [data-testid="stSidebarNav"] h3,
[data-testid="stSidebarNavSeparator"] span, [data-testid="stSidebarNavSeparator"] span,
@@ -316,6 +322,9 @@ body, .stApp {
[data-testid="stSidebarNavItems"] > li { [data-testid="stSidebarNavItems"] > li {
margin-bottom: 1px !important; margin-bottom: 1px !important;
} }
/* Legacy testid — kept for streamlit~=1.35 (see note above). The
tighter padding for the current ``stNavSectionHeader`` is set in
the dedicated block further down. */
[data-testid="stSidebarNavSectionHeader"] { [data-testid="stSidebarNavSectionHeader"] {
padding-top: 10px !important; padding-top: 10px !important;
padding-bottom: 2px !important; padding-bottom: 2px !important;
@@ -2168,6 +2177,13 @@ def render_tool_header(tool_id: str) -> None:
# button floats above the big title text. # button floats above the big title text.
st.write("") st.write("")
body = _t(f"tools.{tool_id}.help_md") body = _t(f"tools.{tool_id}.help_md")
# ``src.i18n.t`` falls back to returning the lookup key itself
# on miss (see ``_resolve`` → key-as-string fallback). That's
# what we detect here: any tool whose ``help_md`` entry is
# absent from both en + es packs shows the generic missing-body
# string instead of the raw dotted key. Real help_md content
# in the packs starts with ``**When to use**``-style markdown,
# so this prefix check is safe.
if body.startswith("tools."): if body.startswith("tools."):
body = _t("help.missing_body") body = _t("help.missing_body")
with st.popover( with st.popover(

View File

@@ -29,7 +29,6 @@ from src.gui.components import (
require_feature_or_render_upgrade, require_feature_or_render_upgrade,
results_summary, results_summary,
) )
from src.i18n import t
from src.license import FeatureFlag from src.license import FeatureFlag
hide_streamlit_chrome() hide_streamlit_chrome()

View File

@@ -24,7 +24,6 @@ from src.gui.components import (
render_hidden_aware_preview, render_hidden_aware_preview,
require_feature_or_render_upgrade, require_feature_or_render_upgrade,
) )
from src.i18n import t
from src.license import FeatureFlag from src.license import FeatureFlag
from src.core.text_clean import ( from src.core.text_clean import (
PRESETS, PRESETS,

View File

@@ -23,7 +23,6 @@ from src.gui.components import (
pickup_or_upload, pickup_or_upload,
require_feature_or_render_upgrade, require_feature_or_render_upgrade,
) )
from src.i18n import t
from src.core.format_standardize import ( from src.core.format_standardize import (
PRESETS, PRESETS,
FieldType, FieldType,

View File

@@ -23,7 +23,6 @@ from src.gui.components import (
pickup_or_upload, pickup_or_upload,
require_feature_or_render_upgrade, require_feature_or_render_upgrade,
) )
from src.i18n import t
from src.core.missing import ( from src.core.missing import (
DEFAULT_SENTINELS, DEFAULT_SENTINELS,
MissingOptions, MissingOptions,

View File

@@ -23,7 +23,6 @@ from src.gui.components import (
pickup_or_upload, pickup_or_upload,
require_feature_or_render_upgrade, require_feature_or_render_upgrade,
) )
from src.i18n import t
from src.core.column_mapper import ( from src.core.column_mapper import (
MapOptions, MapOptions,
PRESETS, PRESETS,

View File

@@ -18,7 +18,6 @@ from src.gui.components import (
hide_streamlit_chrome, hide_streamlit_chrome,
require_feature_or_render_upgrade, require_feature_or_render_upgrade,
) )
from src.i18n import t
from src.license import FeatureFlag from src.license import FeatureFlag
hide_streamlit_chrome() hide_streamlit_chrome()

View File

@@ -18,7 +18,6 @@ from src.gui.components import (
hide_streamlit_chrome, hide_streamlit_chrome,
require_feature_or_render_upgrade, require_feature_or_render_upgrade,
) )
from src.i18n import t
from src.license import FeatureFlag from src.license import FeatureFlag
hide_streamlit_chrome() hide_streamlit_chrome()

View File

@@ -18,7 +18,6 @@ from src.gui.components import (
hide_streamlit_chrome, hide_streamlit_chrome,
require_feature_or_render_upgrade, require_feature_or_render_upgrade,
) )
from src.i18n import t
from src.license import FeatureFlag from src.license import FeatureFlag
hide_streamlit_chrome() hide_streamlit_chrome()

View File

@@ -23,7 +23,6 @@ from src.gui.components import (
pickup_or_upload, pickup_or_upload,
require_feature_or_render_upgrade, require_feature_or_render_upgrade,
) )
from src.i18n import t
from src.core.pipeline import ( from src.core.pipeline import (
Pipeline, Pipeline,
SOFT_DEPENDENCIES, SOFT_DEPENDENCIES,

View File

@@ -177,7 +177,7 @@
"description": "Pull transactions out of bank-statement PDFs into a clean CSV file.", "description": "Pull transactions out of bank-statement PDFs into a clean CSV file.",
"page_title": "PDF to CSV", "page_title": "PDF to CSV",
"page_caption": "Pull transactions out of bank-statement PDFs into a clean CSV file.", "page_caption": "Pull transactions out of bank-statement PDFs into a clean CSV file.",
"help_md": "**When to use**\n- Bank or credit-card statements\n- Vendor invoices with line-item tables\n- Any PDF with a transaction table\n\n**Steps**\n1. Upload a PDF\n2. Draw a box around the table (once per source)\n3. Save the template (e.g. `Chase Checking`)\n4. Reuse the template on every future statement of that type\n5. Export the CSV\n\n**Examples**\n- Chase March statement → 87 transactions extracted\n- Same template auto-runs on April, May, June\n- Batch mode: process 12 months at once\n\n**Tip** Templates are per source (Chase, Wells Fargo, …). Build one per source you receive regularly." "help_md": "**When to use**\n- Bank or credit-card statements\n- Vendor invoices with line-item tables\n- Any PDF with a transaction table\n\n**Steps**\n1. Upload one or more PDFs (batch is fine)\n2. Click **Scan** — rows that look like transactions are pulled out automatically\n3. Uncheck any rows you don't want\n4. Pick your date format (and turn on OCR if the PDF is a scanned image)\n5. Download the CSV\n\n**Examples**\n- Chase March statement → 87 transactions found\n- Drop in 12 months at once and get one combined CSV\n- Image-only scan + OCR → still works if Tesseract is installed\n\n**Tip** If a withdrawal shows as `(4.50)`, leave **Treat (4.50) as negative** on. Turn it off only if your statements use a different convention."
}, },
"11_reconciler": { "11_reconciler": {
"name": "Reconcile Two Files", "name": "Reconcile Two Files",

View File

@@ -177,7 +177,7 @@
"description": "Extrae transacciones de extractos bancarios en PDF a un archivo CSV limpio.", "description": "Extrae transacciones de extractos bancarios en PDF a un archivo CSV limpio.",
"page_title": "PDF a CSV", "page_title": "PDF a CSV",
"page_caption": "Extrae transacciones de extractos bancarios en PDF a un archivo CSV limpio.", "page_caption": "Extrae transacciones de extractos bancarios en PDF a un archivo CSV limpio.",
"help_md": "**Cuándo usarlo**\n- Extractos bancarios o de tarjeta\n- Facturas de proveedor con tablas\n- Cualquier PDF con tabla de transacciones\n\n**Pasos**\n1. Sube un PDF\n2. Dibuja un recuadro alrededor de la tabla (una vez por fuente)\n3. Guarda la plantilla (p. ej. `Chase Cuenta`)\n4. Reutiliza la plantilla en los siguientes extractos del mismo tipo\n5. Exporta el CSV\n\n**Ejemplos**\n- Extracto Chase de marzo → 87 transacciones extraídas\n- La misma plantilla se ejecuta sola en abril, mayo, junio\n- Modo lote: procesa 12 meses de una vez\n\n**Consejo** Las plantillas son por fuente (Chase, Wells Fargo…). Crea una por cada banco que recibas con regularidad." "help_md": "<!-- TODO: review Spanish -->\n**Cuándo usarlo**\n- Extractos bancarios o de tarjeta\n- Facturas de proveedor con tablas\n- Cualquier PDF con tabla de transacciones\n\n**Pasos**\n1. Sube uno o más PDFs (modo lote permitido)\n2. Pulsa **Escanear** — las filas que parecen transacciones se extraen automáticamente\n3. Desmarca las filas que no quieras\n4. Elige el formato de fecha (y activa OCR si el PDF es una imagen escaneada)\n5. Descarga el CSV\n\n**Ejemplos**\n- Extracto Chase de marzo → 87 transacciones detectadas\n- Procesa 12 meses de una vez y obtén un CSV combinado\n- PDF solo-imagen + OCR → funciona si Tesseract está instalado\n\n**Consejo** Si un cargo aparece como `(4,50)`, deja activado **Tratar (4,50) como negativo**. Desactívalo solo si tus extractos usan otra convención."
}, },
"11_reconciler": { "11_reconciler": {
"name": "Reconciliar dos archivos", "name": "Reconciliar dos archivos",

View File

@@ -63,12 +63,11 @@ EXPECTED_SUBSTRINGS: dict[str, dict[str, str]] = {
"7_Multi_File_Merger": {"en": "Combine Files", "es": "Combinar archivos"}, "7_Multi_File_Merger": {"en": "Combine Files", "es": "Combinar archivos"},
"8_Validator_Reporter": {"en": "Quality Check", "es": "Verificación de calidad"}, "8_Validator_Reporter": {"en": "Quality Check", "es": "Verificación de calidad"},
"9_Pipeline_Runner": {"en": "Automated", "es": "Flujos automatizados"}, "9_Pipeline_Runner": {"en": "Automated", "es": "Flujos automatizados"},
# The PDF Extractor and Reconciler pages are English-only today # PDF Extractor + Reconciler page titles are now translated in
# (translations tracked as a follow-up). The smoke test value is # both packs (``tools.<id>.page_title``). Their hero copy diverges
# still that the page *renders at all* in 'es'; the substring is # by language, so the smoke test pins the localized substring.
# the same English hero text under both languages. "10_PDF_Extractor": {"en": "PDF to CSV", "es": "PDF a CSV"},
"10_PDF_Extractor": {"en": "PDF to CSV", "es": "PDF to CSV"}, "11_Reconciler": {"en": "Reconcile", "es": "Reconciliar"},
"11_Reconciler": {"en": "Reconcile", "es": "Reconcile"},
"99_Close": {"en": "Shutting down", "es": "Cerrando"}, "99_Close": {"en": "Shutting down", "es": "Cerrando"},
} }

View File

@@ -178,3 +178,32 @@ class TestKeyCoverage:
for lang in ("en", "es"): for lang in ("en", "es"):
value = t(key, lang) value = t(key, lang)
assert value and value != key, f"missing {key!r} in {lang}" assert value and value != key, f"missing {key!r} in {lang}"
class TestHelpPopoverKeys:
"""Every tool's inline Help popover (``render_tool_header``) pulls
its copy from ``tools.<id>.help_md`` and the two shared labels
``help.button_label`` / ``help.missing_body``. A missing key would
fall back to the literal lookup key and render that string in the
popover instead of helpful content."""
@pytest.mark.parametrize("lang", ["en", "es"])
def test_help_shared_keys_present(self, lang):
for key in ("help.button_label", "help.missing_body"):
value = t(key, lang)
assert value and value != key, f"missing {key!r} in {lang!r}"
@pytest.mark.parametrize("lang", ["en", "es"])
def test_every_tool_has_help_md(self, lang):
# Import lazily so this file stays importable without the GUI.
from src.gui.tools_registry import TOOLS
missing: list[str] = []
for tool in TOOLS:
key = f"tools.{tool.tool_id}.help_md"
value = t(key, lang)
if not value or value == key or not value.strip():
missing.append(tool.tool_id)
assert not missing, (
f"language {lang!r} is missing help_md for: {missing}"
)

View File

@@ -157,6 +157,78 @@ class TestLocalizedAccessors:
assert label and label != f"nav.section_{section}" assert label and label != f"nav.section_{section}"
class TestDescriptionCopy:
"""The post-jargon-strip descriptions are intentionally tight one-
liners. Pin them so future drift toward bloated marketing copy
(or an accidentally-empty string) is caught by CI."""
# Roomy upper bound; the tightest description today is ~60 chars
# and the longest is just over 90. ~120 leaves headroom for minor
# copy tweaks without inviting paragraph-length card bodies.
_MAX_DESCRIPTION_CHARS = 120
def test_every_description_is_non_empty(self):
empty = [t.tool_id for t in TOOLS if not t.description.strip()]
assert not empty, f"tools with empty descriptions: {empty}"
def test_every_description_under_max_chars(self):
too_long = [
(t.tool_id, len(t.description))
for t in TOOLS
if len(t.description) > self._MAX_DESCRIPTION_CHARS
]
assert not too_long, (
f"tool descriptions exceed {self._MAX_DESCRIPTION_CHARS} chars: "
f"{too_long}"
)
class TestRenderToolHeaderSmoke:
"""``render_tool_header`` is the helper every tool page now calls in
place of ``st.title(...) + st.caption(...)``. We can't render it
without a Streamlit script context, but we CAN verify it imports
cleanly via the public ``src.gui.components`` surface and resolves
the expected i18n keys for a known tool id."""
def test_importable_from_public_components_package(self):
from src.gui.components import render_tool_header
assert callable(render_tool_header)
def test_listed_in_public_all(self):
# The public ``__all__`` is what per-tool builds key off; a
# removal here would silently break tool pages that import
# from ``src.gui.components`` directly.
from src.gui import components as components_pkg
assert "render_tool_header" in components_pkg.__all__
def test_resolves_expected_i18n_keys_for_known_tool(self):
# The helper reads four pack keys per render:
# ``tools.<id>.page_title``, ``tools.<id>.page_caption``,
# ``tools.<id>.help_md``, plus shared ``help.button_label`` /
# ``help.missing_body``. We don't invoke the helper (no script
# context) — we verify the keys it would touch resolve to
# non-empty strings in both packs.
from src.i18n import t as _t
tool_id = "02_text_cleaner"
for lang in ("en", "es"):
for suffix in ("page_title", "page_caption", "help_md"):
key = f"tools.{tool_id}.{suffix}"
value = _t(key, lang)
assert value and value != key, (
f"render_tool_header({tool_id!r}) "
f"would render the literal key {key!r} in {lang!r}"
)
for key in ("help.button_label", "help.missing_body"):
value = _t(key, lang)
assert value and value != key, (
f"render_tool_header would render the literal key "
f"{key!r} in {lang!r}"
)
class TestReconcilerAndPdfArePresent: class TestReconcilerAndPdfArePresent:
"""The two newest pages were the most likely to be forgotten in """The two newest pages were the most likely to be forgotten in
the registry — pin them explicitly so a regression flagging the registry — pin them explicitly so a regression flagging