Compare commits
3 Commits
4a8961d58a
...
4d8513b1a3
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d8513b1a3 | |||
| ac94208d8f | |||
| 4955fb239b |
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user