TheBug95's picture
Solucion de problemas con los botones de volver a grabar y restaurar original. Solucion de incongruencias en los dialogos de descargas e implementacion de internacionalizacion de la herramienta
1f7c87f
"""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)