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

@@ -347,7 +347,7 @@ body, .stApp {
border-radius: var(--dt-r-sm) !important;
}
/* 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
and visually doubles up on filenames. The dropzone's borderless
``+`` 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:
"""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(_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
# any page renders. Idempotent — subsequent calls are no-ops.
# Wrapped because a broken audit log MUST NOT take the GUI down.