chore(ui): rename Upload → Import in user-facing strings

DataTools is local-first — "Upload" reads like "send data somewhere
remote", which contradicts the product positioning. Sweep replaces
the user-visible term throughout the UI:

- ``src/i18n/packs/en.json`` + ``es.json``: all ``upload.*`` strings
  (heading, intro, uploader labels, empty state, switch-back, etc.)
  and ``gate.default_name``. The ``intro_multi`` "no upload anywhere"
  phrasing dropped the verb entirely — now reads "nothing leaves
  this computer".
- All 9 tool pages: ``st.file_uploader(label="Upload …")`` →
  ``"Import …"``; matching ``st.info("Upload a …")`` empty-state
  banners; ``help="Upload …"`` strings on disabled uploaders.
- ``9_Pipeline_Runner`` + ``5_Column_Mapper``: radio-option text
  ``"Upload schema/pipeline JSON"`` → ``"Import …"`` plus the
  ``.startswith("Upload")`` branch guards that read those values.
- ``_home.py``: "**Uploaded files**" → "**Imported files**".
- ``app_demo.py``: "Uploaded file is …" → "Imported file is …".

Internal identifiers left untouched: function names
(``pickup_or_upload``, ``_StashedUpload``), session-state keys
(``home_upload``, ``home_uploads``, ``home_uploaded_*``,
``merger_file_upload``), audit-log event category (``"upload"``),
Streamlit testid CSS selectors. None of those are visible to the
user.

The file_uploader's dropzone button text is a baked-in React
literal that Streamlit's ``label=`` doesn't reach; rewritten at the
DOM level with a small ``_RENAME_UPLOAD_BUTTON_JS`` snippet shipped
through ``st.iframe`` (same pattern the sticky footer uses to mount
on ``<body>``). A ``MutationObserver`` on the parent document re-
applies the swap when Streamlit remounts the dropzone after file
add/remove or page navigation, throttled via ``requestAnimationFrame``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 23:48:31 +00:00
parent 3c4b80895e
commit 444dffbc63
14 changed files with 98 additions and 44 deletions

View File

@@ -156,7 +156,7 @@ def _home_page() -> None:
# interleaving widget-key updates with state changes. # interleaving widget-key updates with state changes.
if home_uploads: if home_uploads:
import hashlib import hashlib
st.markdown("**Uploaded files**") st.markdown("**Imported files**")
to_remove: str | None = None to_remove: str | None = None
for name in list(home_uploads.keys()): for name in list(home_uploads.keys()):
digest = hashlib.sha1( digest = hashlib.sha1(

View File

@@ -239,7 +239,7 @@ def _read_uploaded(uploaded_file) -> tuple[pd.DataFrame, list[str]]:
size_mb = len(raw) / 1024 / 1024 size_mb = len(raw) / 1024 / 1024
if size_mb > DEMO_FILE_CAP_MB: if size_mb > DEMO_FILE_CAP_MB:
warnings.append( warnings.append(
f"Uploaded file is {size_mb:.1f} MB — demo capped at " f"Imported file is {size_mb:.1f} MB — demo capped at "
f"{DEMO_FILE_CAP_MB} MB. The paid product has no size limit." f"{DEMO_FILE_CAP_MB} MB. The paid product has no size limit."
) )
return sample_df.copy(), warnings return sample_df.copy(), warnings

View File

@@ -347,7 +347,7 @@ body, .stApp {
border-radius: var(--dt-r-sm) !important; border-radius: var(--dt-r-sm) !important;
} }
/* Hide Streamlit's built-in compact file-chip row once files exist — /* Hide Streamlit's built-in compact file-chip row once files exist —
the home page renders its own canonical "Uploaded files" list with the home page renders its own canonical "Imported files" list with
a Remove button beneath the uploader, so the chip row is redundant a Remove button beneath the uploader, so the chip row is redundant
and visually doubles up on filenames. The dropzone's borderless and visually doubles up on filenames. The dropzone's borderless
``+`` button is left in place as the "add more files" affordance. ``+`` button is left in place as the "add more files" affordance.
@@ -459,6 +459,56 @@ div[data-testid="stContainer"][data-border="true"] {
""" """
# Streamlit ships the file_uploader's dropzone button with hard-coded
# "Upload" text (it's a text node baked into the React component, not
# a Streamlit i18n string we can override from Python). Our product
# positioning is local-first, so the word "Upload" is misleading. This
# script walks the dropzone buttons after first paint and rewrites the
# label to "Import" — and re-runs on Streamlit's component-rerender
# DOM mutations so the swap survives navigation and reruns.
_RENAME_UPLOAD_BUTTON_JS = """
<script>
(function () {
function swap(doc) {
var dropzones = doc.querySelectorAll('[data-testid="stFileUploaderDropzone"]');
dropzones.forEach(function (dz) {
var btn = dz.querySelector('button');
if (!btn) return;
// The label is a text node directly inside the outer label span;
// walk all text nodes and replace any exact "Upload".
var walker = doc.createTreeWalker(btn, NodeFilter.SHOW_TEXT, null, false);
var node;
while ((node = walker.nextNode())) {
if (node.nodeValue && node.nodeValue.trim() === 'Upload') {
node.nodeValue = node.nodeValue.replace('Upload', 'Import');
}
}
});
}
try {
var doc = window.parent.document;
swap(doc);
// Streamlit re-mounts dropzone subtrees on file changes / page
// switches — observe the parent doc and re-apply the swap when
// new ``stFileUploaderDropzone`` nodes appear. Throttled via
// requestAnimationFrame so a burst of mutations is one swap.
var raf = 0;
var obs = new (doc.defaultView || window).MutationObserver(function () {
if (raf) return;
raf = (doc.defaultView || window).requestAnimationFrame(function () {
raf = 0;
swap(doc);
});
});
obs.observe(doc.body, { childList: true, subtree: true });
} catch (e) {
swap(document);
}
})();
</script>
"""
def hide_streamlit_chrome(*, gate_license: bool = True) -> None: def hide_streamlit_chrome(*, gate_license: bool = True) -> None:
"""Inject CSS to hide Streamlit's default header, menu, and footer. """Inject CSS to hide Streamlit's default header, menu, and footer.
@@ -477,6 +527,10 @@ def hide_streamlit_chrome(*, gate_license: bool = True) -> None:
""" """
st.markdown(_HIDE_CHROME_CSS, unsafe_allow_html=True) st.markdown(_HIDE_CHROME_CSS, unsafe_allow_html=True)
st.markdown(_DESIGN_TOKENS_CSS, unsafe_allow_html=True) st.markdown(_DESIGN_TOKENS_CSS, unsafe_allow_html=True)
# ``st.markdown`` doesn't execute embedded scripts; ship the
# Upload→Import rewriter through an iframe component the same way
# the sticky footer mounts on ``<body>``.
st.iframe(_RENAME_UPLOAD_BUTTON_JS, height=1)
# Stamp a session-start record into the audit log the first time # Stamp a session-start record into the audit log the first time
# any page renders. Idempotent — subsequent calls are no-ops. # any page renders. Idempotent — subsequent calls are no-ops.
# Wrapped because a broken audit log MUST NOT take the GUI down. # Wrapped because a broken audit log MUST NOT take the GUI down.

View File

@@ -69,7 +69,7 @@ st.caption(t("tools.01_deduplicator.page_caption"))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
uploaded = pickup_or_upload( uploaded = pickup_or_upload(
label="Upload CSV or Excel file", label="Import CSV or Excel file",
key="dedup_file_upload", key="dedup_file_upload",
types=["csv", "tsv", "xlsx", "xls"], types=["csv", "tsv", "xlsx", "xls"],
help="Supports CSV, TSV, and Excel files. Encoding and delimiters are auto-detected.", help="Supports CSV, TSV, and Excel files. Encoding and delimiters are auto-detected.",
@@ -399,7 +399,7 @@ if uploaded is not None:
else: else:
# No file uploaded — show placeholder # No file uploaded — show placeholder
st.info("Upload a file to get started.") st.info("Import a file to get started.")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@@ -53,13 +53,13 @@ st.caption(t("tools.02_text_cleaner.page_caption"))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
uploaded = pickup_or_upload( uploaded = pickup_or_upload(
label="Upload CSV or Excel file", label="Import CSV or Excel file",
key="textclean_file_upload", key="textclean_file_upload",
types=["csv", "tsv", "xlsx", "xls"], types=["csv", "tsv", "xlsx", "xls"],
) )
if uploaded is None: if uploaded is None:
st.info("Upload a CSV, TSV, or Excel file to begin.") st.info("Import a CSV, TSV, or Excel file to begin.")
st.stop() st.stop()

View File

@@ -52,13 +52,13 @@ st.caption(t("tools.03_format_standardizer.page_caption"))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
uploaded = pickup_or_upload( uploaded = pickup_or_upload(
label="Upload CSV or Excel file", label="Import CSV or Excel file",
key="fmtstd_file_upload", key="fmtstd_file_upload",
types=["csv", "tsv", "xlsx", "xls"], types=["csv", "tsv", "xlsx", "xls"],
) )
if uploaded is None: if uploaded is None:
st.info("Upload a CSV, TSV, or Excel file to begin.") st.info("Import a CSV, TSV, or Excel file to begin.")
st.stop() st.stop()

View File

@@ -53,13 +53,13 @@ st.caption(t("tools.04_missing_handler.page_caption"))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
uploaded = pickup_or_upload( uploaded = pickup_or_upload(
label="Upload CSV or Excel file", label="Import CSV or Excel file",
key="missing_file_upload", key="missing_file_upload",
types=["csv", "tsv", "xlsx", "xls"], types=["csv", "tsv", "xlsx", "xls"],
) )
if uploaded is None: if uploaded is None:
st.info("Upload a CSV, TSV, or Excel file to begin.") st.info("Import a CSV, TSV, or Excel file to begin.")
st.stop() st.stop()

View File

@@ -54,13 +54,13 @@ st.caption(t("tools.05_column_mapper.page_caption"))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
uploaded = pickup_or_upload( uploaded = pickup_or_upload(
label="Upload CSV or Excel file", label="Import CSV or Excel file",
key="colmap_file_upload", key="colmap_file_upload",
types=["csv", "tsv", "xlsx", "xls"], types=["csv", "tsv", "xlsx", "xls"],
) )
if uploaded is None: if uploaded is None:
st.info("Upload a CSV, TSV, or Excel file to begin.") st.info("Import a CSV, TSV, or Excel file to begin.")
st.stop() st.stop()
@@ -122,12 +122,12 @@ with st.expander("Options", expanded=not _has_result):
"How would you like to define the target schema?", "How would you like to define the target schema?",
[ [
"Build interactively (start from current columns)", "Build interactively (start from current columns)",
"Upload schema JSON", "Import schema JSON",
"Skip (rename / coerce only — no schema)", "Skip (rename / coerce only — no schema)",
], ],
index=0, index=0,
help=( help=(
"An interactive build is fastest for one-off cleanup. Upload a JSON " "An interactive build is fastest for one-off cleanup. Import a JSON "
"when you have a fixed contract (a CRM import format, db schema). " "when you have a fixed contract (a CRM import format, db schema). "
"Skip when you only want to rename or coerce specific columns." "Skip when you only want to rename or coerce specific columns."
), ),
@@ -135,7 +135,7 @@ with st.expander("Options", expanded=not _has_result):
schema: TargetSchema | None = None schema: TargetSchema | None = None
if schema_mode.startswith("Upload"): if schema_mode.startswith("Import"):
schema_file = st.file_uploader( schema_file = st.file_uploader(
"Schema JSON", "Schema JSON",
type=["json"], type=["json"],

View File

@@ -57,9 +57,9 @@ st.divider()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
uploaded = st.file_uploader( uploaded = st.file_uploader(
"Upload CSV or Excel file", "Import CSV or Excel file",
type=["csv", "tsv", "xlsx", "xls"], type=["csv", "tsv", "xlsx", "xls"],
help="Upload a file to preview. Processing is not yet available.", help="Import a file to preview. Processing is not yet available.",
key="outlier_file_upload", key="outlier_file_upload",
) )

View File

@@ -42,7 +42,7 @@ st.info("This tool is under development.")
st.markdown(""" st.markdown("""
**Features:** **Features:**
- Upload multiple CSV/Excel files at once - Import multiple CSV/Excel files at once
- Automatic schema alignment (matching columns by name) - Automatic schema alignment (matching columns by name)
- Append mode: stack files vertically (union) - Append mode: stack files vertically (union)
- Join mode: merge files on shared key columns - Join mode: merge files on shared key columns
@@ -57,10 +57,10 @@ st.divider()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
uploaded_files = st.file_uploader( uploaded_files = st.file_uploader(
"Upload CSV or Excel files", "Import CSV or Excel files",
type=["csv", "tsv", "xlsx", "xls"], type=["csv", "tsv", "xlsx", "xls"],
accept_multiple_files=True, accept_multiple_files=True,
help="Upload multiple files to preview. Processing is not yet available.", help="Import multiple files to preview. Processing is not yet available.",
key="merger_file_upload", key="merger_file_upload",
) )

View File

@@ -57,9 +57,9 @@ st.divider()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
uploaded = st.file_uploader( uploaded = st.file_uploader(
"Upload CSV or Excel file", "Import CSV or Excel file",
type=["csv", "tsv", "xlsx", "xls"], type=["csv", "tsv", "xlsx", "xls"],
help="Upload a file to preview. Processing is not yet available.", help="Import a file to preview. Processing is not yet available.",
key="validator_file_upload", key="validator_file_upload",
) )

View File

@@ -55,13 +55,13 @@ st.caption(t("tools.09_pipeline_runner.page_caption"))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
uploaded = pickup_or_upload( uploaded = pickup_or_upload(
label="Upload CSV or Excel file", label="Import CSV or Excel file",
key="pipeline_file_upload", key="pipeline_file_upload",
types=["csv", "tsv", "xlsx", "xls"], types=["csv", "tsv", "xlsx", "xls"],
) )
if uploaded is None: if uploaded is None:
st.info("Upload a CSV, TSV, or Excel file to begin.") st.info("Import a CSV, TSV, or Excel file to begin.")
st.stop() st.stop()
@@ -119,7 +119,7 @@ with st.expander("Options", expanded=not _has_result):
[ [
"Use the recommended default (text-clean → format → missing → dedup)", "Use the recommended default (text-clean → format → missing → dedup)",
"Build interactively", "Build interactively",
"Upload a saved pipeline JSON", "Import a saved pipeline JSON",
], ],
index=0, index=0,
) )
@@ -143,7 +143,7 @@ with st.expander("Options", expanded=not _has_result):
} }
for s in default.steps for s in default.steps
]) ])
elif mode.startswith("Upload"): elif mode.startswith("Import"):
pipeline_file = st.file_uploader( pipeline_file = st.file_uploader(
"Pipeline JSON", type=["json"], key="pipeline_upload", "Pipeline JSON", type=["json"], key="pipeline_upload",
) )

View File

@@ -15,23 +15,23 @@
"coming_soon": "Coming Soon" "coming_soon": "Coming Soon"
}, },
"upload": { "upload": {
"heading": "📤 Upload one or more files to start", "heading": "📤 Import one or more files to start",
"intro": "Optional: scan an uploaded file for data quality issues and see which tools can fix each one. Skip if you already know what you need.", "intro": "Optional: scan an imported file for data quality issues and see which tools can fix each one. Skip if you already know what you need.",
"limits": "**Up to 1.5 GB.** Formats: CSV, TSV, XLSX, XLS. Delimiters auto-detected: comma, tab, semicolon, pipe. Encodings auto-detected: UTF-8 (with/without BOM), UTF-16, cp1252, Latin-1/9, cp1250, ISO-8859-2, cp1251, KOI8-R, Mac Roman, Shift_JIS, GB18030, Big5, EUC-KR — and override on the Review page.", "limits": "**Up to 1.5 GB.** Formats: CSV, TSV, XLSX, XLS. Delimiters auto-detected: comma, tab, semicolon, pipe. Encodings auto-detected: UTF-8 (with/without BOM), UTF-16, cp1252, Latin-1/9, cp1250, ISO-8859-2, cp1251, KOI8-R, Mac Roman, Shift_JIS, GB18030, Big5, EUC-KR — and override on the Review page.",
"uploader_label": "Upload CSV or Excel", "uploader_label": "Import CSV or Excel",
"uploader_help": "Up to 1.5 GB. Comma / tab / semicolon / pipe delimiters all auto-detected. Encoding auto-detected with override on the Review page if needed.", "uploader_help": "Up to 1.5 GB. Comma / tab / semicolon / pipe delimiters all auto-detected. Encoding auto-detected with override on the Review page if needed.",
"run_button": "Run analysis", "run_button": "Run analysis",
"skip_button": "Skip", "skip_button": "Skip",
"scanning": "Scanning…", "scanning": "Scanning…",
"skipped_notice": "Analysis skipped. Open any tool below to start working.", "skipped_notice": "Analysis skipped. Open any tool below to start working.",
"using_session_file": "Using **{name}** from the upload screen.", "using_session_file": "Using **{name}** from the import screen.",
"use_different_file": "Use a different file", "use_different_file": "Use a different file",
"switch_back": "Switch back to upload-screen file", "switch_back": "Switch back to import-screen file",
"pickup_caption": "Up to 1.5 GB. Delimiters auto-detected: comma, tab, semicolon, pipe. Encoding auto-detected (UTF-8 / UTF-16 / cp1252 / Latin-1 family / cp1250 / cp1251 / KOI8-R / Mac Roman / Shift_JIS / GB18030 / Big5 / EUC-KR), with override on the Review page.", "pickup_caption": "Up to 1.5 GB. Delimiters auto-detected: comma, tab, semicolon, pipe. Encoding auto-detected (UTF-8 / UTF-16 / cp1252 / Latin-1 family / cp1250 / cp1251 / KOI8-R / Mac Roman / Shift_JIS / GB18030 / Big5 / EUC-KR), with override on the Review page.",
"intro_multi": "Drop files below. Each one is analyzed locally — no upload anywhere.", "intro_multi": "Drop files below. Each one is analyzed locally — nothing leaves this computer.",
"uploader_label_multi": "Upload CSV, TSV, or Excel files", "uploader_label_multi": "Import CSV, TSV, or Excel files",
"clear_results": "Clear results", "clear_results": "Clear results",
"empty_state": "Upload one or more files to begin. Your data never leaves this computer." "empty_state": "Import one or more files to begin. Your data never leaves this computer."
}, },
"findings": { "findings": {
"header": "Detected issues", "header": "Detected issues",
@@ -44,7 +44,7 @@
}, },
"gate": { "gate": {
"warning": "**{name}** must pass the CSV-normalization gate before you can use this tool. Open the Review page to apply the fixes our analyzer recommends.", "warning": "**{name}** must pass the CSV-normalization gate before you can use this tool. Open the Review page to apply the fixes our analyzer recommends.",
"default_name": "the uploaded file", "default_name": "the imported file",
"open_review": "Go to Review & Normalize" "open_review": "Go to Review & Normalize"
}, },
"quit": { "quit": {

View File

@@ -15,23 +15,23 @@
"coming_soon": "Próximamente" "coming_soon": "Próximamente"
}, },
"upload": { "upload": {
"heading": "📤 Sube uno o más archivos para empezar", "heading": "📤 Importa uno o más archivos para empezar",
"intro": "Opcional: analiza un archivo para detectar problemas de calidad de datos y ver qué herramientas pueden corregir cada uno. Sáltalo si ya sabes lo que necesitas.", "intro": "Opcional: analiza un archivo para detectar problemas de calidad de datos y ver qué herramientas pueden corregir cada uno. Sáltalo si ya sabes lo que necesitas.",
"limits": "**Hasta 1,5 GB.** Formatos: CSV, TSV, XLSX, XLS. Delimitadores detectados automáticamente: coma, tabulador, punto y coma, barra vertical. Codificaciones detectadas automáticamente: UTF-8 (con/sin BOM), UTF-16, cp1252, Latin-1/9, cp1250, ISO-8859-2, cp1251, KOI8-R, Mac Roman, Shift_JIS, GB18030, Big5, EUC-KR — y se pueden sustituir desde la página Revisar.", "limits": "**Hasta 1,5 GB.** Formatos: CSV, TSV, XLSX, XLS. Delimitadores detectados automáticamente: coma, tabulador, punto y coma, barra vertical. Codificaciones detectadas automáticamente: UTF-8 (con/sin BOM), UTF-16, cp1252, Latin-1/9, cp1250, ISO-8859-2, cp1251, KOI8-R, Mac Roman, Shift_JIS, GB18030, Big5, EUC-KR — y se pueden sustituir desde la página Revisar.",
"uploader_label": "Sube un archivo CSV o Excel", "uploader_label": "Importa un archivo CSV o Excel",
"uploader_help": "Hasta 1,5 GB. Delimitadores coma / tabulador / punto y coma / barra vertical detectados automáticamente. Codificación detectada automáticamente, con opción de sustituirla en la página Revisar.", "uploader_help": "Hasta 1,5 GB. Delimitadores coma / tabulador / punto y coma / barra vertical detectados automáticamente. Codificación detectada automáticamente, con opción de sustituirla en la página Revisar.",
"run_button": "Ejecutar análisis", "run_button": "Ejecutar análisis",
"skip_button": "Omitir", "skip_button": "Omitir",
"scanning": "Analizando…", "scanning": "Analizando…",
"skipped_notice": "Análisis omitido. Abre cualquier herramienta de abajo para empezar a trabajar.", "skipped_notice": "Análisis omitido. Abre cualquier herramienta de abajo para empezar a trabajar.",
"using_session_file": "Usando **{name}** de la pantalla de carga.", "using_session_file": "Usando **{name}** de la pantalla de importación.",
"use_different_file": "Usar otro archivo", "use_different_file": "Usar otro archivo",
"switch_back": "Volver al archivo de la pantalla de carga", "switch_back": "Volver al archivo de la pantalla de importación",
"pickup_caption": "Hasta 1,5 GB. Delimitadores detectados automáticamente: coma, tabulador, punto y coma, barra vertical. Codificación detectada automáticamente (UTF-8 / UTF-16 / cp1252 / familia Latin-1 / cp1250 / cp1251 / KOI8-R / Mac Roman / Shift_JIS / GB18030 / Big5 / EUC-KR), con opción de sustituirla en la página Revisar.", "pickup_caption": "Hasta 1,5 GB. Delimitadores detectados automáticamente: coma, tabulador, punto y coma, barra vertical. Codificación detectada automáticamente (UTF-8 / UTF-16 / cp1252 / familia Latin-1 / cp1250 / cp1251 / KOI8-R / Mac Roman / Shift_JIS / GB18030 / Big5 / EUC-KR), con opción de sustituirla en la página Revisar.",
"intro_multi": "Suelta archivos abajo. Cada uno se analiza localmente — no se sube a ningún lado.", "intro_multi": "Suelta archivos abajo. Cada uno se analiza localmente — nada sale de este equipo.",
"uploader_label_multi": "Sube archivos CSV, TSV o Excel", "uploader_label_multi": "Importa archivos CSV, TSV o Excel",
"clear_results": "Borrar resultados", "clear_results": "Borrar resultados",
"empty_state": "Sube uno o más archivos para empezar. Tus datos nunca salen de este equipo." "empty_state": "Importa uno o más archivos para empezar. Tus datos nunca salen de este equipo."
}, },
"findings": { "findings": {
"header": "Problemas detectados", "header": "Problemas detectados",
@@ -44,7 +44,7 @@
}, },
"gate": { "gate": {
"warning": "**{name}** debe pasar la verificación de normalización CSV antes de poder usar esta herramienta. Abre la página Revisar para aplicar las correcciones recomendadas por el analizador.", "warning": "**{name}** debe pasar la verificación de normalización CSV antes de poder usar esta herramienta. Abre la página Revisar para aplicar las correcciones recomendadas por el analizador.",
"default_name": "el archivo cargado", "default_name": "el archivo importado",
"open_review": "Ir a Revisar y Normalizar" "open_review": "Ir a Revisar y Normalizar"
}, },
"quit": { "quit": {