/** * ───────────────────────────────────────────────────────────────────────────── * i18n FR/EN (miroir du système Android : values/strings.xml EN défaut + * values-fr/strings.xml). * * - `t(key, ...args)` : traduction + substitution de placeholders {0},{1}… ; * - langue persistée dans le store ("system"/"fr"/"en") — "system" suit * navigator.language (équivalent AppCompatDelegate.setApplicationLocales) ; * - ⚠️ TOUTE nouvelle string existe dans les DEUX dictionnaires (leçon * Android §12 : une référence manquante = affichage brut de la clé). * ───────────────────────────────────────────────────────────────────────────── */ import { store } from '../data/store.js'; /** Dictionnaire ANGLAIS (défaut — même convention que values/strings.xml). */ const EN = { app_name: 'HormoneTrack', // Navigation nav_home: 'Home', nav_chart: 'Charts', nav_doses: 'Doses', nav_labs: 'Labs', nav_treatments: 'Treatments', settings: 'Settings', back: 'Back', save: 'Save', cancel: 'Cancel', delete: 'Delete', ok: 'OK', add_dose: 'Log dose', add_lab: 'Add blood test', add_treatment: 'Add treatment', edit_treatment: 'Edit treatment', edit_dose: 'Edit dose', no_data: 'No data yet', loading: 'Loading…', // Avertissement médical disclaimer: '⚠️ Medical disclaimer: the curves are pharmacokinetic estimates for information purposes — not measurements. Always trust your blood tests and your endocrinologist.', // Accueil current_level: 'Current level (estimated)', no_data_short: 'No data — log an injection to see estimates', delta_6h: '{0} {1} pg/mL vs 6 h ago', next_dose: 'Next dose', next_dose_days: '{0} d {1} h · {2} ({3})', logged_today: 'Quick log', home_chart_title: 'Last 24 h', home_legend_e2: 'E2 — estradiol (pg/mL)', home_legend_t: 'T — testosterone (ng/mL)', open_charts_hint: 'Tap to open full charts', // Alertes alert_card_title: 'Threshold crossed (estimate)', alert_high: '▲ {0} estimated ≈ {1} {2} — above your limit ({3})', alert_low: '▼ {0} estimated ≈ {1} {2} — below your limit ({3})', alert_disclaimer: 'Based on the estimated level, not a measurement.', // Doses hrt_since: 'On HRT since {0}', hrt_duration: '{0} months, {1} days — {2} days total', hrt_duration_days_only: '{0} days', interval_value: '{0} d since previous', confirm_delete: 'Delete this entry?', confirm_delete_treatment: 'Delete this treatment? Its dose history will be removed too (labs are kept).', // Labs edit_which_lab: 'Which entry to edit?', confirm_delete_blood_draw: 'Delete the whole blood draw ({0})?', // Traitements inactive: 'inactive', pk_absorption: 'Tmax', pk_halflife: 'Half-life (h)', pk_bioavail: 'Bioavailability (0–1)', treatment_name: 'Name', treatment_type: 'Type', treatment_route: 'Route', ester: 'Ester', pk_model: 'PK model', default_dose: 'Standard dose', dose_unit: 'Unit', select_preset: 'Select a preset', type_estradiol: 'Estradiol', type_antiandrogen: 'Anti-androgen', type_progestogen: 'Progestogen', type_other: 'Other', route_oral: 'Oral', route_gel: 'Transdermal gel', route_patch: 'Transdermal patch', route_injection_im: 'IM injection', route_injection_sc: 'Subcutaneous injection', route_other: 'Other route', model_ese: 'Estrannaise', model_tfs: 'Transfem Science', model_whs: 'WHSAH', // Éditeur : calibration calibration_title: 'Calibration', scale_factor: 'Scale factor', calibration_hint: 'Ratio lab ÷ model prediction. Use “Calibrate from labs” or enable auto-calibration in Settings.', calibration_auto_override: 'Auto-calibration is ON: this manual factor is ignored on charts display.', calibrate_from_labs: 'Calibrate from labs', // Éditeur : posologie / prévision freq_section: 'Posology (forecast)', freq_interval: 'Interval (days)', freq_hint: 'Projects upcoming doses on the chart and schedules reminders on this grid.', // Éditeur : rappel reminder: 'Reminder', reminder_time: 'Reminder time', // Éditeur : agenda agenda_section: 'Calendar event', agenda_hint: 'Recurring event in a local calendar (Android only). Not available in the web app.', agenda_needs_posology: 'A posology is required for recurring events.', agenda_permission: 'Requires calendar permissions on Android.', // Éditeur : actif active: 'Active', // Dose dialog dose_amount: 'Dose amount', dose_time: 'Date & time', dose_notes: 'Notes', ester_default: 'Default ({0})', no_treatment_hint: 'Create a treatment first (Treatments tab).', // Lab dialog lab_e2_value: 'E2 (pg/mL)', lab_t_value: 'T', lab_units_hint: 'Accepted T units: ng/mL, ng/dL, ng/L, nmol/L', lab_timestamp: 'Date & time', lab_notes: 'Notes', lab_optional: 'optional', // Paramètres language: 'Language', language_system: 'System', auto_calibrate: 'Auto-calibration (E2 + T)', auto_calibrate_hint: 'Adjusts scale factors per ester period and the T model from your labs — applied to charts display, nothing is stored.', t_model_title: 'Testosterone estimate (manual settings)', t_model_hint: 'T = floor + (base − floor) / (1 + k·E2). Empirical model — estimate only.', t_base: 'Base (ng/mL)', t_floor: 'Floor (ng/mL)', t_calibrated: 'k calibrated from labs', alert_section: 'Alert thresholds', alert_hint: 'Evaluated against the ESTIMATED level on Home (opt-in: empty = no alert). High must be above low.', alert_e2_high: 'E2 high (pg/mL)', alert_e2_low: 'E2 low (pg/mL)', alert_t_high: 'T high (ng/mL)', alert_t_low: 'T low (ng/mL)', alert_invalid: 'Invalid values (high must be above low, numbers only)', saved: 'Saved', timezone_section: 'Chart timezone', timezone_hint: 'Timezone used for chart X labels and dose markers (IANA ID, e.g. Europe/Paris). Empty = device timezone.', timezone_field: 'IANA timezone ID', timezone_auto: 'Auto (device)', timezone_invalid: 'Unknown timezone ID', reminders_section: 'Reminders (web)', reminders_note: 'Reminders fire via browser notifications while this page is open. No notification when the tab is closed (unlike the Android app).', notif_grant: 'Enable notifications', notif_granted: 'Notifications enabled', notif_denied: 'Notifications blocked by the browser', backup_section: 'Backup (JSON)', backup_hint: 'Same format as the Android app — backups are interchangeable in both directions.', export_json: 'Export JSON', import_json: 'Import JSON', export_ok: 'Backup exported', export_fail: 'Export failed', import_ok: 'Restored: {0} treatments, {1} doses, {2} labs', import_fail: 'Import failed (invalid file?)', import_title: 'Import backup', import_warning: '⚠️ Importing REPLACES ALL current data (treatments, doses, labs) with the backup content. Continue?', import_confirm: 'Erase & restore', logs_section: 'Diagnostic logs', logs_count: '{0} lines', logs_export: 'Export logs', logs_clear: 'Clear', logs_export_ok: 'Diagnostic logs exported', logs_export_fail: 'Logs export failed', about_title: 'About', settings_version: 'Version {0} (web)', releases_link: 'Releases', models_credit: 'PK models belong to their respective authors (Estrannaise, Transfem Science, WHSAH Collective via Mona).', privacy_note: '100% local: your data never leaves this browser (localStorage). No account, no server.', // Chart chart_24h: '24 h', chart_7j: '7 d', chart_30j: '30 d', zoom_in: 'Zoom in', zoom_out: 'Zoom out', show_labs: 'Labs', show_forecast: 'Forecast', show_extrema: 'Peaks & troughs', back_to_present: 'Back to present', legend_ese_e2: 'E2 — Estrannaise (blue)', legend_ese_t: 'T — Estrannaise (pink)', legend_tfs_e2: 'E2 — Transfem Science (teal)', legend_tfs_t: 'T — Transfem Science (pink dashed)', legend_whs_e2: 'E2 — WHSAH (green)', legend_whs_t: 'T — WHSAH (purple dashed)', legend_labs: '● labs (orange) — circle = E2, square = T', legend_forecast_dose: 'Dashed line + ▼ = projected dose', legend_extrema: '▲▼ = local peaks & troughs (with estimated values)', drag_hint: 'Drag horizontally to pan (right = past, left = future with forecast). Pinch or − / + to zoom (6 h → 300 d).', // Rappels (bannière + notification) reminder_notif_title: 'HormoneTrack — dose reminder', reminder_notif_body: '{0} · {1}{2} — scheduled for {3}', reminder_log_now: 'Log now', reminder_snooze: 'Snooze 1 h', reminder_logged: 'Dose logged from reminder', // Dialog « Nouveautés » whats_new_title: 'What\'s new', whats_new_close: 'Close', // Demo (hook de test ?demo=1) demo_banner: 'DEMO DATA (test hook) — this data lives only in this browser.', }; /** Dictionnaire FRANÇAIS (miroir values-fr/strings.xml). */ const FR = { app_name: 'HormoneTrack', nav_home: 'Accueil', nav_chart: 'Graphiques', nav_doses: 'Doses', nav_labs: 'Analyses', nav_treatments: 'Traitements', settings: 'Paramètres', back: 'Retour', save: 'Enregistrer', cancel: 'Annuler', delete: 'Supprimer', ok: 'OK', add_dose: 'Loguer une dose', add_lab: 'Ajouter une analyse', add_treatment: 'Ajouter un traitement', edit_treatment: 'Modifier le traitement', edit_dose: 'Modifier la dose', no_data: 'Aucune donnée', loading: 'Chargement…', disclaimer: '⚠️ Avertissement médical : les courbes sont des estimations pharmacocinétiques à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de sang et aux consignes de ton endocrinologue.', current_level: 'Niveau actuel (estimé)', no_data_short: 'Aucune donnée — logue une injection pour voir les estimations', delta_6h: '{0} {1} pg/mL vs il y a 6 h', next_dose: 'Prochaine dose', next_dose_days: '{0} j {1} h · {2} ({3})', logged_today: 'Log rapide', home_chart_title: 'Dernières 24 h', home_legend_e2: 'E2 — œstradiol (pg/mL)', home_legend_t: 'T — testostérone (ng/mL)', open_charts_hint: 'Taper pour ouvrir le graphique complet', alert_card_title: 'Seuil franchi (estimation)', alert_high: '▲ {0} estimé ≈ {1} {2} — au-dessus de ta limite ({3})', alert_low: '▼ {0} estimé ≈ {1} {2} — en dessous de ta limite ({3})', alert_disclaimer: 'Basé sur le niveau estimé, pas une mesure.', hrt_since: 'Sous THS depuis le {0}', hrt_duration: '{0} mois, {1} jours — {2} jours au total', hrt_duration_days_only: '{0} jours', interval_value: '{0} j depuis la précédente', confirm_delete: 'Supprimer cette entrée ?', confirm_delete_treatment: 'Supprimer ce traitement ? Son historique de doses sera supprimé aussi (les analyses sont conservées).', edit_which_lab: 'Quelle entrée modifier ?', confirm_delete_blood_draw: 'Supprimer la prise de sang entière ({0}) ?', inactive: 'inactive', pk_absorption: 'Tmax', pk_halflife: 'Demi-vie (h)', pk_bioavail: 'Biodisponibilité (0–1)', treatment_name: 'Nom', treatment_type: 'Type', treatment_route: 'Voie', ester: 'Ester', pk_model: 'Modèle PK', default_dose: 'Dose standard', dose_unit: 'Unité', select_preset: 'Choisir un preset', type_estradiol: 'Œstradiol', type_antiandrogen: 'Anti-androgène', type_progestogen: 'Progestatif', type_other: 'Autre', route_oral: 'Orale', route_gel: 'Gel transdermique', route_patch: 'Patch transdermique', route_injection_im: 'Injection IM', route_injection_sc: 'Injection SC', route_other: 'Autre voie', model_ese: 'Estrannaise', model_tfs: 'Transfem Science', model_whs: 'WHSAH', calibration_title: 'Calibration', scale_factor: 'Facteur d\'échelle', calibration_hint: 'Ratio lab ÷ prédiction du modèle. Utilise « Calibrer avec les analyses » ou active la calibration automatique dans Paramètres.', calibration_auto_override: 'Auto-calibration active : ce facteur manuel est ignoré à l\'affichage des courbes.', calibrate_from_labs: 'Calibrer avec les analyses', freq_section: 'Posologie (prévision)', freq_interval: 'Intervalle (jours)', freq_hint: 'Projette les doses à venir sur le graphique et cale les rappels sur cette grille.', reminder: 'Rappel', reminder_time: 'Heure du rappel', agenda_section: 'Événement d\'agenda', agenda_hint: 'Événement récurrent dans un calendrier local (Android uniquement). Non disponible dans l\'app web.', agenda_needs_posology: 'Une Posologie est requise pour les événements récurrents.', agenda_permission: 'Nécessite les permissions agenda sur Android.', active: 'Active', dose_amount: 'Dose (quantité)', dose_time: 'Date & heure', dose_notes: 'Notes', ester_default: 'Défaut ({0})', no_treatment_hint: 'Crée d\'abord un traitement (onglet Traitements).', lab_e2_value: 'E2 (pg/mL)', lab_t_value: 'T', lab_units_hint: 'Unités T acceptées : ng/mL, ng/dL, ng/L, nmol/L', lab_timestamp: 'Date & heure', lab_notes: 'Notes', lab_optional: 'optionnel', language: 'Langue', language_system: 'Système', auto_calibrate: 'Calibration automatique (E2 + T)', auto_calibrate_hint: 'Ajuste les facteurs d\'échelle par période d\'ester et le modèle T depuis tes labs — appliqué à l\'affichage des courbes, rien n\'est stocké.', t_model_title: 'Estimation T (réglages manuels)', t_model_hint: 'T = plancher + (base − plancher) / (1 + k·E2). Modèle empirique — estimation seulement.', t_base: 'Base (ng/mL)', t_floor: 'Plancher (ng/mL)', t_calibrated: 'k calibré avec les analyses', alert_section: 'Seuils d\'alerte', alert_hint: 'Évalués contre le niveau ESTIMÉ de l\'accueil (opt-in : vide = pas d\'alerte). Haut > bas requis.', alert_e2_high: 'E2 haute (pg/mL)', alert_e2_low: 'E2 basse (pg/mL)', alert_t_high: 'T haute (ng/mL)', alert_t_low: 'T basse (ng/mL)', alert_invalid: 'Valeurs invalides (haut > bas requis, nombres seulement)', saved: 'Enregistré', timezone_section: 'Fuseau du graphique', timezone_hint: 'Fuseau des labels X et marqueurs de doses (ID IANA, ex. Europe/Paris). Vide = fuseau du téléphone.', timezone_field: 'ID de fuseau IANA', timezone_auto: 'Auto (téléphone)', timezone_invalid: 'ID de fuseau inconnu', reminders_section: 'Rappels (web)', reminders_note: 'Les rappels passent par les notifications du navigateur tant que cette page est ouverte. Pas de notification onglet fermé (contrairement à l\'app Android).', notif_grant: 'Activer les notifications', notif_granted: 'Notifications activées', notif_denied: 'Notifications bloquées par le navigateur', backup_section: 'Sauvegarde (JSON)', backup_hint: 'Même format que l\'app Android — les backups sont interchangeables dans les deux sens.', export_json: 'Exporter JSON', import_json: 'Importer JSON', export_ok: 'Sauvegarde exportée', export_fail: 'Échec de l\'export', import_ok: 'Restauré : {0} traitements, {1} doses, {2} analyses', import_fail: 'Échec de l\'import (fichier invalide ?)', import_title: 'Importer une sauvegarde', import_warning: '⚠️ L\'import REMPLACE toutes les données actuelles (traitements, doses, analyses) par le contenu de la sauvegarde. Continuer ?', import_confirm: 'Effacer & restaurer', logs_section: 'Logs de diagnostic', logs_count: '{0} lignes', logs_export: 'Exporter les logs', logs_clear: 'Effacer', logs_export_ok: 'Logs de diagnostic exportés', logs_export_fail: 'Échec de l\'export des logs', about_title: 'À propos', settings_version: 'Version {0} (web)', releases_link: 'Releases', models_credit: 'Les modèles PK appartiennent à leurs autrices respectives (Estrannaise, Transfem Science, WHSAH Collective via Mona).', privacy_note: '100 % local : tes données ne quittent jamais ce navigateur (localStorage). Aucun compte, aucun serveur.', chart_24h: '24 h', chart_7j: '7 j', chart_30j: '30 j', zoom_in: 'Zoom avant', zoom_out: 'Zoom arrière', show_labs: 'Labs', show_forecast: 'Prévision', show_extrema: 'Pics & creux', back_to_present: 'Revenir à maintenant', legend_ese_e2: 'E2 — Estrannaise (bleu)', legend_ese_t: 'T — Estrannaise (rose)', legend_tfs_e2: 'E2 — Transfem Science (turquoise)', legend_tfs_t: 'T — Transfem Science (rose pointillé)', legend_whs_e2: 'E2 — WHSAH (vert)', legend_whs_t: 'T — WHSAH (violet pointillé)', legend_labs: '● labs (orange) — cercle = E2, carré = T', legend_forecast_dose: 'Ligne pointillée + ▼ = dose projetée', legend_extrema: '▲▼ = pics/creux locaux (avec valeurs estimées)', drag_hint: 'Glisser horizontalement pour panoramique (droite = passé, gauche = futur avec la prévision). Pincer ou − / + pour zoomer (6 h → 300 j).', reminder_notif_title: 'HormoneTrack — rappel de dose', reminder_notif_body: '{0} · {1}{2} — prévu à {3}', reminder_log_now: 'Loguer maintenant', reminder_snooze: 'Reporter 1 h', reminder_logged: 'Dose loguée depuis le rappel', whats_new_title: 'Nouveautés', whats_new_close: 'Fermer', demo_banner: 'DONNÉES DE DÉMO (hook de test) — ces données ne vivent que dans ce navigateur.', }; /** Presets (même libellé que les strings Android). */ const EN_PRESETS = { preset_ev_ese: 'EV injection — Estrannaise', preset_eu_ese: 'EU injection — Estrannaise', preset_een_ese: 'EEn injection — Estrannaise', preset_ev_tfs: 'EV injection — Transfem Science', preset_eu_tfs: 'EU injection — Transfem Science', preset_een_tfs: 'EEn injection — Transfem Science', preset_eb_tfs: 'EB injection — Transfem Science', preset_ec_tfs: 'EC injection (oil) — Transfem Science', preset_ecs_tfs: 'EC injection (aqueous suspension) — Transfem Science', preset_pep_tfs: 'PEP injection — Transfem Science', preset_ev_whs: 'EV injection — WHSAH', preset_eu_whs: 'EU injection — WHSAH', preset_een_whs: 'EEn injection — WHSAH', preset_eb_whs: 'EB injection — WHSAH', preset_ec_whs: 'EC injection (oil) — WHSAH', preset_ecs_whs: 'EC injection (aqueous suspension) — WHSAH', preset_e2_gel: 'E2 transdermal gel', preset_e2_patch: 'E2 patch', preset_e2_oral: 'Oral E2', preset_cpa: 'Cyproterone acetate (CPA)', preset_spiro: 'Spironolactone', preset_bica: 'Bicalutamide', }; const FR_PRESETS = { preset_ev_ese: 'Injection EV — Estrannaise', preset_eu_ese: 'Injection EU — Estrannaise', preset_een_ese: 'Injection EEn — Estrannaise', preset_ev_tfs: 'Injection EV — Transfem Science', preset_eu_tfs: 'Injection EU — Transfem Science', preset_een_tfs: 'Injection EEn — Transfem Science', preset_eb_tfs: 'Injection EB — Transfem Science', preset_ec_tfs: 'Injection EC (huile) — Transfem Science', preset_ecs_tfs: 'Injection EC (suspension aqueuse) — Transfem Science', preset_pep_tfs: 'Injection PEP — Transfem Science', preset_ev_whs: 'Injection EV — WHSAH', preset_eu_whs: 'Injection EU — WHSAH', preset_een_whs: 'Injection EEn — WHSAH', preset_eb_whs: 'Injection EB — WHSAH', preset_ec_whs: 'Injection EC (huile) — WHSAH', preset_ecs_whs: 'Injection EC (suspension aqueuse) — WHSAH', preset_e2_gel: 'E2 gel transdermique', preset_e2_patch: 'E2 patch', preset_e2_oral: 'E2 orale', preset_cpa: 'Acétate de cyprotérone (CPA)', preset_spiro: 'Spironolactone', preset_bica: 'Bicalutamide', }; const DICTS = { en: { ...EN, ...EN_PRESETS }, fr: { ...FR, ...FR_PRESETS }, }; /** Langue résolue pour un réglage ("system" suit le navigateur). */ export function resolveLocale(setting) { if (setting === 'fr' || setting === 'en') return setting; const nav = typeof navigator !== 'undefined' && navigator.language ? navigator.language.toLowerCase() : 'en'; return nav.startsWith('fr') ? 'fr' : 'en'; } /** * Traduit une clé dans la langue COURANTE du store, avec substitution de * placeholders {0}, {1}… * * @param {string} key * @param {...(string|number)} args * @returns {string} */ export function t(key, ...args) { const locale = resolveLocale(store.getSettings().language); let s = DICTS[locale][key] ?? DICTS.en[key] ?? key; args.forEach((a, i) => { s = s.replaceAll(`{${i}}`, String(a)); }); return s; } /** Clé i18n → libellé dans la langue courante (utile pour les listes). */ export function trKey(key) { return t(key); }