File size: 17,495 Bytes
b0c3a57
 
1f7c87f
 
b0c3a57
 
1f7c87f
 
 
 
 
 
 
 
 
 
b0c3a57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f7c87f
b0c3a57
 
1f7c87f
 
b0c3a57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f7c87f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b0c3a57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f7c87f
b0c3a57
 
1f7c87f
 
b0c3a57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f7c87f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b0c3a57
1f7c87f
b0c3a57
 
 
1f7c87f
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
"""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)