feat(nav,i18n): sticky footer with Back-to-Home + localized tool headers

Two unrelated UX issues addressed in one sweep across all nine tool
pages because they share the same edit surface.

(1) Sticky footer replaces the top + bottom back-link buttons.

Reported: a big white empty footer space at the bottom of every page;
the Back to Home button at the top scrolled out of view on long pages.

New ``render_sticky_footer()`` helper in ``components/_legacy.py``
injects a fixed-position bar at ``bottom: 0`` of the viewport with:

- A border-top so it visually reads as a non-movable bar.
- A semi-transparent background (rgba 0.96 + ``backdrop-filter: blur``)
  so content underneath shows through faintly when the user scrolls.
- A styled ``<a href="home">`` anchor (not an ``st.button``) because
  Streamlit widgets can't be CSS-positioned reliably — Streamlit owns
  the widget's DOM container and re-mounts it on every rerun. A real
  anchor sits exactly where the CSS puts it and triggers Streamlit's
  URL routing to the home page.
- ``padding-bottom: 3.5rem`` on the main container so the last widget
  isn't hidden behind the bar.

Called once per tool page, immediately after ``hide_streamlit_chrome()``
so it renders even on pages that ``st.stop()`` early before any other
content runs. The old top-and-bottom ``back_to_home_link()`` calls are
removed from every tool page; their entry/exit points were dropping
the button when the script short-circuited.

(2) Tool-page headers now localize.

Reported: switching the sidebar language picker to Spanish left the
tool page's title + caption in English. Root cause: every page had
hard-coded ``st.title("✂️ Clean Text")`` / ``st.caption("Trim
whitespace...")`` strings.

Added per-tool ``tools.<id>.page_title`` and
``tools.<id>.page_caption`` keys to ``en.json`` and ``es.json`` for
all nine tools. Routed each page's title/caption call through ``t()``.
Verified: with ``ui_lang=es`` set, the Clean Text page now renders
"✂️ Limpiar texto" + the Spanish caption.

Updated ``tests/gui/test_smoke.py::EXPECTED_SUBSTRINGS`` so the
``es`` column for each tool page asserts the actual Spanish string
(was a duplicate of the English string back when the page bodies
were English-only).

2220 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 00:42:45 +00:00
parent 84e4665ab0
commit 7ad19ac7f4
14 changed files with 194 additions and 88 deletions

View File

@@ -47,6 +47,7 @@ from .activation import ( # noqa: F401 re-exported
__all__ = [
# Shared chrome / pickup
"back_to_home_link",
"render_sticky_footer",
"hide_streamlit_chrome",
"html_download_button",
"local_download_button",

View File

@@ -491,6 +491,88 @@ def local_download_button(
html_download_button = local_download_button
def render_sticky_footer() -> None:
"""Render a slim fixed-position footer at the bottom of the viewport.
Contains a "Back to Home" link that's always visible regardless of
scroll position. Replaces the previous top + bottom-of-page
``back_to_home_link`` buttons: a single sticky bar is more
discoverable, doesn't scroll out of view, and visually reads as a
"non-movable footer" (border-top + slim padding) rather than
just-another-button-at-the-bottom.
Implementation notes:
- Uses a styled ``<a href="home">`` anchor rather than an
``st.button`` because Streamlit widgets can't be CSS-positioned
reliably (their DOM container is owned by Streamlit's renderer
and gets re-mounted on every rerun). A real anchor sits anywhere
we want and triggers Streamlit's URL routing to the home page.
- ``href="home"`` is a relative path so it works behind a reverse
proxy / non-root mount.
- We bump ``padding-bottom`` on the main container so the last
widget isn't hidden behind the fixed bar.
- Theme: ``rgba(255,255,255,0.96)`` background with a thin
semi-transparent border-top. The blur backdrop keeps content
slightly visible underneath. Works against both light and dark
Streamlit themes — explicit colours, not theme variables,
because Streamlit's CSS variable surface is unstable across
minor versions.
"""
import html as _html
label = _html.escape(_t("nav.back_to_home"))
st.markdown(
f"""
<style>
[data-testid="stAppViewBlockContainer"] {{
padding-bottom: 3.5rem !important;
}}
.datatools-sticky-footer {{
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.96);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border-top: 1px solid rgba(49, 51, 63, 0.22);
padding: 0.45rem 1.25rem;
z-index: 9999;
display: flex;
align-items: center;
justify-content: flex-start;
font-family: inherit;
}}
.datatools-sticky-footer a {{
display: inline-block;
color: rgb(38, 39, 48);
text-decoration: none;
padding: 0.3rem 0.85rem;
border-radius: 0.5rem;
border: 1px solid rgba(49, 51, 63, 0.22);
background: rgb(240, 242, 246);
font-size: 14px;
font-weight: 500;
line-height: 1.4;
cursor: pointer;
transition: background 0.12s ease, border-color 0.12s ease;
}}
.datatools-sticky-footer a:hover {{
background: rgb(225, 228, 235);
border-color: rgba(49, 51, 63, 0.35);
}}
.datatools-sticky-footer a:active {{
background: rgb(210, 214, 222);
}}
</style>
<div class="datatools-sticky-footer">
<a href="home" target="_self">{label}</a>
</div>
""",
unsafe_allow_html=True,
)
def back_to_home_link(*, key: str = "_back_to_home_link") -> None:
"""Render a "← Back to Home" affordance on a tool page.

View File

@@ -19,6 +19,7 @@ from src.core.io import read_file, list_sheets, detect_encoding, detect_delimite
from src.gui.components import (
apply_review_decisions,
back_to_home_link,
render_sticky_footer,
config_panel,
hide_streamlit_chrome,
html_download_button,
@@ -27,10 +28,11 @@ from src.gui.components import (
require_feature_or_render_upgrade,
results_summary,
)
from src.i18n import t
from src.license import FeatureFlag
hide_streamlit_chrome()
back_to_home_link()
render_sticky_footer()
require_feature_or_render_upgrade(FeatureFlag.DEDUPLICATOR)
# ---------------------------------------------------------------------------
@@ -55,8 +57,8 @@ for key, default in _DEFAULTS.items():
# Header
# ---------------------------------------------------------------------------
st.title("🔍 Find Duplicates")
st.caption("Find and remove duplicate rows in CSV, delimited text, and Excel files.")
st.title(t("tools.01_deduplicator.page_title"))
st.caption(t("tools.01_deduplicator.page_caption"))
# ---------------------------------------------------------------------------
@@ -399,8 +401,6 @@ else:
# Footer
# ---------------------------------------------------------------------------
back_to_home_link(key="_back_to_home_link_bottom")
st.divider()
st.caption(
"Runs locally. Your data never leaves this computer. "

View File

@@ -16,12 +16,14 @@ if str(_project_root) not in sys.path:
from src.gui.components import (
back_to_home_link,
render_sticky_footer,
hide_streamlit_chrome,
html_download_button,
pickup_or_upload,
render_hidden_aware_preview,
require_feature_or_render_upgrade,
)
from src.i18n import t
from src.license import FeatureFlag
from src.core.text_clean import (
PRESETS,
@@ -32,7 +34,7 @@ from src.core.text_clean import (
)
hide_streamlit_chrome()
back_to_home_link()
render_sticky_footer()
require_feature_or_render_upgrade(FeatureFlag.TEXT_CLEANER)
@@ -40,11 +42,8 @@ require_feature_or_render_upgrade(FeatureFlag.TEXT_CLEANER)
# Header
# ---------------------------------------------------------------------------
st.title("✂️ Clean Text")
st.caption(
"Trim whitespace, fold smart quotes, strip invisible characters, and "
"normalize line endings. Runs locally — your data never leaves this computer."
)
st.title(t("tools.02_text_cleaner.page_title"))
st.caption(t("tools.02_text_cleaner.page_caption"))
# ---------------------------------------------------------------------------
# File upload
@@ -379,8 +378,6 @@ with dl_c:
mime="application/json",
)
back_to_home_link(key="_back_to_home_link_bottom")
st.divider()
st.caption("Runs locally. Your data never leaves this computer. | DataTools v3.0")

View File

@@ -16,11 +16,13 @@ if str(_project_root) not in sys.path:
from src.gui.components import (
back_to_home_link,
render_sticky_footer,
hide_streamlit_chrome,
html_download_button,
pickup_or_upload,
require_feature_or_render_upgrade,
)
from src.i18n import t
from src.core.format_standardize import (
PRESETS,
FieldType,
@@ -30,7 +32,7 @@ from src.core.format_standardize import (
from src.license import FeatureFlag
hide_streamlit_chrome()
back_to_home_link()
render_sticky_footer()
require_feature_or_render_upgrade(FeatureFlag.FORMAT_STANDARDIZER)
@@ -38,12 +40,8 @@ require_feature_or_render_upgrade(FeatureFlag.FORMAT_STANDARDIZER)
# Header
# ---------------------------------------------------------------------------
st.title("📐 Standardize Formats")
st.caption(
"Canonicalize dates, phone numbers, currency, names, addresses, and "
"booleans on a per-column basis. Runs locally — your data never leaves "
"this computer."
)
st.title(t("tools.03_format_standardizer.page_title"))
st.caption(t("tools.03_format_standardizer.page_caption"))
# ---------------------------------------------------------------------------
@@ -650,8 +648,6 @@ with dl_c:
mime="application/json",
)
back_to_home_link(key="_back_to_home_link_bottom")
st.divider()
st.caption("Runs locally. Your data never leaves this computer. | DataTools v3.0")

View File

@@ -16,11 +16,13 @@ if str(_project_root) not in sys.path:
from src.gui.components import (
back_to_home_link,
render_sticky_footer,
hide_streamlit_chrome,
html_download_button,
pickup_or_upload,
require_feature_or_render_upgrade,
)
from src.i18n import t
from src.core.missing import (
DEFAULT_SENTINELS,
MissingOptions,
@@ -31,7 +33,7 @@ from src.core.missing import (
from src.license import FeatureFlag
hide_streamlit_chrome()
back_to_home_link()
render_sticky_footer()
require_feature_or_render_upgrade(FeatureFlag.MISSING_HANDLER)
@@ -39,11 +41,8 @@ require_feature_or_render_upgrade(FeatureFlag.MISSING_HANDLER)
# Header
# ---------------------------------------------------------------------------
st.title("🕳️ Fix Missing Values")
st.caption(
"Detect disguised nulls, profile missingness, and apply imputation or "
"drop strategies. Runs locally — your data never leaves this computer."
)
st.title(t("tools.04_missing_handler.page_title"))
st.caption(t("tools.04_missing_handler.page_caption"))
# ---------------------------------------------------------------------------
@@ -412,8 +411,6 @@ with dl_c:
mime="application/json",
)
back_to_home_link(key="_back_to_home_link_bottom")
st.divider()
st.caption("Runs locally. Your data never leaves this computer. | DataTools v3.0")

View File

@@ -16,11 +16,13 @@ if str(_project_root) not in sys.path:
from src.gui.components import (
back_to_home_link,
render_sticky_footer,
hide_streamlit_chrome,
html_download_button,
pickup_or_upload,
require_feature_or_render_upgrade,
)
from src.i18n import t
from src.core.column_mapper import (
MapOptions,
PRESETS,
@@ -32,7 +34,7 @@ from src.core.column_mapper import (
from src.license import FeatureFlag
hide_streamlit_chrome()
back_to_home_link()
render_sticky_footer()
require_feature_or_render_upgrade(FeatureFlag.COLUMN_MAPPER)
@@ -40,11 +42,8 @@ require_feature_or_render_upgrade(FeatureFlag.COLUMN_MAPPER)
# Header
# ---------------------------------------------------------------------------
st.title("🗂️ Map Columns")
st.caption(
"Rename columns, enforce a target schema, and coerce types. Runs locally — "
"your data never leaves this computer."
)
st.title(t("tools.05_column_mapper.page_title"))
st.caption(t("tools.05_column_mapper.page_caption"))
# ---------------------------------------------------------------------------
@@ -456,8 +455,6 @@ with dl_c:
mime="application/json",
)
back_to_home_link(key="_back_to_home_link_bottom")
st.divider()
st.caption("Runs locally. Your data never leaves this computer. | DataTools v3.0")

View File

@@ -13,21 +13,23 @@ if str(_project_root) not in sys.path:
from src.gui.components import (
back_to_home_link,
render_sticky_footer,
hide_streamlit_chrome,
require_feature_or_render_upgrade,
)
from src.i18n import t
from src.license import FeatureFlag
hide_streamlit_chrome()
back_to_home_link()
render_sticky_footer()
require_feature_or_render_upgrade(FeatureFlag.OUTLIER_DETECTOR)
# ---------------------------------------------------------------------------
# Header
# ---------------------------------------------------------------------------
st.title("📊 Find Unusual Values")
st.caption("Detect and handle outliers in numeric columns.")
st.title(t("tools.06_outlier_detector.page_title"))
st.caption(t("tools.06_outlier_detector.page_caption"))
st.info("This tool is under development.")
@@ -96,10 +98,9 @@ st.button("Detect Outliers", type="primary", use_container_width=True, disabled=
# Footer
# ---------------------------------------------------------------------------
back_to_home_link(key="_back_to_home_link_bottom")
st.divider()
st.caption(
"Runs locally. Your data never leaves this computer. "
"| DataTools v3.0"
)

View File

@@ -13,21 +13,23 @@ if str(_project_root) not in sys.path:
from src.gui.components import (
back_to_home_link,
render_sticky_footer,
hide_streamlit_chrome,
require_feature_or_render_upgrade,
)
from src.i18n import t
from src.license import FeatureFlag
hide_streamlit_chrome()
back_to_home_link()
render_sticky_footer()
require_feature_or_render_upgrade(FeatureFlag.MULTI_FILE_MERGER)
# ---------------------------------------------------------------------------
# Header
# ---------------------------------------------------------------------------
st.title("📎 Combine Files")
st.caption("Combine multiple CSV and Excel files into one dataset.")
st.title(t("tools.07_multi_file_merger.page_title"))
st.caption(t("tools.07_multi_file_merger.page_caption"))
st.info("This tool is under development.")
@@ -94,10 +96,9 @@ st.button("Merge Files", type="primary", use_container_width=True, disabled=True
# Footer
# ---------------------------------------------------------------------------
back_to_home_link(key="_back_to_home_link_bottom")
st.divider()
st.caption(
"Runs locally. Your data never leaves this computer. "
"| DataTools v3.0"
)

View File

@@ -13,21 +13,23 @@ if str(_project_root) not in sys.path:
from src.gui.components import (
back_to_home_link,
render_sticky_footer,
hide_streamlit_chrome,
require_feature_or_render_upgrade,
)
from src.i18n import t
from src.license import FeatureFlag
hide_streamlit_chrome()
back_to_home_link()
render_sticky_footer()
require_feature_or_render_upgrade(FeatureFlag.VALIDATOR_REPORTER)
# ---------------------------------------------------------------------------
# Header
# ---------------------------------------------------------------------------
st.title("✅ Quality Check")
st.caption("Validate data against rules and generate quality reports.")
st.title(t("tools.08_validator_reporter.page_title"))
st.caption(t("tools.08_validator_reporter.page_caption"))
st.info("This tool is under development.")
@@ -101,10 +103,9 @@ st.button("Validate & Generate Report", type="primary", use_container_width=True
# Footer
# ---------------------------------------------------------------------------
back_to_home_link(key="_back_to_home_link_bottom")
st.divider()
st.caption(
"Runs locally. Your data never leaves this computer. "
"| DataTools v3.0"
)

View File

@@ -16,11 +16,13 @@ if str(_project_root) not in sys.path:
from src.gui.components import (
back_to_home_link,
render_sticky_footer,
hide_streamlit_chrome,
html_download_button,
pickup_or_upload,
require_feature_or_render_upgrade,
)
from src.i18n import t
from src.core.pipeline import (
Pipeline,
SOFT_DEPENDENCIES,
@@ -33,7 +35,7 @@ from src.core.pipeline import (
from src.license import FeatureFlag
hide_streamlit_chrome()
back_to_home_link()
render_sticky_footer()
require_feature_or_render_upgrade(FeatureFlag.PIPELINE_RUNNER)
@@ -41,11 +43,8 @@ require_feature_or_render_upgrade(FeatureFlag.PIPELINE_RUNNER)
# Header
# ---------------------------------------------------------------------------
st.title("⚙️ Automated Workflows")
st.caption(
"Chain DataTools cleaning steps into one repeatable workflow. The "
"pipeline recommends an order; you stay in control."
)
st.title(t("tools.09_pipeline_runner.page_title"))
st.caption(t("tools.09_pipeline_runner.page_caption"))
# ---------------------------------------------------------------------------
@@ -414,8 +413,6 @@ with dl_c:
mime="application/json",
)
back_to_home_link(key="_back_to_home_link_bottom")
st.divider()
st.caption("Runs locally. Your data never leaves this computer. | DataTools v3.0")