Skip to main content

eulumdat_i18n/
lib.rs

1//! Internationalization for Eulumdat/ATLA photometric libraries
2//!
3//! This crate provides localized strings for:
4//! - SVG diagram labels (polar, cartesian, heatmap, spectral)
5//! - UI strings for web and mobile apps
6//! - Report generation
7//!
8//! # Supported Languages
9//!
10//! - English (en) - default
11//! - German (de)
12//! - Chinese Simplified (zh)
13//! - French (fr)
14//! - Italian (it)
15//! - Russian (ru)
16//! - Spanish (es)
17//! - Portuguese Brazilian (pt-BR)
18//!
19//! # Usage
20//!
21//! ```rust
22//! use eulumdat_i18n::{Locale, Language};
23//!
24//! // Get English locale (default)
25//! let en = Locale::english();
26//! assert_eq!(en.diagram.axis.gamma, "Gamma (γ)");
27//!
28//! // Get German locale
29//! let de = Locale::german();
30//! assert_eq!(de.diagram.axis.intensity, "Lichtstärke (cd/klm)");
31//!
32//! // Get by language code
33//! let locale = Locale::for_language(Language::Chinese);
34//! assert_eq!(locale.diagram.placeholder.no_data, "无数据");
35//! ```
36
37use serde::{Deserialize, Serialize};
38
39/// Supported languages
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
41pub enum Language {
42    #[default]
43    English,
44    German,
45    Chinese,
46    French,
47    Italian,
48    Russian,
49    Spanish,
50    PortugueseBrazil,
51}
52
53impl Language {
54    /// Get language from ISO 639-1 code
55    pub fn from_code(code: &str) -> Self {
56        match code.to_lowercase().as_str() {
57            "en" => Self::English,
58            "de" => Self::German,
59            "zh" | "zh-cn" | "zh-hans" => Self::Chinese,
60            "fr" => Self::French,
61            "it" => Self::Italian,
62            "ru" => Self::Russian,
63            "es" => Self::Spanish,
64            "pt" | "pt-br" => Self::PortugueseBrazil,
65            _ => Self::English,
66        }
67    }
68
69    /// Get ISO 639-1 code
70    pub fn code(&self) -> &'static str {
71        match self {
72            Self::English => "en",
73            Self::German => "de",
74            Self::Chinese => "zh",
75            Self::French => "fr",
76            Self::Italian => "it",
77            Self::Russian => "ru",
78            Self::Spanish => "es",
79            Self::PortugueseBrazil => "pt-BR",
80        }
81    }
82
83    /// Get native language name
84    pub fn native_name(&self) -> &'static str {
85        match self {
86            Self::English => "English",
87            Self::German => "Deutsch",
88            Self::Chinese => "简体中文",
89            Self::French => "Français",
90            Self::Italian => "Italiano",
91            Self::Russian => "Русский",
92            Self::Spanish => "Español",
93            Self::PortugueseBrazil => "Português (Brasil)",
94        }
95    }
96
97    /// All available languages
98    pub fn all() -> &'static [Language] {
99        &[
100            Self::English,
101            Self::German,
102            Self::Chinese,
103            Self::French,
104            Self::Italian,
105            Self::Russian,
106            Self::Spanish,
107            Self::PortugueseBrazil,
108        ]
109    }
110}
111
112/// Complete locale with all translatable strings
113#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
114pub struct Locale {
115    pub meta: LocaleMeta,
116    pub diagram: DiagramLocale,
117    pub spectral: SpectralLocale,
118    pub luminaire: LuminaireLocale,
119    pub validation: ValidationLocale,
120    pub ui: UiLocale,
121    pub report: ReportLocale,
122    #[serde(default)]
123    pub comparison: ComparisonLocale,
124}
125
126/// Locale metadata
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
128pub struct LocaleMeta {
129    pub language: String,
130    pub code: String,
131    pub direction: String,
132}
133
134/// Diagram-related translations
135#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
136pub struct DiagramLocale {
137    pub units: DiagramUnits,
138    pub bug: DiagramBug,
139    pub axis: DiagramAxis,
140    pub plane: DiagramPlane,
141    pub angle: DiagramAngle,
142    pub metrics: DiagramMetrics,
143    pub title: DiagramTitle,
144    pub placeholder: DiagramPlaceholder,
145    pub cone: DiagramCone,
146    pub greenhouse: DiagramGreenhouse,
147}
148
149#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
150pub struct DiagramBug {
151    pub forward_light: String,
152    pub back_light: String,
153    pub uplight: String,
154    pub total: String,
155    pub sum: String,
156    pub zone_low: String,
157    pub zone_medium: String,
158    pub zone_high: String,
159    pub zone_very_high: String,
160    pub lumens: String,
161    pub percent: String,
162}
163
164#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
165pub struct DiagramUnits {
166    pub intensity: String,
167    pub intensity_short: String,
168    pub candela: String,
169    pub lumen: String,
170    pub watt: String,
171    pub lux: String,
172}
173
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
175pub struct DiagramAxis {
176    pub gamma: String,
177    pub intensity: String,
178    pub c_plane: String,
179    pub gamma_angle: String,
180}
181
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183pub struct DiagramPlane {
184    pub c0_c180: String,
185    pub c90_c270: String,
186}
187
188#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
189pub struct DiagramAngle {
190    pub beam: String,
191    pub field: String,
192    pub beam_50: String,
193    pub field_10: String,
194}
195
196#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
197pub struct DiagramMetrics {
198    pub cie: String,
199    pub efficacy: String,
200    pub max: String,
201    pub sh_ratio: String,
202}
203
204#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
205pub struct DiagramTitle {
206    pub heatmap: String,
207    pub polar: String,
208    pub cartesian: String,
209    pub cone: String,
210}
211
212#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
213pub struct DiagramPlaceholder {
214    pub no_data: String,
215}
216
217/// Cone diagram translations (beam spread visualization)
218#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
219pub struct DiagramCone {
220    pub beam_angle: String,
221    pub field_angle: String,
222    pub mounting_height: String,
223    pub beam_diameter: String,
224    pub field_diameter: String,
225    pub intensity_50: String,
226    pub intensity_10: String,
227    pub floor: String,
228    pub meter: String,
229    pub classification: ConeClassification,
230    pub c_plane: String,
231    pub all_planes: String,
232    pub symmetric_note: String,
233    pub illuminance_table: ConeIlluminanceTableLocale,
234}
235
236/// Cone illuminance table translations
237#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
238pub struct ConeIlluminanceTableLocale {
239    pub title: String,
240    pub height: String,
241    pub beam_field_diameter: String,
242    pub e_nadir: String,
243    pub e_c0: String,
244    pub e_c90: String,
245    pub no_flux: String,
246}
247
248/// Beam classification labels
249#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
250pub struct ConeClassification {
251    pub very_narrow_spot: String,
252    pub narrow_spot: String,
253    pub spot: String,
254    pub medium_flood: String,
255    pub wide_flood: String,
256    pub very_wide_flood: String,
257}
258
259/// Greenhouse diagram translations (PPFD at distance)
260#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
261pub struct DiagramGreenhouse {
262    pub max_height: String,
263}
264
265/// Spectral diagram translations
266#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
267pub struct SpectralLocale {
268    pub axis: SpectralAxis,
269    pub title: SpectralTitle,
270    pub region: SpectralRegion,
271    pub warning: SpectralWarning,
272    pub units: SpectralUnits,
273    pub tm30: Tm30Locale,
274    pub metrics: SpectralMetrics,
275}
276
277#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
278pub struct SpectralAxis {
279    pub wavelength: String,
280    pub relative_power: String,
281}
282
283#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
284pub struct SpectralTitle {
285    pub spd: String,
286    pub cvg: String,
287    pub hue: String,
288}
289
290#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
291pub struct SpectralRegion {
292    pub uv_a: String,
293    pub visible: String,
294    pub near_ir: String,
295    pub blue: String,
296    pub green: String,
297    pub red: String,
298}
299
300#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
301pub struct SpectralWarning {
302    pub uv_thermal: String,
303    pub uv_exposure: String,
304    pub high_thermal: String,
305}
306
307#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
308pub struct SpectralUnits {
309    pub watts_per_nm: String,
310    pub relative: String,
311}
312
313#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
314pub struct Tm30Locale {
315    pub rf: String,
316    pub rg: String,
317    pub reference: String,
318    pub test: String,
319}
320
321#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
322pub struct SpectralMetrics {
323    pub energy_distribution: String,
324    pub uv_percent: String,
325    pub visible_percent: String,
326    pub ir_percent: String,
327    pub r_fr_ratio: String,
328}
329
330/// Luminaire information translations
331#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
332pub struct LuminaireLocale {
333    pub info: LuminaireInfo,
334    pub physical: LuminairePhysical,
335    pub optical: LuminaireOptical,
336    pub photometric: LuminairePhotometric,
337    pub electrical: LuminaireElectrical,
338    pub lamp_set: LampSetLocale,
339    pub summary: SummaryLocale,
340    pub direct_ratios: DirectRatiosLocale,
341}
342
343#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
344pub struct LuminaireInfo {
345    pub manufacturer: String,
346    pub catalog_number: String,
347    pub description: String,
348    pub luminaire_name: String,
349    pub luminaire_number: String,
350    pub luminaire_type: String,
351    pub lamp_type: String,
352    pub test_date: String,
353    pub report_number: String,
354    pub laboratory: String,
355    pub identification: String,
356    pub type_indicator: String,
357    pub type_indicator_1: String,
358    pub type_indicator_2: String,
359    pub type_indicator_3: String,
360    pub symmetry: String,
361    pub symmetry_0: String,
362    pub symmetry_1: String,
363    pub symmetry_2: String,
364    pub symmetry_3: String,
365    pub symmetry_4: String,
366    pub num_c_planes: String,
367    pub c_plane_distance: String,
368    pub num_g_planes: String,
369    pub g_plane_distance: String,
370    pub measurement_report: String,
371    pub file_name: String,
372    pub date_user: String,
373}
374
375#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
376pub struct LuminairePhysical {
377    pub dimensions: String,
378    pub dimensions_mm: String,
379    pub length: String,
380    pub length_diameter: String,
381    pub width: String,
382    pub width_b: String,
383    pub height: String,
384    pub height_h: String,
385    pub luminous_area: String,
386    pub luminous_area_mm: String,
387    pub luminous_length: String,
388    pub luminous_width: String,
389    pub luminous_height_c_planes: String,
390    pub mounting: String,
391}
392
393#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
394pub struct LuminaireOptical {
395    pub title: String,
396    pub downward_flux_fraction: String,
397    pub light_output_ratio: String,
398    pub conversion_factor: String,
399    pub tilt_angle: String,
400}
401
402#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
403pub struct LuminairePhotometric {
404    pub total_flux: String,
405    pub total_lamp_flux: String,
406    pub total_wattage: String,
407    pub lamp_efficacy: String,
408    pub luminaire_efficacy: String,
409    pub efficacy: String,
410    pub lor: String,
411    pub dlor: String,
412    pub ulor: String,
413    pub cct: String,
414    pub cri: String,
415    pub beam_angle: String,
416    pub beam_angle_50: String,
417    pub field_angle: String,
418    pub field_angle_10: String,
419    pub cie_class: String,
420    pub symmetry: String,
421    pub max_intensity: String,
422    pub spacing_criterion: String,
423    pub photometric_code: String,
424    pub cutoff_angle: String,
425}
426
427#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
428pub struct LuminaireElectrical {
429    pub power: String,
430    pub voltage: String,
431    pub current: String,
432    pub power_factor: String,
433}
434
435#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
436pub struct LampSetLocale {
437    pub title: String,
438    pub set_n: String,
439    pub num_lamps: String,
440    pub luminous_flux: String,
441    pub wattage: String,
442    pub lamp_type: String,
443    pub color_appearance: String,
444    pub color_rendering: String,
445    pub remove: String,
446}
447
448#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
449pub struct SummaryLocale {
450    pub title: String,
451    pub description: String,
452    pub beam_characteristics: String,
453    pub zonal_lumens: String,
454    pub glare_assessment: String,
455    pub luminaire_luminance: String,
456    pub room_config: String,
457}
458
459#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
460pub struct DirectRatiosLocale {
461    pub description: String,
462    pub calculate: String,
463}
464
465/// Validation message translations
466#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
467pub struct ValidationLocale {
468    pub level: ValidationLevel,
469    pub messages: ValidationMessageLocale,
470}
471
472#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
473pub struct ValidationLevel {
474    pub error: String,
475    pub warning: String,
476    pub info: String,
477}
478
479/// Validation message translations keyed by code (W001–W046, E001–E006).
480/// Messages may contain `{0}`, `{1}`, … placeholders for dynamic values.
481#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
482pub struct ValidationMessageLocale {
483    pub w001: String,
484    pub w002: String,
485    pub w003: String,
486    pub w004: String,
487    pub w005: String,
488    pub w006: String,
489    pub w007: String,
490    pub w008: String,
491    pub w009: String,
492    pub w010: String,
493    pub w011: String,
494    pub w012: String,
495    pub w013: String,
496    pub w014: String,
497    pub w015: String,
498    pub w016: String,
499    pub w017: String,
500    pub w018: String,
501    pub w019: String,
502    pub w020: String,
503    pub w021: String,
504    pub w022: String,
505    pub w023: String,
506    pub w024: String,
507    pub w025: String,
508    pub w026: String,
509    pub w027: String,
510    pub w028: String,
511    pub w029: String,
512    pub w030: String,
513    pub w031: String,
514    pub w032: String,
515    pub w033: String,
516    pub w034: String,
517    pub w035: String,
518    pub w036: String,
519    pub w037: String,
520    pub w038: String,
521    pub w039: String,
522    pub w040: String,
523    pub w041: String,
524    pub w042: String,
525    pub w043: String,
526    pub w044: String,
527    pub w045: String,
528    pub w046: String,
529    pub w047: String,
530    pub e001: String,
531    pub e002: String,
532    pub e003: String,
533    pub e004: String,
534    pub e005: String,
535    pub e006: String,
536}
537
538/// UI translations
539#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
540pub struct UiLocale {
541    pub header: UiHeader,
542    pub tabs: UiTabs,
543    pub subtabs: UiSubtabs,
544    pub dropzone: UiDropzone,
545    pub diagram: UiDiagram,
546    pub intensity: UiIntensity,
547    pub validation: UiValidation,
548    pub spectral: UiSpectral,
549    pub butterfly: UiButterfly,
550    pub bevy_scene: UiBevyScene,
551    pub bug_rating: UiBugRating,
552    pub lcs: UiLcs,
553    pub floodlight: UiFloodlight,
554    pub data_table: UiDataTable,
555    pub validation_panel: UiValidationPanel,
556    pub spectral_badges: UiSpectralBadges,
557    pub actions: UiActions,
558    pub file: UiFile,
559    pub theme: UiTheme,
560    pub language: UiLanguage,
561    pub template: UiTemplate,
562    pub messages: UiMessages,
563    pub compare: UiCompare,
564}
565
566#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
567pub struct UiHeader {
568    pub title: String,
569    pub file: String,
570    pub new: String,
571    pub open: String,
572    pub templates: String,
573    pub save_ldt: String,
574    pub export_ies: String,
575    pub atla_xml: String,
576    pub atla_json: String,
577    pub switch_to_dark: String,
578    pub switch_to_light: String,
579}
580
581#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
582pub struct UiTabs {
583    // Sub-tabs
584    pub general: String,
585    pub dimensions: String,
586    pub lamp_sets: String,
587    pub direct_ratios: String,
588    pub intensity: String,
589    pub diagram_2d: String,
590    pub diagram_3d: String,
591    pub heatmap: String,
592    pub spectral: String,
593    pub greenhouse: String,
594    pub bug_rating: String,
595    pub lcs: String,
596    pub validation: String,
597    pub scene_3d: String,
598    pub floodlight_vh: String,
599    pub floodlight_isolux: String,
600    pub floodlight_isocandela: String,
601    // Main tab groups
602    pub info: String,
603    pub data: String,
604    pub diagrams: String,
605    pub analysis: String,
606    pub floodlight: String,
607    // Other
608    pub polar: String,
609    pub cartesian: String,
610    pub cone: String,
611    pub export: String,
612    pub compare: String,
613}
614
615#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
616pub struct UiSubtabs {
617    pub spd: String,
618    pub tm30_cvg: String,
619    pub tm30_hue: String,
620    pub metrics: String,
621}
622
623#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
624pub struct UiDropzone {
625    pub text: String,
626    pub current_file: String,
627}
628
629#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
630pub struct UiDiagram {
631    pub title_2d: String,
632    pub title_3d: String,
633    pub title_heatmap: String,
634    pub title_spectral: String,
635    pub title_greenhouse: String,
636    pub title_bug: String,
637    pub title_lcs: String,
638    pub title_scene: String,
639    pub title_floodlight_vh: String,
640    pub title_isolux: String,
641    pub title_isocandela: String,
642    pub polar: String,
643    pub cartesian: String,
644    pub zoom_hint: String,
645    pub rotate_hint: String,
646    pub scene_controls: String,
647}
648
649#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
650pub struct UiIntensity {
651    pub title: String,
652    pub table_info: String,
653}
654
655#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
656pub struct UiValidation {
657    pub title: String,
658}
659
660#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
661pub struct UiSpectral {
662    pub subtitle: String,
663    pub greenhouse_subtitle: String,
664    pub bug_subtitle: String,
665    pub lcs_subtitle: String,
666    pub direct_spd: String,
667    pub sample: String,
668    pub load_hint: String,
669    pub wavelength_range: String,
670    pub peak: String,
671    pub energy_distribution: String,
672    pub par_distribution: String,
673    pub par_total: String,
674    pub hort_metrics: String,
675    pub far_red: String,
676    pub r_fr_hint: String,
677    pub warnings: String,
678    pub thermal_warning: String,
679    pub uv_warning: String,
680    pub cvg_legend1: String,
681    pub cvg_legend2: String,
682    pub hue_table: UiHueTable,
683    pub hue_legend: UiHueLegend,
684}
685
686#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
687pub struct UiHueTable {
688    pub hue: String,
689    pub rf: String,
690    pub rcs: String,
691    pub rhs: String,
692}
693
694#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
695pub struct UiHueLegend {
696    pub rf: String,
697    pub rcs: String,
698    pub rhs: String,
699}
700
701#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
702pub struct UiButterfly {
703    pub pause: String,
704    pub auto: String,
705    pub reset: String,
706    pub drag_hint: String,
707    pub max: String,
708}
709
710#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
711pub struct UiBevyScene {
712    pub title: String,
713    pub click_to_load: String,
714    pub load_button: String,
715    pub loading: String,
716    pub downloading: String,
717    pub load_failed: String,
718    pub try_again: String,
719    // Viewer controls
720    pub scene_type: String,
721    pub scene_room: String,
722    pub scene_road: String,
723    pub scene_parking: String,
724    pub scene_outdoor: String,
725    pub room_width: String,
726    pub room_length: String,
727    pub room_height: String,
728    pub mounting_height: String,
729    pub pendulum_length: String,
730    pub show_luminaire: String,
731    pub show_solid: String,
732    pub show_shadows: String,
733    pub controls_hint: String,
734}
735
736#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
737pub struct UiBugRating {
738    pub title: String,
739    pub title_detailed: String,
740    pub show_details: String,
741    pub hide_details: String,
742    pub footer_basic: String,
743    pub footer_detailed: String,
744}
745
746#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
747pub struct UiLcs {
748    pub footer: String,
749}
750
751#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
752pub struct UiFloodlight {
753    pub vh_subtitle: String,
754    pub isolux_subtitle: String,
755    pub isocandela_subtitle: String,
756    pub h_plane: String,
757    pub v_plane: String,
758    pub log_scale: String,
759    pub linear_scale: String,
760    pub mounting_height: String,
761    pub tilt_angle: String,
762    pub area_size: String,
763    pub nema_classification: String,
764    pub show_contours: String,
765}
766
767#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
768pub struct UiDataTable {
769    pub no_data: String,
770    pub copy_to_clipboard: String,
771}
772
773#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
774pub struct UiValidationPanel {
775    pub all_passed: String,
776    pub error_count: String,
777}
778
779#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
780pub struct UiSpectralBadges {
781    pub ir: String,
782    pub uv: String,
783    pub ir_high_title: String,
784    pub ir_title: String,
785    pub uv_high_title: String,
786    pub uv_title: String,
787    pub rg: String,
788    pub duv: String,
789}
790
791#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
792pub struct UiActions {
793    pub load: String,
794    pub save: String,
795    pub export: String,
796    pub import: String,
797    pub clear: String,
798    pub reset: String,
799    pub apply: String,
800    pub cancel: String,
801    pub close: String,
802    pub download: String,
803}
804
805#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
806pub struct UiFile {
807    pub open: String,
808    pub save_as: String,
809    pub export_ldt: String,
810    pub export_ies: String,
811    pub export_atla: String,
812    pub export_svg: String,
813    pub export_pdf: String,
814}
815
816#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
817pub struct UiTheme {
818    pub light: String,
819    pub dark: String,
820    pub system: String,
821}
822
823#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
824pub struct UiLanguage {
825    pub select: String,
826}
827
828#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
829pub struct UiTemplate {
830    pub select: String,
831    pub downlight: String,
832    pub projector: String,
833    pub linear: String,
834    pub fluorescent: String,
835    pub road: String,
836    pub uplight: String,
837    pub atla_fluorescent_xml: String,
838    pub atla_fluorescent_json: String,
839    pub atla_grow_light_fs: String,
840    pub atla_grow_light_rb: String,
841    pub halogen: String,
842    pub incandescent: String,
843    pub heat_lamp: String,
844    pub uv_blacklight: String,
845}
846
847#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
848pub struct UiMessages {
849    pub loading: String,
850    pub saving: String,
851    pub error: String,
852    pub success: String,
853    pub file_loaded: String,
854    pub file_saved: String,
855    pub invalid_file: String,
856    pub no_file: String,
857}
858
859/// Compare panel translations
860#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
861pub struct UiCompare {
862    pub title: String,
863    pub drop_hint: String,
864    pub browse: String,
865    pub or: String,
866    pub select_template: String,
867    pub file_b: String,
868    pub file_b_label: String,
869    pub clear: String,
870    pub similarity: String,
871    pub export_pdf: String,
872    pub export_typ: String,
873    pub exporting: String,
874    pub file_a_c_plane: String,
875    pub file_b_c_plane: String,
876    pub link_sliders: String,
877    pub metric: String,
878    pub file_a: String,
879    pub delta: String,
880    pub percent: String,
881    pub empty_title: String,
882    pub empty_hint: String,
883}
884
885/// Comparison locale (metric names for photometric comparison)
886#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
887pub struct ComparisonLocale {
888    pub metrics: ComparisonMetricLocale,
889}
890
891/// Comparison metric name translations keyed by metric key.
892#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
893pub struct ComparisonMetricLocale {
894    pub total_lamp_flux: String,
895    pub calculated_flux: String,
896    pub lor: String,
897    pub dlor: String,
898    pub ulor: String,
899    pub lamp_efficacy: String,
900    pub luminaire_efficacy: String,
901    pub total_wattage: String,
902    pub beam_angle: String,
903    pub field_angle: String,
904    pub beam_angle_cie: String,
905    pub field_angle_cie: String,
906    pub upward_beam_angle: String,
907    pub upward_field_angle: String,
908    pub max_intensity: String,
909    pub min_intensity: String,
910    pub avg_intensity: String,
911    pub spacing_c0: String,
912    pub spacing_c90: String,
913    pub zonal_0_30: String,
914    pub zonal_30_60: String,
915    pub zonal_60_90: String,
916    pub zonal_90_120: String,
917    pub zonal_120_150: String,
918    pub zonal_150_180: String,
919    pub cie_n1: String,
920    pub cie_n2: String,
921    pub cie_n3: String,
922    pub cie_n4: String,
923    pub cie_n5: String,
924    pub bug_b: String,
925    pub bug_u: String,
926    pub bug_g: String,
927    pub length: String,
928    pub width: String,
929    pub height: String,
930}
931
932/// Report translations
933#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
934pub struct ReportLocale {
935    pub title: String,
936    pub generated: String,
937    pub page: String,
938    pub of: String,
939    pub summary: String,
940    pub details: String,
941    pub appendix: String,
942}
943
944// Embedded locale JSON files
945const EN_JSON: &str = include_str!("../locales/en.json");
946const DE_JSON: &str = include_str!("../locales/de.json");
947const ZH_JSON: &str = include_str!("../locales/zh.json");
948const FR_JSON: &str = include_str!("../locales/fr.json");
949const IT_JSON: &str = include_str!("../locales/it.json");
950const RU_JSON: &str = include_str!("../locales/ru.json");
951const ES_JSON: &str = include_str!("../locales/es.json");
952const PT_BR_JSON: &str = include_str!("../locales/pt-BR.json");
953
954impl Locale {
955    /// Parse locale from JSON string
956    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
957        serde_json::from_str(json)
958    }
959
960    /// Get English locale (default)
961    pub fn english() -> Self {
962        Self::from_json(EN_JSON).expect("embedded English locale is valid")
963    }
964
965    /// Get German locale
966    pub fn german() -> Self {
967        Self::from_json(DE_JSON).expect("embedded German locale is valid")
968    }
969
970    /// Get Chinese locale
971    pub fn chinese() -> Self {
972        Self::from_json(ZH_JSON).expect("embedded Chinese locale is valid")
973    }
974
975    /// Get French locale
976    pub fn french() -> Self {
977        Self::from_json(FR_JSON).expect("embedded French locale is valid")
978    }
979
980    /// Get Italian locale
981    pub fn italian() -> Self {
982        Self::from_json(IT_JSON).expect("embedded Italian locale is valid")
983    }
984
985    /// Get Russian locale
986    pub fn russian() -> Self {
987        Self::from_json(RU_JSON).expect("embedded Russian locale is valid")
988    }
989
990    /// Get Spanish locale
991    pub fn spanish() -> Self {
992        Self::from_json(ES_JSON).expect("embedded Spanish locale is valid")
993    }
994
995    /// Get Portuguese (Brazil) locale
996    pub fn portuguese_brazil() -> Self {
997        Self::from_json(PT_BR_JSON).expect("embedded Portuguese locale is valid")
998    }
999
1000    /// Get locale for a specific language
1001    pub fn for_language(lang: Language) -> Self {
1002        match lang {
1003            Language::English => Self::english(),
1004            Language::German => Self::german(),
1005            Language::Chinese => Self::chinese(),
1006            Language::French => Self::french(),
1007            Language::Italian => Self::italian(),
1008            Language::Russian => Self::russian(),
1009            Language::Spanish => Self::spanish(),
1010            Language::PortugueseBrazil => Self::portuguese_brazil(),
1011        }
1012    }
1013
1014    /// Get locale by ISO 639-1 code
1015    pub fn for_code(code: &str) -> Self {
1016        Self::for_language(Language::from_code(code))
1017    }
1018
1019    /// Look up a validation message template by code (e.g. "W001", "E001").
1020    /// Returns the template string which may contain `{0}`, `{1}`, … placeholders.
1021    pub fn validation_message(&self, code: &str) -> Option<&str> {
1022        let m = &self.validation.messages;
1023        let s = match code {
1024            "W001" => &m.w001,
1025            "W002" => &m.w002,
1026            "W003" => &m.w003,
1027            "W004" => &m.w004,
1028            "W005" => &m.w005,
1029            "W006" => &m.w006,
1030            "W007" => &m.w007,
1031            "W008" => &m.w008,
1032            "W009" => &m.w009,
1033            "W010" => &m.w010,
1034            "W011" => &m.w011,
1035            "W012" => &m.w012,
1036            "W013" => &m.w013,
1037            "W014" => &m.w014,
1038            "W015" => &m.w015,
1039            "W016" => &m.w016,
1040            "W017" => &m.w017,
1041            "W018" => &m.w018,
1042            "W019" => &m.w019,
1043            "W020" => &m.w020,
1044            "W021" => &m.w021,
1045            "W022" => &m.w022,
1046            "W023" => &m.w023,
1047            "W024" => &m.w024,
1048            "W025" => &m.w025,
1049            "W026" => &m.w026,
1050            "W027" => &m.w027,
1051            "W028" => &m.w028,
1052            "W029" => &m.w029,
1053            "W030" => &m.w030,
1054            "W031" => &m.w031,
1055            "W032" => &m.w032,
1056            "W033" => &m.w033,
1057            "W034" => &m.w034,
1058            "W035" => &m.w035,
1059            "W036" => &m.w036,
1060            "W037" => &m.w037,
1061            "W038" => &m.w038,
1062            "W039" => &m.w039,
1063            "W040" => &m.w040,
1064            "W041" => &m.w041,
1065            "W042" => &m.w042,
1066            "W043" => &m.w043,
1067            "W044" => &m.w044,
1068            "W045" => &m.w045,
1069            "W046" => &m.w046,
1070            "W047" => &m.w047,
1071            "E001" => &m.e001,
1072            "E002" => &m.e002,
1073            "E003" => &m.e003,
1074            "E004" => &m.e004,
1075            "E005" => &m.e005,
1076            "E006" => &m.e006,
1077            _ => return None,
1078        };
1079        Some(s.as_str())
1080    }
1081
1082    /// Look up a comparison metric name by key (e.g. "total_lamp_flux").
1083    pub fn comparison_metric_name(&self, key: &str) -> Option<&str> {
1084        let m = &self.comparison.metrics;
1085        let s = match key {
1086            "total_lamp_flux" => &m.total_lamp_flux,
1087            "calculated_flux" => &m.calculated_flux,
1088            "lor" => &m.lor,
1089            "dlor" => &m.dlor,
1090            "ulor" => &m.ulor,
1091            "lamp_efficacy" => &m.lamp_efficacy,
1092            "luminaire_efficacy" => &m.luminaire_efficacy,
1093            "total_wattage" => &m.total_wattage,
1094            "beam_angle" => &m.beam_angle,
1095            "field_angle" => &m.field_angle,
1096            "beam_angle_cie" => &m.beam_angle_cie,
1097            "field_angle_cie" => &m.field_angle_cie,
1098            "upward_beam_angle" => &m.upward_beam_angle,
1099            "upward_field_angle" => &m.upward_field_angle,
1100            "max_intensity" => &m.max_intensity,
1101            "min_intensity" => &m.min_intensity,
1102            "avg_intensity" => &m.avg_intensity,
1103            "spacing_c0" => &m.spacing_c0,
1104            "spacing_c90" => &m.spacing_c90,
1105            "zonal_0_30" => &m.zonal_0_30,
1106            "zonal_30_60" => &m.zonal_30_60,
1107            "zonal_60_90" => &m.zonal_60_90,
1108            "zonal_90_120" => &m.zonal_90_120,
1109            "zonal_120_150" => &m.zonal_120_150,
1110            "zonal_150_180" => &m.zonal_150_180,
1111            "cie_n1" => &m.cie_n1,
1112            "cie_n2" => &m.cie_n2,
1113            "cie_n3" => &m.cie_n3,
1114            "cie_n4" => &m.cie_n4,
1115            "cie_n5" => &m.cie_n5,
1116            "bug_b" => &m.bug_b,
1117            "bug_u" => &m.bug_u,
1118            "bug_g" => &m.bug_g,
1119            "length" => &m.length,
1120            "width" => &m.width,
1121            "height" => &m.height,
1122            _ => return None,
1123        };
1124        Some(s.as_str())
1125    }
1126}
1127
1128/// Replace `{0}`, `{1}`, … placeholders in a template string with provided args.
1129pub fn format_template(template: &str, args: &[&dyn std::fmt::Display]) -> String {
1130    let mut result = template.to_string();
1131    for (i, arg) in args.iter().enumerate() {
1132        result = result.replace(&format!("{{{}}}", i), &arg.to_string());
1133    }
1134    result
1135}
1136
1137impl Default for Locale {
1138    fn default() -> Self {
1139        Self::english()
1140    }
1141}
1142
1143#[cfg(test)]
1144mod tests {
1145    use super::*;
1146
1147    #[test]
1148    fn test_english_locale() {
1149        let en = Locale::english();
1150        assert_eq!(en.meta.code, "en");
1151        assert_eq!(en.diagram.axis.gamma, "Gamma (γ)");
1152        assert_eq!(en.ui.tabs.polar, "Polar");
1153    }
1154
1155    #[test]
1156    fn test_german_locale() {
1157        let de = Locale::german();
1158        assert_eq!(de.meta.code, "de");
1159        assert_eq!(de.diagram.axis.intensity, "Lichtstärke (cd/klm)");
1160        assert_eq!(de.ui.tabs.polar, "Polar");
1161    }
1162
1163    #[test]
1164    fn test_chinese_locale() {
1165        let zh = Locale::chinese();
1166        assert_eq!(zh.meta.code, "zh");
1167        assert_eq!(zh.diagram.placeholder.no_data, "无数据");
1168        assert_eq!(zh.ui.tabs.polar, "极坐标");
1169    }
1170
1171    #[test]
1172    fn test_russian_locale() {
1173        let ru = Locale::russian();
1174        assert_eq!(ru.meta.code, "ru");
1175        assert_eq!(ru.diagram.placeholder.no_data, "Нет данных");
1176        assert_eq!(ru.luminaire.photometric.cct, "Цветовая температура");
1177    }
1178
1179    #[test]
1180    fn test_spanish_locale() {
1181        let es = Locale::spanish();
1182        assert_eq!(es.meta.code, "es");
1183        assert_eq!(es.diagram.placeholder.no_data, "Sin datos");
1184        assert_eq!(es.ui.actions.save, "Guardar");
1185    }
1186
1187    #[test]
1188    fn test_portuguese_brazil_locale() {
1189        let pt = Locale::portuguese_brazil();
1190        assert_eq!(pt.meta.code, "pt-BR");
1191        assert_eq!(pt.diagram.placeholder.no_data, "Sem dados");
1192        assert_eq!(pt.ui.actions.save, "Salvar");
1193    }
1194
1195    #[test]
1196    fn test_french_locale() {
1197        let fr = Locale::french();
1198        assert_eq!(fr.meta.code, "fr");
1199        assert_eq!(fr.diagram.placeholder.no_data, "Aucune donnée");
1200        assert_eq!(fr.ui.actions.save, "Enregistrer");
1201    }
1202
1203    #[test]
1204    fn test_italian_locale() {
1205        let it = Locale::italian();
1206        assert_eq!(it.meta.code, "it");
1207        assert_eq!(it.diagram.placeholder.no_data, "Nessun dato");
1208        assert_eq!(it.ui.actions.save, "Salva");
1209    }
1210
1211    #[test]
1212    fn test_language_from_code() {
1213        assert_eq!(Language::from_code("de"), Language::German);
1214        assert_eq!(Language::from_code("zh-CN"), Language::Chinese);
1215        assert_eq!(Language::from_code("fr"), Language::French);
1216        assert_eq!(Language::from_code("it"), Language::Italian);
1217        assert_eq!(Language::from_code("ru"), Language::Russian);
1218        assert_eq!(Language::from_code("es"), Language::Spanish);
1219        assert_eq!(Language::from_code("pt-BR"), Language::PortugueseBrazil);
1220        assert_eq!(Language::from_code("unknown"), Language::English);
1221    }
1222
1223    #[test]
1224    fn test_for_code() {
1225        let locale = Locale::for_code("de");
1226        assert_eq!(locale.meta.code, "de");
1227
1228        let locale_ru = Locale::for_code("ru");
1229        assert_eq!(locale_ru.meta.code, "ru");
1230    }
1231
1232    #[test]
1233    fn test_all_languages() {
1234        let all = Language::all();
1235        assert_eq!(all.len(), 8);
1236
1237        // Verify all languages can load their locale
1238        for lang in all {
1239            let locale = Locale::for_language(*lang);
1240            assert_eq!(locale.meta.code, lang.code());
1241        }
1242    }
1243}