Spaces:
Sleeping
Sleeping
| """OphthalmoCapture — Internationalization (i18n) | |
| Centralized UI strings with session-state-based language selection. | |
| All components call t(key) to get translated strings. | |
| """ | |
| import streamlit as st | |
| SUPPORTED_LANGUAGES = {"es": "Español", "en": "English"} | |
| DEFAULT_LANGUAGE = "es" | |
| def _get_lang() -> str: | |
| """Return the active UI language code from session state.""" | |
| return st.session_state.get("ui_language", DEFAULT_LANGUAGE) | |
| _STRINGS = { | |
| "es": { | |
| # App | |
| "app_subtitle": "Sistema de Etiquetado Médico Oftalmológico", | |
| # Sidebar | |
| "settings": "⚙️ Configuración", | |
| "doctor_name": "👨⚕️ Nombre del Doctor", | |
| "whisper_model": "Modelo Whisper", | |
| "dictation_language": "Idioma de dictado", | |
| "current_session": "📊 Sesión Actual", | |
| "db_type": "Base de datos", | |
| "images_loaded": "Imágenes cargadas", | |
| "labeled_count": "Etiquetadas", | |
| "no_images": "No hay imágenes en la sesión.", | |
| "history": "🗄️ Historial", | |
| "search_image": "🔍 Buscar por imagen", | |
| "no_records": "Sin registros.", | |
| "label_header": "Etiqueta", | |
| "doctor_header": "Doctor", | |
| "no_transcription": "Sin transcripción", | |
| "end_session": "� Cerrar sesión", | |
| "undownloaded_warning": "⚠️ Datos no descargados", | |
| "timeout_in": "⏱️ Timeout en", | |
| "confirm_delete": "¿Está seguro? **Se cerrará la sesión y todos los datos se eliminarán permanentemente.**", | |
| "yes_delete": "✅ Sí, cerrar sesión", | |
| "cancel": "❌ Cancelar", | |
| "logout": "🚪 Cerrar sesión", | |
| # Upload | |
| "upload_images": "📤 Subir imágenes médicas", | |
| "upload_help_formats": "Formatos aceptados", | |
| "upload_help_max": "Máx.", | |
| "invalid_files": "archivo(s) no son imágenes válidas y fueron ignorados.", | |
| "duplicate_files": "archivo(s) duplicados fueron omitidos.", | |
| "upload_prompt": "📤 Suba imágenes médicas para comenzar el etiquetado.", | |
| # Gallery | |
| "progress": "Progreso", | |
| "labeled_suffix": "etiquetadas", | |
| "page": "Página", | |
| # Labeler | |
| "labeling": "🏷️ Etiquetado", | |
| "select_label": "— Seleccione una etiqueta —", | |
| "classification": "Clasificación de la imagen", | |
| "unlabeled": "🔴 Sin etiquetar", | |
| "label_set": "🟢 Etiqueta", | |
| "code": "código", | |
| "save_label": "💾 Guardar etiqueta en historial", | |
| "select_before_save": "Seleccione una etiqueta antes de guardar.", | |
| "label_saved": "✅ Etiqueta guardada en la base de datos.", | |
| "save_error": "Error al guardar", | |
| # Recorder | |
| "dictation": "🎙️ Dictado y Transcripción", | |
| "record_audio": "Grabar audio", | |
| "transcribing": "Transcribiendo audio…", | |
| "transcription_editable": "Transcripción (editable)", | |
| "transcription_placeholder": "Grabe un audio o escriba la transcripción manualmente…", | |
| "segments_timestamps": "🕐 Segmentos con timestamps", | |
| "restore_original": "🔄 Restaurar original", | |
| "clear_text": "🗑️ Limpiar texto", | |
| "words": "palabras", | |
| "manually_modified": "✏️ _modificada manualmente_", | |
| "no_transcription_yet": "Sin transcripción aún.", | |
| # Downloader | |
| "download": "📥 Descarga", | |
| "current_image": "Imagen actual", | |
| "label_to_enable": "Etiquete la imagen para habilitar la descarga individual.", | |
| "download_label": "⬇️ Descargar etiquetado", | |
| "full_session": "Toda la sesión", | |
| "images_metric": "Imágenes", | |
| "with_audio": "Con audio", | |
| "labeled_metric": "Etiquetadas", | |
| "with_transcription": "Con transcripción", | |
| "unlabeled_warning": "imagen(es) sin etiquetar. Se incluirán en la descarga pero sin etiqueta.", | |
| "no_images_download": "No hay imágenes para descargar.", | |
| "download_all": "⬇️ Descargar todo el etiquetado (ZIP)", | |
| "ml_formats": "Formatos para ML", | |
| "hf_csv": "📊 CSV (HuggingFace)", | |
| "jsonl_finetune": "📄 JSONL (Fine-tuning)", | |
| # Nav | |
| "previous": "⬅️ Anterior", | |
| "next": "Siguiente ➡️", | |
| "delete_image": "🗑️ Eliminar esta imagen", | |
| # Timeout | |
| "session_expired_data": "⏰ Sesión expirada por inactividad", | |
| "session_expired_clean": "⏰ Sesión expirada por inactividad. Se inició una nueva sesión.", | |
| "download_before_expire": "Descargue sus datos antes de que expire la sesión la próxima vez.", | |
| # Auth | |
| "login_prompt": "👨⚕️ Inicie sesión para acceder al sistema de etiquetado.", | |
| "login_error": "❌ Usuario o contraseña incorrectos.", | |
| # i18n | |
| "ui_language": "🌐 Idioma / Language", | |
| "loading_whisper": "Cargando modelo Whisper '{model}'...", | |
| # Session expiry with placeholders | |
| "session_expired": "⏰ Sesión expirada por inactividad ({minutes} min). Se eliminaron **{total}** imágenes, **{labeled}** etiquetadas, **{with_audio}** con audio. Descargue sus datos antes de que expire la sesión la próxima vez.", | |
| "db_error": "Error crítico de base de datos: {error}", | |
| "history_error": "Error al obtener historial: {error}", | |
| # Labeler | |
| "select_label_hint": "⬇️ Seleccione una etiqueta para esta imagen", | |
| "locs_title": "**Clasificación LOCS III**", | |
| "locs_placeholder": "Seleccionar…", | |
| "locs_progress": "📋 LOCS: {filled}/{total} campos completados", | |
| "locs_complete": "✅ LOCS: {filled}/{total} campos completados", | |
| # Recorder | |
| "re_record": "🎤 Volver a grabar", | |
| "word_count": "{count} palabras", | |
| # Downloader | |
| "single_download": "📥 Descarga individual", | |
| "session_info": "📊 Información de sesión", | |
| "bulk_download": "📦 Descargar todo el etiquetado", | |
| "download_all_zip": "⬇️ Descargar todo el etiquetado (ZIP)", | |
| "download_file": "⬇️ Descargar — {filename}", | |
| "incomplete_fields_msg": "La imagen **{filename}** tiene campos sin completar:", | |
| "missing_categorical": "Etiqueta categórica", | |
| "missing_locs": "LOCS III – {field}", | |
| "missing_voice": "Etiquetado por voz", | |
| "download_anyway": "⬇️ Descargar igualmente", | |
| "go_back_finish": "🔙 Regresar y terminar", | |
| "bulk_incomplete_msg": "**{count} imagen(es)** tienen etiquetado incompleto:", | |
| "col_image": "Imagen", | |
| "col_categorical": "Categórica", | |
| "col_locs": "LOCS III", | |
| "col_voice": "Voz", | |
| "locs_not_required": "No Necesario", | |
| "image_counter": "{current} de {total}", | |
| # Gallery | |
| "gallery_prev": "◀ Ant.", | |
| "gallery_next": "Sig. ▶", | |
| # Uploader | |
| "relabel_dialog_msg": "**{count} imagen(es)** ya fueron etiquetadas anteriormente. Seleccione cuáles desea volver a etiquetar.", | |
| "relabel_new_info": "ℹ️ Las otras **{count}** imagen(es) nuevas se subirán automáticamente.", | |
| "accept_upload": "✅ Aceptar y subir", | |
| "cancel_labeled": "❌ Cancelar etiquetadas", | |
| "duplicates_dialog_msg": "Las siguientes imágenes **ya se encuentran en la sesión actual** y no se volverán a subir:", | |
| "accept": "Aceptar", | |
| # Dialog titles | |
| "dlg_single_incomplete": "⚠️ Etiquetado incompleto", | |
| "dlg_bulk_incomplete": "⚠️ Imágenes con etiquetado incompleto", | |
| "dlg_relabel": "⚠️ Imágenes ya etiquetadas", | |
| "dlg_duplicates": "ℹ️ Imágenes duplicadas en sesión", | |
| # Uploader badge | |
| "times_badge": "{n} vez", | |
| "times_badge_plural": "{n} veces", | |
| }, | |
| "en": { | |
| "app_subtitle": "Ophthalmological Medical Labeling System", | |
| "settings": "⚙️ Settings", | |
| "doctor_name": "👨⚕️ Doctor Name", | |
| "whisper_model": "Whisper Model", | |
| "dictation_language": "Dictation Language", | |
| "current_session": "📊 Current Session", | |
| "db_type": "Database", | |
| "images_loaded": "Images loaded", | |
| "labeled_count": "Labeled", | |
| "no_images": "No images in session.", | |
| "history": "🗄️ History", | |
| "search_image": "🔍 Search by image", | |
| "no_records": "No records.", | |
| "label_header": "Label", | |
| "doctor_header": "Doctor", | |
| "no_transcription": "No transcription", | |
| "end_session": "� Log out", | |
| "undownloaded_warning": "⚠️ Undownloaded data", | |
| "timeout_in": "⏱️ Timeout in", | |
| "confirm_delete": "Are you sure? **The session will be closed and all data permanently deleted.**", | |
| "yes_delete": "✅ Yes, log out", | |
| "cancel": "❌ Cancel", | |
| "logout": "🚪 Log out", | |
| "upload_images": "📤 Upload medical images", | |
| "upload_help_formats": "Accepted formats", | |
| "upload_help_max": "Max.", | |
| "invalid_files": "file(s) are not valid images and were ignored.", | |
| "duplicate_files": "duplicate file(s) were skipped.", | |
| "upload_prompt": "📤 Upload medical images to start labeling.", | |
| "progress": "Progress", | |
| "labeled_suffix": "labeled", | |
| "page": "Page", | |
| "labeling": "🏷️ Labeling", | |
| "select_label": "— Select a label —", | |
| "classification": "Image classification", | |
| "unlabeled": "🔴 Unlabeled", | |
| "label_set": "🟢 Label", | |
| "code": "code", | |
| "save_label": "💾 Save label to history", | |
| "select_before_save": "Select a label before saving.", | |
| "label_saved": "✅ Label saved to database.", | |
| "save_error": "Save error", | |
| "dictation": "🎙️ Dictation & Transcription", | |
| "record_audio": "Record audio", | |
| "transcribing": "Transcribing audio…", | |
| "transcription_editable": "Transcription (editable)", | |
| "transcription_placeholder": "Record audio or type the transcription manually…", | |
| "segments_timestamps": "🕐 Segments with timestamps", | |
| "restore_original": "🔄 Restore original", | |
| "clear_text": "🗑️ Clear text", | |
| "words": "words", | |
| "manually_modified": "✏️ _manually modified_", | |
| "no_transcription_yet": "No transcription yet.", | |
| "download": "📥 Download", | |
| "current_image": "Current image", | |
| "label_to_enable": "Label the image to enable individual download.", | |
| "download_label": "⬇️ Download labeling", | |
| "full_session": "Full session", | |
| "images_metric": "Images", | |
| "with_audio": "With audio", | |
| "labeled_metric": "Labeled", | |
| "with_transcription": "With transcription", | |
| "unlabeled_warning": "unlabeled image(s). They will be included in the download without a label.", | |
| "no_images_download": "No images to download.", | |
| "download_all": "⬇️ Download all labeling (ZIP)", | |
| "ml_formats": "ML Formats", | |
| "hf_csv": "📊 CSV (HuggingFace)", | |
| "jsonl_finetune": "📄 JSONL (Fine-tuning)", | |
| "previous": "⬅️ Previous", | |
| "next": "Next ➡️", | |
| "delete_image": "🗑️ Delete this image", | |
| "session_expired_data": "⏰ Session expired due to inactivity", | |
| "session_expired_clean": "⏰ Session expired. A new session has started.", | |
| "download_before_expire": "Download your data before the session expires next time.", | |
| "login_prompt": "👨⚕️ Log in to access the labeling system.", | |
| "login_error": "❌ Wrong username or password.", | |
| "ui_language": "🌐 Language / Idioma", | |
| "loading_whisper": "Loading Whisper model '{model}'...", | |
| "session_expired": "⏰ Session expired due to inactivity ({minutes} min). Removed **{total}** images, **{labeled}** labeled, **{with_audio}** with audio. Download your data before the session expires next time.", | |
| "db_error": "Critical database error: {error}", | |
| "history_error": "Error fetching history: {error}", | |
| "select_label_hint": "⬇️ Select a label for this image", | |
| "locs_title": "**LOCS III Classification**", | |
| "locs_placeholder": "Select…", | |
| "locs_progress": "📋 LOCS: {filled}/{total} fields completed", | |
| "locs_complete": "✅ LOCS: {filled}/{total} fields completed", | |
| "re_record": "🎤 Re-record", | |
| "word_count": "{count} words", | |
| "single_download": "📥 Individual Download", | |
| "session_info": "📊 Session Information", | |
| "bulk_download": "📦 Download All Labeling", | |
| "download_all_zip": "⬇️ Download all labeling (ZIP)", | |
| "download_file": "⬇️ Download — {filename}", | |
| "incomplete_fields_msg": "Image **{filename}** has incomplete fields:", | |
| "missing_categorical": "Categorical label", | |
| "missing_locs": "LOCS III – {field}", | |
| "missing_voice": "Voice labeling", | |
| "download_anyway": "⬇️ Download anyway", | |
| "go_back_finish": "🔙 Go back and finish", | |
| "bulk_incomplete_msg": "**{count} image(s)** have incomplete labeling:", | |
| "col_image": "Image", | |
| "col_categorical": "Categorical", | |
| "col_locs": "LOCS III", | |
| "col_voice": "Voice", | |
| "locs_not_required": "Not Required", | |
| "image_counter": "{current} of {total}", | |
| "gallery_prev": "◀ Prev", | |
| "gallery_next": "Next ▶", | |
| "relabel_dialog_msg": "**{count} image(s)** were previously labeled. Select which ones to re-label.", | |
| "relabel_new_info": "ℹ️ The other **{count}** new image(s) will be uploaded automatically.", | |
| "accept_upload": "✅ Accept and upload", | |
| "cancel_labeled": "❌ Cancel labeled", | |
| "duplicates_dialog_msg": "The following images **are already in the current session** and will not be re-uploaded:", | |
| "accept": "Accept", | |
| "dlg_single_incomplete": "⚠️ Incomplete labeling", | |
| "dlg_bulk_incomplete": "⚠️ Images with incomplete labeling", | |
| "dlg_relabel": "⚠️ Previously labeled images", | |
| "dlg_duplicates": "ℹ️ Duplicate images in session", | |
| "times_badge": "{n} time", | |
| "times_badge_plural": "{n} times", | |
| }, | |
| } | |
| def t(key: str, **kwargs) -> str: | |
| """Return the translated string for *key*, with optional format kwargs.""" | |
| lang = _get_lang() | |
| text = _STRINGS.get(lang, _STRINGS["es"]).get(key, key) | |
| if kwargs: | |
| try: | |
| text = text.format(**kwargs) | |
| except (KeyError, IndexError): | |
| pass | |
| return text | |
| # ── Label display translations ─────────────────────────────────────────────── | |
| # Labels are stored in English (config.LABEL_OPTIONS["display"]). | |
| # These mappings translate for UI display only. | |
| _LABEL_DISPLAY = { | |
| "es": { | |
| "Normal": "Normal", | |
| "Cataract": "Catarata", | |
| "Bad quality": "Mala calidad", | |
| "Needs dilation": "Necesita dilatación", | |
| }, | |
| "en": { | |
| "Normal": "Normal", | |
| "Cataract": "Cataract", | |
| "Bad quality": "Bad quality", | |
| "Needs dilation": "Needs dilation", | |
| }, | |
| } | |
| def label_display(english_name: str) -> str: | |
| """Translate a label's English display name to the active UI language.""" | |
| lang = _get_lang() | |
| return _LABEL_DISPLAY.get(lang, _LABEL_DISPLAY["en"]).get(english_name, english_name) | |
| def label_from_display(translated_name: str) -> str | None: | |
| """Reverse-map a translated label back to its English storage name.""" | |
| lang = _get_lang() | |
| mapping = _LABEL_DISPLAY.get(lang, _LABEL_DISPLAY["en"]) | |
| reverse = {v: k for k, v in mapping.items()} | |
| return reverse.get(translated_name) | |
| # ── LOCS display translations ──────────────────────────────────────────────── | |
| _LOCS_DISPLAY = { | |
| "es": { | |
| "Nuclear Cataract \u2013 Opalescence (NO)": "Catarata Nuclear \u2013 Opalescencia (NO)", | |
| "Nuclear Cataract \u2013 Color (NC)": "Catarata Nuclear \u2013 Color (NC)", | |
| "Cortical Cataract (C)": "Catarata Cortical (C)", | |
| "None / Clear": "Ninguna / Transparente", | |
| "Very mild": "Muy leve", | |
| "Mild": "Leve", | |
| "Mild\u2013moderate": "Leve\u2013moderada", | |
| "Moderate": "Moderada", | |
| "Moderate\u2013severe": "Moderada\u2013severa", | |
| "Severe": "Severa", | |
| "Very mild yellowing": "Amarillamiento muy leve", | |
| "Mild yellowing": "Amarillamiento leve", | |
| "Moderate yellow": "Amarillo moderado", | |
| "Yellow\u2013brown": "Amarillo\u2013marrón", | |
| "Brown": "Marrón", | |
| "Dark brown": "Marrón oscuro", | |
| "None": "Ninguna", | |
| "Peripheral spokes only": "Solo radios periféricos", | |
| "Mild peripheral involvement": "Compromiso periférico leve", | |
| "Moderate spokes approaching center": "Radios moderados acercándose al centro", | |
| "Central involvement": "Compromiso central", | |
| "Severe / dense central spokes": "Severa / radios centrales densos", | |
| }, | |
| "en": {}, | |
| } | |
| def locs_display(english_text: str) -> str: | |
| """Translate a LOCS field label or option to the active UI language.""" | |
| lang = _get_lang() | |
| if lang == "en": | |
| return english_text | |
| return _LOCS_DISPLAY.get(lang, {}).get(english_text, english_text) | |