diagcalc 3.2.4 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@
5
5
  }
6
6
 
7
7
  root.DiagcalcDatasets = factory();
8
- }(typeof globalThis !== "undefined" ? globalThis : this, () => {
8
+ }(/** @type {any} */ (typeof globalThis !== "undefined" ? globalThis : this), () => {
9
9
  const datasets = {
10
10
  screening: {
11
11
  tp: 42,
@@ -45,6 +45,7 @@
45
45
  preTestProb: 28.6,
46
46
  name: "D-dimer for pulmonary embolism",
47
47
  description: "Low Wells score + D-dimer to rule out pulmonary embolism in the emergency department.",
48
+ lastReviewed: "2026-05-13",
48
49
  reference: {
49
50
  authors: "Righini M, et al.",
50
51
  title: "Age-Adjusted D-Dimer Cutoff Levels to Rule Out Pulmonary Embolism",
@@ -61,6 +62,7 @@
61
62
  preTestProb: 15,
62
63
  name: "High-sensitivity troponin for AMI",
63
64
  description: "High-sensitivity troponin for diagnosis of acute myocardial infarction.",
65
+ lastReviewed: "2026-05-13",
64
66
  reference: {
65
67
  authors: "Reichlin T, et al.",
66
68
  title: "Early Diagnosis of Myocardial Infarction with Sensitive Cardiac Troponin Assays",
@@ -77,6 +79,7 @@
77
79
  preTestProb: 1,
78
80
  name: "Screening mammography",
79
81
  description: "Mammographic screening in women aged 50-69 (aggregated data).",
82
+ lastReviewed: "2026-05-13",
80
83
  reference: {
81
84
  authors: "Kopans DB, et al.",
82
85
  title: "Screening Mammography Performance Metrics",
@@ -93,6 +96,7 @@
93
96
  preTestProb: 42.9,
94
97
  name: "Rapid COVID-19 antigen test",
95
98
  description: "Rapid antigen test for SARS-CoV-2 in symptomatic patients (first 7 days).",
99
+ lastReviewed: "2026-05-13",
96
100
  reference: {
97
101
  authors: "Dinnes J, et al.",
98
102
  title: "Rapid, Point-of-Care Antigen Tests for COVID-19",
@@ -109,6 +113,7 @@
109
113
  preTestProb: 2,
110
114
  name: "HIV ELISA",
111
115
  description: "4th-generation ELISA for HIV screening in the general population.",
116
+ lastReviewed: "2026-05-13",
112
117
  reference: {
113
118
  authors: "Masciotra S, et al.",
114
119
  title: "Performance of HIV Diagnostic Tests",
@@ -125,6 +130,7 @@
125
130
  preTestProb: 32,
126
131
  name: "Rapid Streptococcus test",
127
132
  description: "Rapid antigen test for streptococcal pharyngitis in children with fever and sore throat.",
133
+ lastReviewed: "2026-05-13",
128
134
  reference: {
129
135
  authors: "Cohen JF, et al.",
130
136
  title: "Rapid Antigen Detection Test for Group A Streptococcus in Children",
@@ -141,6 +147,7 @@
141
147
  preTestProb: 33.3,
142
148
  name: "Chest X-ray for pneumonia",
143
149
  description: "Chest X-ray for community-acquired pneumonia in symptomatic adults.",
150
+ lastReviewed: "2026-05-13",
144
151
  reference: {
145
152
  authors: "Hagaman JT, et al.",
146
153
  title: "Admission Chest Radiograph Lacks Sensitivity in Pneumonia",
@@ -149,8 +156,139 @@
149
156
  url: "https://www.amjmedsci.org/",
150
157
  },
151
158
  },
159
+ wells_dvt: {
160
+ tp: 60,
161
+ fp: 85,
162
+ fn: 15,
163
+ tn: 340,
164
+ preTestProb: 15,
165
+ name: "Wells score (≥2) for DVT",
166
+ description: "Wells clinical prediction rule (cut-off ≥2 points = 'DVT likely') applied to outpatients with suspected lower-limb deep vein thrombosis.",
167
+ lastReviewed: "2026-05-13",
168
+ reference: {
169
+ authors: "Wells PS, et al.",
170
+ title: "Evaluation of D-Dimer in the Diagnosis of Suspected Deep-Vein Thrombosis",
171
+ journal: "N Engl J Med. 2003;349(13):1227-1235",
172
+ doi: "10.1056/NEJMoa023153",
173
+ url: "https://www.nejm.org/doi/full/10.1056/NEJMoa023153",
174
+ },
175
+ },
176
+ heart_acs: {
177
+ tp: 380,
178
+ fp: 1020,
179
+ fn: 20,
180
+ tn: 1020,
181
+ preTestProb: 17,
182
+ name: "HEART score (≥4) for MACE in chest pain",
183
+ description: "HEART score (History, ECG, Age, Risk factors, Troponin) ≥4 to predict 6-week major adverse cardiac events in undifferentiated ED chest pain.",
184
+ lastReviewed: "2026-05-13",
185
+ reference: {
186
+ authors: "Backus BE, et al.",
187
+ title: "A prospective validation of the HEART score for chest pain patients at the emergency department",
188
+ journal: "Int J Cardiol. 2013;168(3):2153-2158",
189
+ doi: "10.1016/j.ijcard.2013.01.255",
190
+ url: "https://www.internationaljournalofcardiology.com/article/S0167-5273(13)00253-2/fulltext",
191
+ },
192
+ },
193
+ curb65: {
194
+ tp: 72,
195
+ fp: 214,
196
+ fn: 24,
197
+ tn: 758,
198
+ preTestProb: 9,
199
+ name: "CURB-65 (≥2) for severe community-acquired pneumonia",
200
+ description: "CURB-65 score ≥2 as a marker of severe community-acquired pneumonia / 30-day mortality, derived/validated across UK, NZ and Netherlands cohorts.",
201
+ lastReviewed: "2026-05-13",
202
+ reference: {
203
+ authors: "Lim WS, et al.",
204
+ title: "Defining community acquired pneumonia severity on presentation to hospital: an international derivation and validation study",
205
+ journal: "Thorax. 2003;58(5):377-382",
206
+ doi: "10.1136/thorax.58.5.377",
207
+ url: "https://thorax.bmj.com/content/58/5/377",
208
+ },
209
+ },
210
+ qsofa: {
211
+ tp: 55,
212
+ fp: 180,
213
+ fn: 45,
214
+ tn: 720,
215
+ preTestProb: 10,
216
+ name: "qSOFA (≥2) for sepsis screening",
217
+ description: "Quick SOFA score ≥2 (SBP ≤100, RR ≥22, altered mentation) as a bedside screen for in-hospital mortality among patients with suspected infection outside the ICU.",
218
+ lastReviewed: "2026-05-13",
219
+ reference: {
220
+ authors: "Seymour CW, et al.",
221
+ title: "Assessment of Clinical Criteria for Sepsis (Sepsis-3)",
222
+ journal: "JAMA. 2016;315(8):762-774",
223
+ doi: "10.1001/jama.2016.0288",
224
+ url: "https://jamanetwork.com/journals/jama/fullarticle/2492875",
225
+ },
226
+ },
227
+ apgar_5min: {
228
+ tp: 20,
229
+ fp: 200,
230
+ fn: 60,
231
+ tn: 19720,
232
+ preTestProb: 0.4,
233
+ name: "APGAR < 7 at 5 min for severe neonatal outcome",
234
+ description: "Five-minute APGAR score below 7 as a predictor of neonatal death in term infants. Illustrates a highly specific but low-sensitivity test at very low disease prevalence.",
235
+ lastReviewed: "2026-05-13",
236
+ reference: {
237
+ authors: "Casey BM, McIntire DD, Leveno KJ.",
238
+ title: "The continuing value of the Apgar score for the assessment of newborn infants",
239
+ journal: "N Engl J Med. 2001;344(7):467-471",
240
+ doi: "10.1056/NEJM200102153440701",
241
+ url: "https://www.nejm.org/doi/full/10.1056/NEJM200102153440701",
242
+ },
243
+ },
244
+ fast_trauma: {
245
+ tp: 110,
246
+ fp: 28,
247
+ fn: 19,
248
+ tn: 1383,
249
+ preTestProb: 8,
250
+ name: "FAST ultrasound for blunt abdominal trauma",
251
+ description: "Focused Assessment with Sonography for Trauma (surgeon-performed) for intra-abdominal injury in blunt trauma patients.",
252
+ lastReviewed: "2026-05-13",
253
+ reference: {
254
+ authors: "Rozycki GS, et al.",
255
+ title: "A prospective study of surgeon-performed ultrasound as the primary adjuvant modality for injured patient assessment",
256
+ journal: "Ann Surg. 1998;228(4):557-567",
257
+ doi: "10.1097/00000658-199810000-00012",
258
+ url: "https://journals.lww.com/annalsofsurgery/Fulltext/1998/10000/A_Prospective_Study_of_Surgeon_Performed.12.aspx",
259
+ },
260
+ },
152
261
  };
153
262
 
263
+ // Existing counts have no retained source-table extraction. Citations are
264
+ // background reading, not evidence that these exact counts were published.
265
+ for (const dataset of Object.values(datasets)) {
266
+ dataset.provenance = Object.freeze({
267
+ kind: "illustrative",
268
+ sourceLocation: null,
269
+ extraction: null,
270
+ population: dataset.description,
271
+ threshold: null,
272
+ referenceStandard: null,
273
+ outcome: null,
274
+ reviewStatus: "Extraction not verified",
275
+ note: dataset.reference
276
+ ? "Illustrative teaching counts. The citation is background reading; these counts have not been verified against a published table."
277
+ : "Generic illustrative teaching scenario; not an empirical study.",
278
+ });
279
+ if (dataset.lastReviewed) {
280
+ dataset.referenceLastReviewed = dataset.lastReviewed;
281
+ delete dataset.lastReviewed;
282
+ }
283
+ Object.freeze(dataset.reference);
284
+ Object.freeze(dataset);
285
+ }
286
+ Object.freeze(datasets);
287
+
288
+ function buildDatasetWarnings(dataset) {
289
+ return dataset && dataset.provenance ? [dataset.provenance.note] : [];
290
+ }
291
+
154
292
  function getDataset(key) {
155
293
  if (!Object.prototype.hasOwnProperty.call(datasets, key)) {
156
294
  return null;
@@ -168,6 +306,7 @@
168
306
 
169
307
  return {
170
308
  datasets,
309
+ buildDatasetWarnings,
171
310
  getDataset,
172
311
  listDatasets,
173
312
  };
@@ -0,0 +1,27 @@
1
+ (function (root, factory) {
2
+ if (typeof module === "object" && module.exports) {
3
+ module.exports = factory();
4
+ return;
5
+ }
6
+ root.DiagcalcGeometry = factory();
7
+ }(/** @type {any} */ (typeof globalThis !== "undefined" ? globalThis : this), () => {
8
+ const limit = Math.log(999);
9
+ function logit(probability) {
10
+ return Math.log(probability / (1 - probability));
11
+ }
12
+ function prePosition(probability) {
13
+ return 0.5 + logit(probability) / (2 * limit);
14
+ }
15
+ function postPosition(probability) {
16
+ return 0.5 - logit(probability) / (2 * limit);
17
+ }
18
+ function lrPosition(lr) {
19
+ return 0.5 - Math.log(lr) / (4 * limit);
20
+ }
21
+ /** Normalised calibrated Fagan coordinates; off-scale values are not clipped. */
22
+ function faganCoordinates(pre, lr, post) {
23
+ const positions = { pre: prePosition(pre), lr: lrPosition(lr), post: postPosition(post) };
24
+ return { ...positions, inRange: Object.values(positions).every((value) => Number.isFinite(value) && value >= 0 && value <= 1) };
25
+ }
26
+ return { prePosition, postPosition, lrPosition, faganCoordinates };
27
+ }));
@@ -0,0 +1,255 @@
1
+ (function (root, factory) {
2
+ if (typeof module === "object" && module.exports) {
3
+ module.exports = factory();
4
+ return;
5
+ }
6
+ root.DiagcalcI18n = factory();
7
+ }(/** @type {any} */ (typeof globalThis !== "undefined" ? globalThis : this), () => {
8
+ // Translation tables. Only UI chrome (headings, labels, buttons, kickers,
9
+ // panel summaries, key help text) is translated — engine output (sens/spec
10
+ // notes, LR/DOR interpretations, validation messages) stays in English so
11
+ // that clinical phrasing stays uniform and so unit tests remain valid.
12
+ const translations = {
13
+ en: {
14
+ "page.title": "DIAG-CALC",
15
+ "intro.tag": "Educational tool",
16
+ "intro.lead": "Interactive calculator for exploring diagnostic test performance measures and applying Bayesian reasoning in clinical practice.",
17
+ "intro.highlight1.h": "Key measures",
18
+ "intro.highlight1.p": "Sensitivity, specificity, PPV, NPV, and likelihood ratios with 95% CI.",
19
+ "intro.highlight2.h": "Post-test probability",
20
+ "intro.highlight2.p": "Convert pre-test probability into positive and negative post-test probability.",
21
+ "intro.highlight3.h": "Guided interpretation",
22
+ "intro.highlight3.p": "Results explained to support self-directed learning.",
23
+ "library.kicker": "Library",
24
+ "library.h": "Load a study scenario",
25
+ "library.help": "Pick a preset to load values, or fill in the matrix manually below.",
26
+ "editor.kicker": "Editor",
27
+ "editor.h": "Build the case",
28
+ "editor.help": "Pick a preset above or enter the 2×2 below. Non-negative integers; pre-test probability under 100%.",
29
+ "theme.toggle.label": "Toggle dark mode",
30
+ "lang.label": "Language",
31
+ "app.case.empty": "No case yet",
32
+ "app.case.prefix": "Case:",
33
+ "app.case.adhoc": "Ad-hoc case",
34
+ "about.h": "About DIAGCALC",
35
+ "form.kicker": "Setup",
36
+ "form.h": "Define the case",
37
+ "form.scenario": "Scenario",
38
+ "form.scenario.empty": "— Select —",
39
+ "form.scenario.group.generic": "Generic scenarios",
40
+ "form.scenario.group.literature": "Illustrative medical scenarios",
41
+ "form.scenario.help": "Pick a preset to load its values, or fill in the matrix manually below.",
42
+ "form.matrix.legend": "Confusion matrix",
43
+ "form.matrix.dpos": "Disease +",
44
+ "form.matrix.dneg": "Disease −",
45
+ "form.matrix.tpos": "Test +",
46
+ "form.matrix.tneg": "Test −",
47
+ "form.pre.label": "Pre-test probability (%)",
48
+ "form.pre.help": "Estimate the disease prevalence in the study population or the clinical probability before the test.",
49
+ "form.advanced.summary": "Advanced settings",
50
+ "form.advanced.continuity": "Continuity correction for LR / DOR CIs",
51
+ "form.advanced.continuity.auto": "Auto — apply +0.5 only when a 2×2 cell is zero",
52
+ "form.advanced.continuity.always": "Always — apply +0.5 to every cell",
53
+ "form.advanced.continuity.never": "Never — leave undefined intervals unavailable",
54
+ "form.advanced.continuity.help": "When any cell of the confusion matrix is zero the log-normal CI for LR+ / LR− / DOR is undefined. The default adds 0.5 to every cell (Cox 1970) when this happens so the maths still produces a (wide) interval.",
55
+ "form.calculate": "Calculate results",
56
+ "form.clear": "Clear fields",
57
+ "results.kicker": "Output",
58
+ "results.h": "Results",
59
+ "results.help": "Values are shown with 95% confidence intervals where applicable.",
60
+ "results.print": "Print report",
61
+ "panel.tag.tool": "Tool",
62
+ "panel.tag.ref": "Reference",
63
+ "fagan.h": "Fagan Nomogram",
64
+ "fagan.help": "Graphical representation of the relationship between pre-test probability, likelihood ratio, and post-test probability.",
65
+ "fagan.pre": "Pre-test",
66
+ "fagan.lrpos": "LR+",
67
+ "fagan.lrneg": "LR−",
68
+ "fagan.postpos": "Post-test (+)",
69
+ "fagan.postneg": "Post-test (−)",
70
+ "feedback.calc.ok": "Calculation complete. Explore the interpretation guide below.",
71
+ "feedback.cleared": "Fields cleared.",
72
+ "feedback.scenario.loaded": "Scenario loaded. Adjust the values as needed and recalculate.",
73
+ "feedback.history.reloaded": "Reloaded \"{label}\" from history.",
74
+ "thresholdDemo.summary": "Threshold explorer",
75
+ "thresholdDemo.summary.sub": "drag the slicer to see how TP, FP, FN, TN and all metrics shift in real time",
76
+ "thresholdDemo.help": "An illustrative binormal model produces scores for 200 individuals, with group sizes adapted to the current case prevalence. Move the threshold slider to see how the confusion matrix and all performance metrics change in real time. There is no perfect threshold — only trade-offs.",
77
+ "thresholdDemo.threshold": "Threshold",
78
+ "thresholdDemo.sens": "Sensitivity",
79
+ "thresholdDemo.spec": "Specificity",
80
+ "thresholdDemo.ppv": "PPV",
81
+ "thresholdDemo.npv": "NPV",
82
+ "thresholdDemo.disease": "Disease +",
83
+ "thresholdDemo.healthy": "Disease −",
84
+ "thresholdDemo.testpos": "Test +",
85
+ "thresholdDemo.testneg": "Test −",
86
+ "thresholdDemo.tp": "True positives",
87
+ "thresholdDemo.fp": "False positives",
88
+ "thresholdDemo.fn": "False negatives",
89
+ "thresholdDemo.tn": "True negatives",
90
+ "thresholdDemo.note.low": "Threshold very low: catches nearly all diseased but flags many healthy. Useful for screening.",
91
+ "thresholdDemo.note.high": "Threshold very high: confirms nearly only diseased but misses many. Useful when treatment is risky.",
92
+ "thresholdDemo.note.balanced": "Reasonable balance. The final choice depends on the clinical cost of missing a case vs. flagging a healthy one.",
93
+ "thresholdDemo.note.sens": "Tilted toward sensitivity: few diseased escape ({sens}), but more healthy are flagged (specificity {spec}).",
94
+ "thresholdDemo.note.spec": "Tilted toward specificity: few healthy flagged ({spec}), but more diseased escape (sensitivity {sens}).",
95
+ },
96
+ "pt-PT": {
97
+ "page.title": "DIAG-CALC",
98
+ "intro.tag": "Ferramenta educacional",
99
+ "intro.lead": "Calculadora interactiva para explorar medidas de desempenho de testes de diagnóstico e aplicar raciocínio Bayesiano na prática clínica.",
100
+ "intro.highlight1.h": "Medidas principais",
101
+ "intro.highlight1.p": "Sensibilidade, especificidade, VPP, VPN e razões de verosimilhança com IC 95%.",
102
+ "intro.highlight2.h": "Probabilidade pós-teste",
103
+ "intro.highlight2.p": "Conversão da probabilidade pré-teste em probabilidades pós-teste positiva e negativa.",
104
+ "intro.highlight3.h": "Interpretação guiada",
105
+ "intro.highlight3.p": "Resultados explicados para suportar aprendizagem auto-dirigida.",
106
+ "library.kicker": "Biblioteca",
107
+ "library.h": "Carregar um cenário de estudo",
108
+ "library.help": "Escolha um preset para carregar valores, ou preencha a matriz manualmente em baixo.",
109
+ "editor.kicker": "Editor",
110
+ "editor.h": "Construir o caso",
111
+ "editor.help": "Escolha um preset acima ou preencha a matriz 2×2 abaixo. Inteiros não-negativos; probabilidade pré-teste entre 0% e 100%.",
112
+ "theme.toggle.label": "Alternar modo escuro",
113
+ "lang.label": "Idioma",
114
+ "app.case.empty": "Sem caso ainda",
115
+ "app.case.prefix": "Caso:",
116
+ "app.case.adhoc": "Caso ad-hoc",
117
+ "about.h": "Sobre o DIAGCALC",
118
+ "form.kicker": "Configuração",
119
+ "form.h": "Definir o caso",
120
+ "form.scenario": "Cenário",
121
+ "form.scenario.empty": "— Seleccionar —",
122
+ "form.scenario.group.generic": "Cenários genéricos",
123
+ "form.scenario.group.literature": "Cenários médicos ilustrativos",
124
+ "form.scenario.help": "Escolha um preset para carregar valores, ou preencha a matriz manualmente em baixo.",
125
+ "form.matrix.legend": "Matriz de confusão",
126
+ "form.matrix.dpos": "Doença +",
127
+ "form.matrix.dneg": "Doença −",
128
+ "form.matrix.tpos": "Teste +",
129
+ "form.matrix.tneg": "Teste −",
130
+ "form.pre.label": "Probabilidade pré-teste (%)",
131
+ "form.pre.help": "Estime a prevalência da doença na população do estudo ou a probabilidade clínica antes do teste.",
132
+ "form.advanced.summary": "Definições avançadas",
133
+ "form.advanced.continuity": "Correcção de continuidade para os IC de LR / DOR",
134
+ "form.advanced.continuity.auto": "Automática — aplicar +0,5 apenas quando alguma célula 2×2 é zero",
135
+ "form.advanced.continuity.always": "Sempre — aplicar +0,5 a todas as células",
136
+ "form.advanced.continuity.never": "Nunca — deixar os intervalos indefinidos indisponíveis",
137
+ "form.advanced.continuity.help": "Quando alguma célula da matriz é zero o IC log-normal para LR+ / LR− / DOR fica indefinido. O default adiciona 0,5 a todas as células (Cox 1970) nesses casos, mantendo um intervalo (largo, mas finito).",
138
+ "form.calculate": "Calcular resultados",
139
+ "form.clear": "Limpar campos",
140
+ "results.kicker": "Resultado",
141
+ "results.h": "Resultados",
142
+ "results.help": "Os valores são apresentados com IC 95% sempre que aplicável.",
143
+ "results.print": "Imprimir relatório",
144
+ "panel.tag.tool": "Ferramenta",
145
+ "panel.tag.ref": "Referência",
146
+ "fagan.h": "Nomograma de Fagan",
147
+ "fagan.help": "Representação gráfica da relação entre probabilidade pré-teste, razão de verosimilhança e probabilidade pós-teste.",
148
+ "fagan.pre": "Pré-teste",
149
+ "fagan.lrpos": "LR+",
150
+ "fagan.lrneg": "LR−",
151
+ "fagan.postpos": "Pós-teste (+)",
152
+ "fagan.postneg": "Pós-teste (−)",
153
+ "feedback.calc.ok": "Cálculo concluído. Explore o guia de interpretação em baixo.",
154
+ "feedback.cleared": "Campos limpos.",
155
+ "feedback.scenario.loaded": "Cenário carregado. Ajuste os valores e recalcule se necessário.",
156
+ "feedback.history.reloaded": "Recarregado \"{label}\" do histórico.",
157
+ "thresholdDemo.summary": "Explorador de limiar",
158
+ "thresholdDemo.summary.sub": "arraste o cursor para ver como VP, FP, FN, VN e todas as métricas mudam em tempo real",
159
+ "thresholdDemo.help": "Um modelo binormal ilustrativo produz scores para 200 indivíduos, com grupos adaptados à prevalência do caso actual. Mova o cursor do limiar para ver como a matriz de confusão e todas as métricas de desempenho mudam em tempo real. Não há limiar perfeito — apenas compromissos.",
160
+ "thresholdDemo.threshold": "Limiar",
161
+ "thresholdDemo.sens": "Sensibilidade",
162
+ "thresholdDemo.spec": "Especificidade",
163
+ "thresholdDemo.ppv": "VPP",
164
+ "thresholdDemo.npv": "VPN",
165
+ "thresholdDemo.disease": "Doença +",
166
+ "thresholdDemo.healthy": "Doença −",
167
+ "thresholdDemo.testpos": "Teste +",
168
+ "thresholdDemo.testneg": "Teste −",
169
+ "thresholdDemo.tp": "Verdadeiros positivos",
170
+ "thresholdDemo.fp": "Falsos positivos",
171
+ "thresholdDemo.fn": "Falsos negativos",
172
+ "thresholdDemo.tn": "Verdadeiros negativos",
173
+ "thresholdDemo.note.low": "Limiar muito baixo: apanha quase todos os doentes mas sinaliza muitos saudáveis. Útil em rastreio.",
174
+ "thresholdDemo.note.high": "Limiar muito alto: confirma quase só doentes mas perde muitos. Útil quando o tratamento é arriscado.",
175
+ "thresholdDemo.note.balanced": "Equilíbrio razoável. A escolha final depende do custo clínico de falhar um doente vs. sinalizar um saudável.",
176
+ "thresholdDemo.note.sens": "Inclinado para a sensibilidade: poucos doentes escapam ({sens}), mas mais saudáveis ficam sinalizados (especificidade {spec}).",
177
+ "thresholdDemo.note.spec": "Inclinado para a especificidade: poucos saudáveis sinalizados ({spec}), mas mais doentes escapam (sensibilidade {sens}).",
178
+ },
179
+ };
180
+
181
+ const FALLBACK = "en";
182
+ const STORAGE_KEY = "diagcalc-lang";
183
+ let activeLocale = FALLBACK;
184
+
185
+ function getLocale() {
186
+ return activeLocale;
187
+ }
188
+
189
+ function setLocale(locale) {
190
+ activeLocale = translations[locale] ? locale : FALLBACK;
191
+ try {
192
+ if (typeof localStorage !== "undefined") localStorage.setItem(STORAGE_KEY, activeLocale);
193
+ } catch (_) {}
194
+ }
195
+
196
+ function detectLocale() {
197
+ try {
198
+ if (typeof localStorage !== "undefined") {
199
+ const stored = localStorage.getItem(STORAGE_KEY);
200
+ if (stored && translations[stored]) return stored;
201
+ }
202
+ } catch (_) {}
203
+ if (typeof navigator !== "undefined" && navigator.language) {
204
+ const tag = navigator.language;
205
+ if (translations[tag]) return tag;
206
+ // Match a base language (e.g., "pt" → "pt-PT")
207
+ const base = tag.split("-")[0];
208
+ const match = Object.keys(translations).find((k) => k === base || k.startsWith(base + "-"));
209
+ if (match) return match;
210
+ }
211
+ return FALLBACK;
212
+ }
213
+
214
+ function t(key, vars) {
215
+ const table = translations[activeLocale] || translations[FALLBACK];
216
+ let value = table[key];
217
+ if (value === undefined) value = translations[FALLBACK][key];
218
+ if (value === undefined) return key;
219
+ if (vars && typeof value === "string") {
220
+ return value.replace(/\{(\w+)\}/g, (_, k) => (vars[k] !== undefined ? String(vars[k]) : `{${k}}`));
221
+ }
222
+ return value;
223
+ }
224
+
225
+ function applyToDom(root) {
226
+ if (typeof document === "undefined") return;
227
+ const scope = root || document;
228
+ scope.querySelectorAll("[data-i18n]").forEach((el) => {
229
+ const key = el.getAttribute("data-i18n");
230
+ if (key) el.textContent = t(key);
231
+ });
232
+ scope.querySelectorAll("[data-i18n-attr]").forEach((el) => {
233
+ const spec = el.getAttribute("data-i18n-attr");
234
+ // Format: "aria-label=some.key;title=other.key"
235
+ spec.split(";").forEach((pair) => {
236
+ const [attr, key] = pair.split("=").map((s) => s.trim());
237
+ if (attr && key) el.setAttribute(attr, t(key));
238
+ });
239
+ });
240
+ }
241
+
242
+ function availableLocales() {
243
+ return Object.keys(translations);
244
+ }
245
+
246
+ return {
247
+ translations,
248
+ getLocale,
249
+ setLocale,
250
+ detectLocale,
251
+ t,
252
+ applyToDom,
253
+ availableLocales,
254
+ };
255
+ }));
@@ -0,0 +1,9 @@
1
+ (function (root, factory) {
2
+ if (typeof module === "object" && module.exports) {
3
+ module.exports = factory();
4
+ return;
5
+ }
6
+ root.DiagcalcMeta = factory();
7
+ }(/** @type {any} */ (typeof globalThis !== "undefined" ? globalThis : this), () => {
8
+ return Object.freeze({ version: "5.0.0", schemaVersion: 2 });
9
+ }));