csaf-crud 1.4.15

CSAF 2.0 / 2.1 advisory CRUD server with HATEOAS JSON API and HTML UI (TLS 1.3, HTTP/1.1 + HTTP/2 + HTTP/3)
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
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Pierre Gronau, ndaal in Cologne

//! Settings (`routes::settings`) page translations.
//!
//! Covers `routes::settings::render_settings_content` (section headings,
//! field labels, help text, the sidecar checkboxes, the classification
//! dropdown option text, and the two form-action buttons) plus
//! `routes::settings::validate_settings_form`'s user-facing error strings.
//!
//! Deliberately left untranslated, in every language:
//!   - The literal `<option value="...">` values compared against the
//!     `csaf-models::settings` whitelists — TLP labels (`CLEAR`, `GREEN`,
//!     `AMBER`, `AMBER+STRICT`, `RED`), Verschlusssache labels, NATO labels,
//!     storage modes (`distribution_text`/`notes`/`both`), CSAF mode
//!     (`2.0`/`2.1`), and theme (`light`/`dark`). Translating these would
//!     make a validation error tell the user to enter a value the code
//!     does not accept.
//!   - The publisher-category option values/text (`vendor`, `discoverer`,
//!     `coordinator`, `user`, `translator`, `other`): these are OASIS CSAF
//!     `document.publisher.category` enum values written verbatim into
//!     exported JSON, so — like the lists above — the visible option text
//!     is kept identical to the value for the same reason.
//!   - The domain terms `TLP`, `NATO`, `Verschlusssache`, `CSAF`, and
//!     `OASIS`: kept as the same acronym/proper noun in every language so
//!     prose referring to them stays consistent with the fixed labels
//!     above (e.g. a French label saying "classification OTAN" next to a
//!     dropdown option literally spelled `NATO RESTRICTED` would be
//!     confusing, not helpful).

use super::Entry;

/// Look up a `settings.*` translation key.
pub(super) fn tr(key: &str) -> Option<Entry> {
    tr_general(key)
        .or_else(|| tr_paths_and_naming(key))
        .or_else(|| tr_sidecar(key))
        .or_else(|| tr_classification(key))
        .or_else(|| tr_storage_mode_options(key))
        .or_else(|| tr_verschlusssache_nato(key))
        .or_else(|| tr_publisher(key))
        .or_else(|| tr_actions(key))
        .or_else(|| tr_validation_fields(key))
        .or_else(|| tr_validation_publisher_classification(key))
}

/// General section: heading, CSAF mode, theme, import/export directory.
fn tr_general(key: &str) -> Option<Entry> {
    Some(match key {
        "settings.section_general" => ("General", "Allgemein", "Général", "General", "Generale"),
        "settings.csaf_mode_label" => (
            "CSAF Mode *",
            "CSAF-Modus *",
            "Mode CSAF *",
            "Modo CSAF *",
            "Modalità CSAF *",
        ),
        "settings.csaf_mode_help" => (
            "Determines validation rules and the <code>csaf_version</code> written into exported files.",
            "Bestimmt die Validierungsregeln und die in exportierte Dateien geschriebene <code>csaf_version</code>.",
            "Détermine les règles de validation et la <code>csaf_version</code> écrite dans les fichiers exportés.",
            "Determina las reglas de validación y la <code>csaf_version</code> escrita en los archivos exportados.",
            "Determina le regole di convalida e il valore <code>csaf_version</code> scritto nei file esportati.",
        ),
        "settings.theme_label" => ("Theme", "Design", "Thème", "Tema", "Tema"),
        "settings.theme_help" => (
            "You can also use the toggle in the top navigation bar.",
            "Sie können auch den Schalter in der oberen Navigationsleiste verwenden.",
            "Vous pouvez également utiliser le bouton dans la barre de navigation supérieure.",
            "También puede usar el interruptor en la barra de navegación superior.",
            "È possibile utilizzare anche l'interruttore nella barra di navigazione superiore.",
        ),
        "settings.import_directory_label" => (
            "Import Directory",
            "Import-Verzeichnis",
            "Répertoire d'importation",
            "Directorio de importación",
            "Directory di importazione",
        ),
        "settings.import_directory_help" => (
            "Filesystem path scanned for CSAF JSON files during import.",
            "Dateisystempfad, der beim Import nach CSAF-JSON-Dateien durchsucht wird.",
            "Chemin du système de fichiers analysé pour les fichiers JSON CSAF lors de l'importation.",
            "Ruta del sistema de archivos examinada en busca de archivos JSON CSAF durante la importación.",
            "Percorso del file system analizzato per i file JSON CSAF durante l'importazione.",
        ),
        "settings.export_directory_label" => (
            "Export Directory",
            "Export-Verzeichnis",
            "Répertoire d'exportation",
            "Directorio de exportación",
            "Directory di esportazione",
        ),
        "settings.export_directory_help" => (
            "Filesystem path where exported CSAF files and sidecars are written.",
            "Dateisystempfad, in den exportierte CSAF-Dateien und Sidecar-Dateien geschrieben werden.",
            "Chemin du système de fichiers où sont écrits les fichiers CSAF exportés et leurs fichiers sidecar.",
            "Ruta del sistema de archivos donde se escriben los archivos CSAF exportados y sus archivos sidecar.",
            "Percorso del file system in cui vengono scritti i file CSAF esportati e i relativi file sidecar.",
        ),
        _ => return None,
    })
}

/// Dump/log directory and naming-convention prefix.
fn tr_paths_and_naming(key: &str) -> Option<Entry> {
    Some(match key {
        "settings.dump_directory_label" => (
            "Dump Directory",
            "Dump-Verzeichnis",
            "Répertoire de vidage",
            "Directorio de volcado",
            "Directory di dump",
        ),
        "settings.dump_directory_help" => (
            "Filesystem path where the database dump files and sidecars are written.",
            "Dateisystempfad, in den die Datenbank-Dump-Dateien und Sidecar-Dateien geschrieben werden.",
            "Chemin du système de fichiers où sont écrits les fichiers de vidage de la base de données et leurs fichiers sidecar.",
            "Ruta del sistema de archivos donde se escriben los archivos de volcado de la base de datos y sus archivos sidecar.",
            "Percorso del file system in cui vengono scritti i file di dump del database e i relativi file sidecar.",
        ),
        "settings.log_directory_label" => (
            "Log Directory",
            "Protokoll-Verzeichnis",
            "Répertoire des journaux",
            "Directorio de registros",
            "Directory dei log",
        ),
        "settings.log_directory_help" => (
            "Filesystem path where the application log files and sidecars are written.",
            "Dateisystempfad, in den die Anwendungsprotokolldateien und Sidecar-Dateien geschrieben werden.",
            "Chemin du système de fichiers où sont écrits les fichiers journaux de l'application et leurs fichiers sidecar.",
            "Ruta del sistema de archivos donde se escriben los archivos de registro de la aplicación y sus archivos sidecar.",
            "Percorso del file system in cui vengono scritti i file di log dell'applicazione e i relativi file sidecar.",
        ),
        "settings.naming_convention_label" => (
            "Naming Convention Prefix",
            "Präfix der Namenskonvention",
            "Préfixe de la convention de nommage",
            "Prefijo de la convención de nomenclatura",
            "Prefisso della convenzione di denominazione",
        ),
        "settings.naming_convention_help" => (
            "Prefix used for tracking IDs (e.g. <code>ndaal-sa-</code>).",
            "Präfix für Tracking-IDs (z. B. <code>ndaal-sa-</code>).",
            "Préfixe utilisé pour les identifiants de suivi (par ex. <code>ndaal-sa-</code>).",
            "Prefijo utilizado para los identificadores de seguimiento (p. ej., <code>ndaal-sa-</code>).",
            "Prefisso utilizzato per gli ID di tracciamento (ad es. <code>ndaal-sa-</code>).",
        ),
        _ => return None,
    })
}

/// Sidecar hash-file checkboxes.
fn tr_sidecar(key: &str) -> Option<Entry> {
    Some(match key {
        "settings.sidecar_hash_files_label" => (
            "Sidecar Hash Files",
            "Sidecar-Hash-Dateien",
            "Fichiers de hachage sidecar",
            "Archivos hash sidecar",
            "File hash sidecar",
        ),
        "settings.sidecar_sha256_label" => (
            "Generate SHA-256 hashes sidecars (extension <code>.sha-256</code>)",
            "SHA-256-Hash-Sidecars erzeugen (Erweiterung <code>.sha-256</code>)",
            "Générer les fichiers sidecar de hachage SHA-256 (extension <code>.sha-256</code>)",
            "Generar archivos sidecar de hash SHA-256 (extensión <code>.sha-256</code>)",
            "Genera i sidecar hash SHA-256 (estensione <code>.sha-256</code>)",
        ),
        "settings.sidecar_sha512_label" => (
            "Generate SHA-512 hashes sidecars (extension <code>.sha-512</code>)",
            "SHA-512-Hash-Sidecars erzeugen (Erweiterung <code>.sha-512</code>)",
            "Générer les fichiers sidecar de hachage SHA-512 (extension <code>.sha-512</code>)",
            "Generar archivos sidecar de hash SHA-512 (extensión <code>.sha-512</code>)",
            "Genera i sidecar hash SHA-512 (estensione <code>.sha-512</code>)",
        ),
        "settings.sidecar_sha3_512_label" => (
            "Generate SHA3-512 hashes sidecars (extension <code>.sha3-512</code>)",
            "SHA3-512-Hash-Sidecars erzeugen (Erweiterung <code>.sha3-512</code>)",
            "Générer les fichiers sidecar de hachage SHA3-512 (extension <code>.sha3-512</code>)",
            "Generar archivos sidecar de hash SHA3-512 (extensión <code>.sha3-512</code>)",
            "Genera i sidecar hash SHA3-512 (estensione <code>.sha3-512</code>)",
        ),
        "settings.sidecar_blake3_512_label" => (
            "Generate BLAKE3-512 hashes sidecars (extension <code>.blake3-512</code>)",
            "BLAKE3-512-Hash-Sidecars erzeugen (Erweiterung <code>.blake3-512</code>)",
            "Générer les fichiers sidecar de hachage BLAKE3-512 (extension <code>.blake3-512</code>)",
            "Generar archivos sidecar de hash BLAKE3-512 (extensión <code>.blake3-512</code>)",
            "Genera i sidecar hash BLAKE3-512 (estensione <code>.blake3-512</code>)",
        ),
        "settings.sidecar_shake256_512_label" => (
            "Generate SHAKE256-512 hashes sidecars (extension <code>.shake256-512</code>)",
            "SHAKE256-512-Hash-Sidecars erzeugen (Erweiterung <code>.shake256-512</code>)",
            "Générer les fichiers sidecar de hachage SHAKE256-512 (extension <code>.shake256-512</code>)",
            "Generar archivos sidecar de hash SHAKE256-512 (extensión <code>.shake256-512</code>)",
            "Genera i sidecar hash SHAKE256-512 (estensione <code>.shake256-512</code>)",
        ),
        _ => return None,
    })
}

/// Classification section: heading, intro, TLP default, storage mode.
fn tr_classification(key: &str) -> Option<Entry> {
    Some(match key {
        "settings.section_classification" => (
            "Classification",
            "Einstufung",
            "Classification",
            "Clasificación",
            "Classificazione",
        ),
        "settings.classification_intro" => (
            "Defaults for the colored TLP 2.0 selector, the German Verschlusssache field, and the NATO classification field. These values prefill the CSAF create form; per-document overrides are still possible.",
            "Standardwerte für den farbigen TLP-2.0-Selektor, das deutsche Verschlusssache-Feld und das NATO-Einstufungsfeld. Diese Werte werden in das CSAF-Erstellungsformular vorausgefüllt; Überschreibungen pro Dokument sind weiterhin möglich.",
            "Valeurs par défaut pour le sélecteur coloré TLP 2.0, le champ Verschlusssache allemand et le champ de classification NATO. Ces valeurs préremplissent le formulaire de création CSAF ; une substitution par document reste possible.",
            "Valores predeterminados para el selector de colores TLP 2.0, el campo Verschlusssache alemán y el campo de clasificación NATO. Estos valores prellenan el formulario de creación de CSAF; aún es posible anularlos por documento.",
            "Valori predefiniti per il selettore colorato TLP 2.0, il campo Verschlusssache tedesco e il campo di classificazione NATO. Questi valori precompilano il modulo di creazione CSAF; è comunque possibile sovrascriverli per singolo documento.",
        ),
        "settings.tlp_default_label" => (
            "TLP 2.0 default *",
            "TLP-2.0-Standard *",
            "Valeur TLP 2.0 par défaut *",
            "Valor predeterminado de TLP 2.0 *",
            "Valore predefinito TLP 2.0 *",
        ),
        "settings.tlp_default_help" => (
            r#"Colour-coding per <a href="https://www.first.org/tlp/" target="_blank" rel="noopener">FIRST TLP 2.0</a>."#,
            r#"Farbcodierung gemäß <a href="https://www.first.org/tlp/" target="_blank" rel="noopener">FIRST TLP 2.0</a>."#,
            r#"Codage couleur selon <a href="https://www.first.org/tlp/" target="_blank" rel="noopener">FIRST TLP 2.0</a>."#,
            r#"Codificación por colores según <a href="https://www.first.org/tlp/" target="_blank" rel="noopener">FIRST TLP 2.0</a>."#,
            r#"Codifica a colori secondo <a href="https://www.first.org/tlp/" target="_blank" rel="noopener">FIRST TLP 2.0</a>."#,
        ),
        "settings.storage_mode_label" => (
            "Storage mode *",
            "Speichermodus *",
            "Mode de stockage *",
            "Modo de almacenamiento *",
            "Modalità di archiviazione *",
        ),
        "settings.storage_mode_help" => (
            "Where to write Verschlusssache / NATO values in the exported CSAF JSON. <code>both</code> writes to <code>document.distribution.text</code> AND <code>document.notes[]</code> so any consumer can find them.",
            "Legt fest, wohin Verschlusssache-/NATO-Werte im exportierten CSAF-JSON geschrieben werden. <code>both</code> schreibt in <code>document.distribution.text</code> UND <code>document.notes[]</code>, damit jeder Konsument sie findet.",
            "Indique où écrire les valeurs Verschlusssache / NATO dans le CSAF JSON exporté. <code>both</code> écrit dans <code>document.distribution.text</code> ET <code>document.notes[]</code> afin que tout consommateur puisse les trouver.",
            "Indica dónde escribir los valores Verschlusssache/NATO en el CSAF JSON exportado. <code>both</code> escribe en <code>document.distribution.text</code> Y <code>document.notes[]</code> para que cualquier consumidor pueda encontrarlos.",
            "Indica dove scrivere i valori Verschlusssache/NATO nel CSAF JSON esportato. <code>both</code> scrive in <code>document.distribution.text</code> E <code>document.notes[]</code> in modo che qualsiasi consumer possa trovarli.",
        ),
        _ => return None,
    })
}

/// The `<option>` display text for the storage-mode dropdown. The `value`
/// attribute stays the fixed `distribution_text`/`notes`/`both` string in
/// every language (see [`crate::routes::settings::options_from_labels`]);
/// only the "Only"/"Both...AND" surrounding words are translated, never the
/// `document.distribution.text` / `document.notes[]` JSON field paths.
fn tr_storage_mode_options(key: &str) -> Option<Entry> {
    Some(match key {
        "settings.storage_mode_option_distribution_text" => (
            "Only document.distribution.text",
            "Nur document.distribution.text",
            "Uniquement document.distribution.text",
            "Solo document.distribution.text",
            "Solo document.distribution.text",
        ),
        "settings.storage_mode_option_notes" => (
            "Only document.notes[]",
            "Nur document.notes[]",
            "Uniquement document.notes[]",
            "Solo document.notes[]",
            "Solo document.notes[]",
        ),
        "settings.storage_mode_option_both" => (
            "Both (distribution.text AND notes[])",
            "Beide (distribution.text UND notes[])",
            "Les deux (distribution.text ET notes[])",
            "Ambos (distribution.text Y notes[])",
            "Entrambi (distribution.text E notes[])",
        ),
        _ => return None,
    })
}

/// Verschlusssache and NATO enable-checkboxes and default-value fields.
fn tr_verschlusssache_nato(key: &str) -> Option<Entry> {
    Some(match key {
        "settings.verschlusssache_enable_label" => (
            "Enable Verschlusssache",
            "Verschlusssache aktivieren",
            "Activer Verschlusssache",
            "Habilitar Verschlusssache",
            "Abilita Verschlusssache",
        ),
        "settings.verschlusssache_default_label" => (
            "Verschlusssache default",
            "Verschlusssache-Standard",
            "Valeur Verschlusssache par défaut",
            "Valor Verschlusssache predeterminado",
            "Valore Verschlusssache predefinito",
        ),
        "settings.verschlusssache_default_help" => (
            r#"German national classification per <a href="https://de.wikipedia.org/wiki/Verschlusssache#Einstufung" target="_blank" rel="noopener">§ Verschlusssache</a>."#,
            r#"Deutsche nationale Einstufung gemäß <a href="https://de.wikipedia.org/wiki/Verschlusssache#Einstufung" target="_blank" rel="noopener">§ Verschlusssache</a>."#,
            r#"Classification nationale allemande selon <a href="https://de.wikipedia.org/wiki/Verschlusssache#Einstufung" target="_blank" rel="noopener">§ Verschlusssache</a>."#,
            r#"Clasificación nacional alemana según <a href="https://de.wikipedia.org/wiki/Verschlusssache#Einstufung" target="_blank" rel="noopener">§ Verschlusssache</a>."#,
            r#"Classificazione nazionale tedesca secondo <a href="https://de.wikipedia.org/wiki/Verschlusssache#Einstufung" target="_blank" rel="noopener">§ Verschlusssache</a>."#,
        ),
        "settings.nato_enable_label" => (
            "Enable NATO",
            "NATO aktivieren",
            "Activer NATO",
            "Habilitar NATO",
            "Abilita NATO",
        ),
        "settings.nato_default_label" => (
            "NATO default",
            "NATO-Standard",
            "Valeur NATO par défaut",
            "Valor NATO predeterminado",
            "Valore NATO predefinito",
        ),
        "settings.nato_default_help" => (
            r#"NATO classification per <a href="https://de.wikipedia.org/wiki/Geheimhaltungsgrad#NATO" target="_blank" rel="noopener">§ Geheimhaltungsgrad</a>."#,
            r#"NATO-Einstufung gemäß <a href="https://de.wikipedia.org/wiki/Geheimhaltungsgrad#NATO" target="_blank" rel="noopener">§ Geheimhaltungsgrad</a>."#,
            r#"Classification NATO selon <a href="https://de.wikipedia.org/wiki/Geheimhaltungsgrad#NATO" target="_blank" rel="noopener">§ Geheimhaltungsgrad</a>."#,
            r#"Clasificación NATO según <a href="https://de.wikipedia.org/wiki/Geheimhaltungsgrad#NATO" target="_blank" rel="noopener">§ Geheimhaltungsgrad</a>."#,
            r#"Classificazione NATO secondo <a href="https://de.wikipedia.org/wiki/Geheimhaltungsgrad#NATO" target="_blank" rel="noopener">§ Geheimhaltungsgrad</a>."#,
        ),
        _ => return None,
    })
}

/// Publisher Defaults section: heading, intro, name/namespace/category/contact.
fn tr_publisher(key: &str) -> Option<Entry> {
    Some(match key {
        "settings.section_publisher" => (
            "Publisher Defaults",
            "Herausgeber-Standardwerte",
            "Valeurs par défaut de l'éditeur",
            "Valores predeterminados del editor",
            "Valori predefiniti dell'editore",
        ),
        "settings.publisher_intro" => (
            "These values are prefilled into the CSAF create form and inherited by every new document. You can still override them per document at creation time.",
            "Diese Werte werden in das CSAF-Erstellungsformular vorausgefüllt und von jedem neuen Dokument übernommen. Sie können sie bei der Erstellung weiterhin pro Dokument überschreiben.",
            "Ces valeurs préremplissent le formulaire de création CSAF et sont héritées par chaque nouveau document. Vous pouvez toujours les remplacer par document lors de la création.",
            "Estos valores prellenan el formulario de creación de CSAF y se heredan en cada documento nuevo. Aún puede anularlos por documento en el momento de la creación.",
            "Questi valori precompilano il modulo di creazione CSAF e vengono ereditati da ogni nuovo documento. È comunque possibile sovrascriverli per singolo documento al momento della creazione.",
        ),
        "settings.publisher_name_label" => ("Name *", "Name *", "Nom *", "Nombre *", "Nome *"),
        "settings.publisher_namespace_label" => (
            "Namespace *",
            "Namensraum *",
            "Espace de noms *",
            "Espacio de nombres *",
            "Spazio dei nomi *",
        ),
        "settings.publisher_category_label" => (
            "Category *",
            "Kategorie *",
            "Catégorie *",
            "Categoría *",
            "Categoria *",
        ),
        "settings.publisher_category_help" => (
            "OASIS CSAF publisher category.",
            "OASIS-CSAF-Herausgeberkategorie.",
            "Catégorie d'éditeur OASIS CSAF.",
            "Categoría de editor OASIS CSAF.",
            "Categoria dell'editore OASIS CSAF.",
        ),
        "settings.publisher_contact_label" => (
            "Contact details",
            "Kontaktdaten",
            "Coordonnées de contact",
            "Datos de contacto",
            "Dati di contatto",
        ),
        "settings.publisher_contact_help" => (
            "Usually an email address or security contact URL.",
            "In der Regel eine E-Mail-Adresse oder eine URL für den Sicherheitskontakt.",
            "Généralement une adresse e-mail ou une URL de contact sécurité.",
            "Normalmente una dirección de correo electrónico o una URL de contacto de seguridad.",
            "Di solito un indirizzo e-mail o un URL di contatto per la sicurezza.",
        ),
        _ => return None,
    })
}

/// The two form-submit buttons and the reset form's help/confirm text.
///
/// `settings.reset_confirm` is interpolated into a single-quoted JS string
/// literal (`onsubmit="return confirm('...');"` in
/// `routes::settings::render_settings_content`) — every language's text
/// MUST avoid a literal `'` (apostrophe) or `"` character, or it would break
/// out of the attribute/string it is embedded in. The completeness test
/// below guards this.
fn tr_actions(key: &str) -> Option<Entry> {
    Some(match key {
        "settings.save_button" => (
            "Save Settings",
            "Einstellungen speichern",
            "Enregistrer les paramètres",
            "Guardar configuración",
            "Salva impostazioni",
        ),
        "settings.reset_button" => (
            "Reset to default settings",
            "Auf Standardeinstellungen zurücksetzen",
            "Réinitialiser aux paramètres par défaut",
            "Restablecer a la configuración predeterminada",
            "Ripristina le impostazioni predefinite",
        ),
        "settings.reset_help" => (
            "Restores every field on this page to the application defaults.",
            "Setzt alle Felder auf dieser Seite auf die Anwendungsstandardwerte zurück.",
            "Réinitialise tous les champs de cette page aux valeurs par défaut de l'application.",
            "Restaura todos los campos de esta página a los valores predeterminados de la aplicación.",
            "Ripristina tutti i campi di questa pagina ai valori predefiniti dell'applicazione.",
        ),
        "settings.reset_confirm" => (
            "Reset ALL settings to their default values? This cannot be undone.",
            "Sollen ALLE Einstellungen auf ihre Standardwerte zurückgesetzt werden? Dies kann nicht rückgängig gemacht werden.",
            "Réinitialiser TOUS les paramètres à leurs valeurs par défaut ? Cette action est irréversible.",
            "¿Restablecer TODOS los ajustes a sus valores predeterminados? Esta acción no se puede deshacer.",
            "Ripristinare TUTTE le impostazioni ai valori predefiniti? Questa azione non può essere annullata.",
        ),
        _ => return None,
    })
}

/// `validate_settings_form` error strings for the general/path fields.
fn tr_validation_fields(key: &str) -> Option<Entry> {
    Some(match key {
        "settings.err_csaf_mode" => (
            "CSAF mode must be '2.0' or '2.1'.",
            "Der CSAF-Modus muss '2.0' oder '2.1' sein.",
            "Le mode CSAF doit être '2.0' ou '2.1'.",
            "El modo CSAF debe ser '2.0' o '2.1'.",
            "La modalità CSAF deve essere '2.0' o '2.1'.",
        ),
        "settings.err_theme" => (
            "Theme must be 'light' or 'dark'.",
            "Das Design muss 'light' oder 'dark' sein.",
            "Le thème doit être 'light' ou 'dark'.",
            "El tema debe ser 'light' o 'dark'.",
            "Il tema deve essere 'light' o 'dark'.",
        ),
        "settings.err_import_directory" => (
            "Import Directory contains invalid or unsafe path characters.",
            "Das Import-Verzeichnis enthält ungültige oder unsichere Pfadzeichen.",
            "Le répertoire d'importation contient des caractères de chemin non valides ou dangereux.",
            "El directorio de importación contiene caracteres de ruta no válidos o inseguros.",
            "La directory di importazione contiene caratteri di percorso non validi o non sicuri.",
        ),
        "settings.err_export_directory" => (
            "Export Directory contains invalid or unsafe path characters.",
            "Das Export-Verzeichnis enthält ungültige oder unsichere Pfadzeichen.",
            "Le répertoire d'exportation contient des caractères de chemin non valides ou dangereux.",
            "El directorio de exportación contiene caracteres de ruta no válidos o inseguros.",
            "La directory di esportazione contiene caratteri di percorso non validi o non sicuri.",
        ),
        "settings.err_dump_directory" => (
            "Dump Directory contains invalid or unsafe path characters.",
            "Das Dump-Verzeichnis enthält ungültige oder unsichere Pfadzeichen.",
            "Le répertoire de vidage contient des caractères de chemin non valides ou dangereux.",
            "El directorio de volcado contiene caracteres de ruta no válidos o inseguros.",
            "La directory di dump contiene caratteri di percorso non validi o non sicuri.",
        ),
        "settings.err_log_directory" => (
            "Log Directory contains invalid or unsafe path characters.",
            "Das Protokoll-Verzeichnis enthält ungültige oder unsichere Pfadzeichen.",
            "Le répertoire des journaux contient des caractères de chemin non valides ou dangereux.",
            "El directorio de registros contiene caracteres de ruta no válidos o inseguros.",
            "La directory dei log contiene caratteri di percorso non validi o non sicuri.",
        ),
        _ => return None,
    })
}

/// `validate_settings_form` error strings for publisher and classification
/// fields. The enumerated literal value lists (`vendor, discoverer, ...`,
/// `CLEAR, GREEN, ...`, the NATO labels, `'distribution_text', 'notes', ...`)
/// are kept byte-for-byte identical across every language: they are the
/// exact values the user must type/select, and the code's whitelist accepts
/// nothing else — see the module doc comment.
fn tr_validation_publisher_classification(key: &str) -> Option<Entry> {
    Some(match key {
        "settings.err_publisher_name" => (
            "Publisher name must not be empty.",
            "Der Herausgebername darf nicht leer sein.",
            "Le nom de l'éditeur ne doit pas être vide.",
            "El nombre del editor no debe estar vacío.",
            "Il nome dell'editore non deve essere vuoto.",
        ),
        "settings.err_publisher_namespace" => (
            "Publisher namespace must not be empty.",
            "Der Herausgeber-Namensraum darf nicht leer sein.",
            "L'espace de noms de l'éditeur ne doit pas être vide.",
            "El espacio de nombres del editor no debe estar vacío.",
            "Lo spazio dei nomi dell'editore non deve essere vuoto.",
        ),
        "settings.err_publisher_category" => (
            "Publisher category must be one of: vendor, discoverer, coordinator, user, translator, other.",
            "Die Herausgeberkategorie muss eine der folgenden sein: vendor, discoverer, coordinator, user, translator, other.",
            "La catégorie d'éditeur doit être l'une des suivantes : vendor, discoverer, coordinator, user, translator, other.",
            "La categoría de editor debe ser una de las siguientes: vendor, discoverer, coordinator, user, translator, other.",
            "La categoria dell'editore deve essere una tra: vendor, discoverer, coordinator, user, translator, other.",
        ),
        "settings.err_tlp_default" => (
            "TLP 2.0 default must be one of: CLEAR, GREEN, AMBER, AMBER+STRICT, RED.",
            "Der TLP-2.0-Standard muss einer der folgenden sein: CLEAR, GREEN, AMBER, AMBER+STRICT, RED.",
            "La valeur TLP 2.0 par défaut doit être l'une des suivantes : CLEAR, GREEN, AMBER, AMBER+STRICT, RED.",
            "El valor predeterminado de TLP 2.0 debe ser uno de los siguientes: CLEAR, GREEN, AMBER, AMBER+STRICT, RED.",
            "Il valore predefinito TLP 2.0 deve essere uno tra: CLEAR, GREEN, AMBER, AMBER+STRICT, RED.",
        ),
        "settings.err_verschlusssache_default" => (
            "Verschlusssache default must be one of the four official German classification labels.",
            "Der Verschlusssache-Standard muss eine der vier offiziellen deutschen Einstufungsbezeichnungen sein.",
            "La valeur Verschlusssache par défaut doit être l'une des quatre classifications officielles allemandes.",
            "El valor Verschlusssache predeterminado debe ser una de las cuatro etiquetas de clasificación oficiales alemanas.",
            "Il valore Verschlusssache predefinito deve essere una delle quattro etichette di classificazione ufficiali tedesche.",
        ),
        "settings.err_nato_default" => (
            "NATO default must be one of: NR (NATO RESTRICTED), NC (NATO CONFIDENTIAL), NS (NATO SECRET), CTS (COSMIC TOP SECRET).",
            "Der NATO-Standard muss einer der folgenden sein: NR (NATO RESTRICTED), NC (NATO CONFIDENTIAL), NS (NATO SECRET), CTS (COSMIC TOP SECRET).",
            "La valeur NATO par défaut doit être l'une des suivantes : NR (NATO RESTRICTED), NC (NATO CONFIDENTIAL), NS (NATO SECRET), CTS (COSMIC TOP SECRET).",
            "El valor NATO predeterminado debe ser uno de los siguientes: NR (NATO RESTRICTED), NC (NATO CONFIDENTIAL), NS (NATO SECRET), CTS (COSMIC TOP SECRET).",
            "Il valore NATO predefinito deve essere uno tra: NR (NATO RESTRICTED), NC (NATO CONFIDENTIAL), NS (NATO SECRET), CTS (COSMIC TOP SECRET).",
        ),
        "settings.err_classification_storage_mode" => (
            "Classification storage mode must be 'distribution_text', 'notes', or 'both'.",
            "Der Speichermodus für die Einstufung muss 'distribution_text', 'notes' oder 'both' sein.",
            "Le mode de stockage de la classification doit être 'distribution_text', 'notes' ou 'both'.",
            "El modo de almacenamiento de la clasificación debe ser 'distribution_text', 'notes' o 'both'.",
            "La modalità di archiviazione della classificazione deve essere 'distribution_text', 'notes' o 'both'.",
        ),
        _ => return None,
    })
}

#[cfg(test)]
mod tests {
    use super::super::{Lang, t};
    use super::*;

    /// Every key this module claims, across all ten `tr_*` groups.
    const ALL_KEYS: &[&str] = &[
        "settings.section_general",
        "settings.csaf_mode_label",
        "settings.csaf_mode_help",
        "settings.theme_label",
        "settings.theme_help",
        "settings.import_directory_label",
        "settings.import_directory_help",
        "settings.export_directory_label",
        "settings.export_directory_help",
        "settings.dump_directory_label",
        "settings.dump_directory_help",
        "settings.log_directory_label",
        "settings.log_directory_help",
        "settings.naming_convention_label",
        "settings.naming_convention_help",
        "settings.sidecar_hash_files_label",
        "settings.sidecar_sha256_label",
        "settings.sidecar_sha512_label",
        "settings.sidecar_sha3_512_label",
        "settings.sidecar_blake3_512_label",
        "settings.sidecar_shake256_512_label",
        "settings.section_classification",
        "settings.classification_intro",
        "settings.tlp_default_label",
        "settings.tlp_default_help",
        "settings.storage_mode_label",
        "settings.storage_mode_help",
        "settings.storage_mode_option_distribution_text",
        "settings.storage_mode_option_notes",
        "settings.storage_mode_option_both",
        "settings.verschlusssache_enable_label",
        "settings.verschlusssache_default_label",
        "settings.verschlusssache_default_help",
        "settings.nato_enable_label",
        "settings.nato_default_label",
        "settings.nato_default_help",
        "settings.section_publisher",
        "settings.publisher_intro",
        "settings.publisher_name_label",
        "settings.publisher_namespace_label",
        "settings.publisher_category_label",
        "settings.publisher_category_help",
        "settings.publisher_contact_label",
        "settings.publisher_contact_help",
        "settings.save_button",
        "settings.reset_button",
        "settings.reset_help",
        "settings.reset_confirm",
        "settings.err_csaf_mode",
        "settings.err_theme",
        "settings.err_import_directory",
        "settings.err_export_directory",
        "settings.err_dump_directory",
        "settings.err_log_directory",
        "settings.err_publisher_name",
        "settings.err_publisher_namespace",
        "settings.err_publisher_category",
        "settings.err_tlp_default",
        "settings.err_verschlusssache_default",
        "settings.err_nato_default",
        "settings.err_classification_storage_mode",
    ];

    /// Protocol-fixed values (see `csaf-models::settings`) that must never
    /// appear as the FULL text of any `settings.*` translation — they are
    /// literal `<option value="...">` values / enum members compared with
    /// `==`, not UI prose. A translation table entry that returned one of
    /// these verbatim strings would indicate a fixed value leaked into a
    /// label instead of staying in the surrounding (translated) prose.
    const PROTOCOL_FIXED_VALUES: &[&str] = &[
        "CLEAR",
        "GREEN",
        "AMBER",
        "AMBER+STRICT",
        "RED",
        "VS-NfD (VS-NUR FÜR DEN DIENSTGEBRAUCH)",
        "VS-Vertr. (VS-VERTRAULICH)",
        "Geh. (GEHEIM)",
        "Str. Geh. (STRENG GEHEIM)",
        "NR (NATO RESTRICTED)",
        "NC (NATO CONFIDENTIAL)",
        "NS (NATO SECRET)",
        "CTS (COSMIC TOP SECRET)",
        "distribution_text",
        "notes",
        "both",
        "2.0",
        "2.1",
        "light",
        "dark",
        "vendor",
        "discoverer",
        "coordinator",
        "user",
        "translator",
        "other",
    ];

    #[test]
    fn every_settings_key_has_five_nonempty_translations() {
        for key in ALL_KEYS {
            let (en, de, fr, es, it) = tr(key).unwrap_or_else(|| panic!("{key} resolves"));
            assert!(
                !en.is_empty()
                    && !de.is_empty()
                    && !fr.is_empty()
                    && !es.is_empty()
                    && !it.is_empty(),
                "{key}"
            );
        }
    }

    #[test]
    fn unknown_key_is_not_claimed_by_this_table() {
        assert_eq!(tr("settings.does_not_exist"), None);
    }

    #[test]
    fn t_dispatches_settings_keys_through_the_shared_lookup() {
        assert_eq!(
            t(Lang::De, "settings.save_button"),
            "Einstellungen speichern"
        );
        assert_eq!(
            t(Lang::Fr, "settings.err_theme"),
            "Le thème doit être 'light' ou 'dark'."
        );
        assert_eq!(t(Lang::It, "settings.nato_enable_label"), "Abilita NATO");
    }

    #[test]
    fn no_translation_ever_equals_a_protocol_fixed_value_verbatim() {
        // Defensive regression guard: a `settings.*` entry must never
        // resolve to exactly one of the fixed enum/whitelist values (see
        // the module doc comment) — that would mean a protocol-fixed
        // literal leaked into a translatable label instead of staying
        // untouched in the surrounding prose.
        for key in ALL_KEYS {
            let (en, de, fr, es, it) = tr(key).unwrap_or_else(|| panic!("{key} resolves"));
            for (lang_code, text) in [("en", en), ("de", de), ("fr", fr), ("es", es), ("it", it)] {
                assert!(
                    !PROTOCOL_FIXED_VALUES.contains(&text),
                    "{key} ({lang_code}) resolved to the protocol-fixed value {text:?} verbatim"
                );
            }
        }
    }

    #[test]
    fn reset_confirm_never_contains_a_quote_character_in_any_language() {
        // `settings.reset_confirm` is interpolated into
        // `onsubmit="return confirm('{reset_confirm}');"` — a literal `'`
        // would close the JS string early and a literal `"` would close
        // the HTML attribute early, both breaking the reset confirmation
        // dialog (or worse, letting later form content run as markup).
        let texts: [&str; 5] = tr("settings.reset_confirm").expect("key resolves").into();
        for text in texts {
            assert!(
                !text.contains('\''),
                "reset_confirm contains an apostrophe: {text:?}"
            );
            assert!(
                !text.contains('"'),
                "reset_confirm contains a double quote: {text:?}"
            );
        }
    }
}