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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user