1use std::collections::HashMap;
6use std::fmt;
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub struct Locale {
12 pub language: String, pub region: Option<String>, pub script: Option<String>, }
16
17impl Locale {
18 pub fn new(language: &str) -> Self {
19 Self {
20 language: language.to_string(),
21 region: None,
22 script: None,
23 }
24 }
25
26 pub fn with_region(language: &str, region: &str) -> Self {
27 Self {
28 language: language.to_string(),
29 region: Some(region.to_string()),
30 script: None,
31 }
32 }
33
34 pub fn with_script(language: &str, script: &str) -> Self {
35 Self {
36 language: language.to_string(),
37 region: None,
38 script: Some(script.to_string()),
39 }
40 }
41
42 pub fn tag(&self) -> String {
43 let mut s = self.language.clone();
44 if let Some(sc) = &self.script {
45 s.push('-');
46 s.push_str(sc);
47 }
48 if let Some(r) = &self.region {
49 s.push('-');
50 s.push_str(r);
51 }
52 s
53 }
54
55 pub fn is_rtl(&self) -> bool {
56 matches!(self.language.as_str(), "ar" | "he" | "fa" | "ur" | "ps" | "yi" | "dv")
57 }
58
59 pub fn decimal_separator(&self) -> char {
60 match self.language.as_str() {
61 "de" | "fr" | "es" | "it" | "pt" | "nl" | "pl" | "ru" | "tr" => ',',
62 _ => '.',
63 }
64 }
65
66 pub fn thousands_separator(&self) -> char {
67 match self.language.as_str() {
68 "de" | "nl" => '.',
69 "fr" | "ru" => '\u{00A0}',
70 _ => ',',
71 }
72 }
73
74 pub fn date_format(&self) -> &'static str {
75 match self.language.as_str() {
76 "en" if self.region.as_deref() == Some("US") => "MM/DD/YYYY",
77 "en" => "DD/MM/YYYY",
78 "de" | "nl" | "pl" | "ru" => "DD.MM.YYYY",
79 "ja" | "zh" | "ko" => "YYYY/MM/DD",
80 "fr" | "es" | "it" | "pt" => "DD/MM/YYYY",
81 _ => "YYYY-MM-DD",
82 }
83 }
84
85 pub fn number_format(&self, value: f64, decimals: usize) -> String {
86 let dec = self.decimal_separator();
87 let thou = self.thousands_separator();
88 let multiplier = 10f64.powi(decimals as i32);
89 let rounded = (value * multiplier).round() / multiplier;
90 let int_part = rounded.abs().trunc() as u64;
91 let frac_part = ((rounded.abs() - rounded.abs().trunc()) * multiplier).round() as u64;
92 let mut int_str = int_part.to_string();
93 let mut result = String::new();
95 let int_chars: Vec<char> = int_str.chars().collect();
96 for (i, &ch) in int_chars.iter().enumerate() {
97 if i > 0 && (int_chars.len() - i) % 3 == 0 {
98 result.push(thou);
99 }
100 result.push(ch);
101 }
102 let _ = int_str;
103 if value < 0.0 { result.insert(0, '-'); }
104 if decimals > 0 {
105 result.push(dec);
106 result.push_str(&format!("{:0>width$}", frac_part, width = decimals));
107 }
108 result
109 }
110}
111
112impl fmt::Display for Locale {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 write!(f, "{}", self.tag())
115 }
116}
117
118impl Default for Locale {
119 fn default() -> Self { Self::new("en") }
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
125pub enum PluralForm {
126 Zero,
127 One,
128 Two,
129 Few,
130 Many,
131 Other,
132}
133
134pub trait PluralRule {
135 fn form(&self, n: u64) -> PluralForm;
136}
137
138pub struct EnglishPlural;
139impl PluralRule for EnglishPlural {
140 fn form(&self, n: u64) -> PluralForm {
141 if n == 1 { PluralForm::One } else { PluralForm::Other }
142 }
143}
144
145pub struct RussianPlural;
146impl PluralRule for RussianPlural {
147 fn form(&self, n: u64) -> PluralForm {
148 let rem100 = n % 100;
149 let rem10 = n % 10;
150 if rem100 >= 11 && rem100 <= 14 { return PluralForm::Many; }
151 match rem10 {
152 1 => PluralForm::One,
153 2 | 3 | 4 => PluralForm::Few,
154 _ => PluralForm::Many,
155 }
156 }
157}
158
159pub struct ArabicPlural;
160impl PluralRule for ArabicPlural {
161 fn form(&self, n: u64) -> PluralForm {
162 if n == 0 { return PluralForm::Zero; }
163 if n == 1 { return PluralForm::One; }
164 if n == 2 { return PluralForm::Two; }
165 let rem100 = n % 100;
166 if rem100 >= 3 && rem100 <= 10 { return PluralForm::Few; }
167 if rem100 >= 11 && rem100 <= 99 { return PluralForm::Many; }
168 PluralForm::Other
169 }
170}
171
172pub struct JapanesePlural;
173impl PluralRule for JapanesePlural {
174 fn form(&self, _n: u64) -> PluralForm { PluralForm::Other }
175}
176
177#[derive(Debug, Clone)]
180pub enum TranslationValue {
181 Simple(String),
182 Plural {
183 zero: Option<String>,
184 one: Option<String>,
185 two: Option<String>,
186 few: Option<String>,
187 many: Option<String>,
188 other: String,
189 },
190 Ordinal { forms: HashMap<u64, String>, other: String },
191 SelectGender { masculine: String, feminine: String, neuter: Option<String> },
192}
193
194impl TranslationValue {
195 pub fn simple(s: &str) -> Self {
196 Self::Simple(s.to_string())
197 }
198
199 pub fn plural_en(one: &str, other: &str) -> Self {
200 Self::Plural {
201 zero: None,
202 one: Some(one.to_string()),
203 two: None,
204 few: None,
205 many: None,
206 other: other.to_string(),
207 }
208 }
209
210 pub fn get(&self, n: Option<u64>) -> &str {
211 match self {
212 Self::Simple(s) => s,
213 Self::Plural { zero, one, two, few, many, other } => {
214 match n {
215 Some(0) => zero.as_deref().unwrap_or(other),
216 Some(1) => one.as_deref().unwrap_or(other),
217 Some(2) => two.as_deref().unwrap_or(other),
218 _ => match n.map(|v| v % 10) {
219 Some(3..=4) => few.as_deref().unwrap_or(other),
220 _ => many.as_deref().unwrap_or(other),
221 }
222 }
223 }
224 Self::Ordinal { forms, other } => {
225 n.and_then(|v| forms.get(&v)).map(|s| s.as_str()).unwrap_or(other)
226 }
227 Self::SelectGender { masculine, .. } => masculine,
228 }
229 }
230}
231
232#[derive(Debug, Clone)]
235pub struct TranslationBundle {
236 pub locale: Locale,
237 pub namespace: String,
238 pub entries: HashMap<String, TranslationValue>,
239 pub fallback: Option<String>,
240 pub author: String,
241 pub version: String,
242}
243
244impl TranslationBundle {
245 pub fn new(locale: Locale, namespace: &str) -> Self {
246 Self {
247 locale,
248 namespace: namespace.to_string(),
249 entries: HashMap::new(),
250 fallback: None,
251 author: String::new(),
252 version: "1.0".into(),
253 }
254 }
255
256 pub fn insert(&mut self, key: &str, value: TranslationValue) {
257 self.entries.insert(key.to_string(), value);
258 }
259
260 pub fn insert_simple(&mut self, key: &str, text: &str) {
261 self.entries.insert(key.to_string(), TranslationValue::simple(text));
262 }
263
264 pub fn get(&self, key: &str) -> Option<&TranslationValue> {
265 self.entries.get(key)
266 }
267
268 pub fn entry_count(&self) -> usize {
269 self.entries.len()
270 }
271}
272
273#[derive(Debug, Clone)]
277pub struct TranslationArgs {
278 args: HashMap<String, String>,
279}
280
281impl TranslationArgs {
282 pub fn new() -> Self {
283 Self { args: HashMap::new() }
284 }
285
286 pub fn set(mut self, key: &str, value: impl fmt::Display) -> Self {
287 self.args.insert(key.to_string(), value.to_string());
288 self
289 }
290
291 pub fn set_mut(&mut self, key: &str, value: impl fmt::Display) {
292 self.args.insert(key.to_string(), value.to_string());
293 }
294
295 pub fn interpolate(&self, template: &str) -> String {
298 let mut result = String::with_capacity(template.len() + 32);
299 let mut chars = template.chars().peekable();
300 while let Some(ch) = chars.next() {
301 if ch == '{' {
302 let mut key = String::new();
303 for inner in chars.by_ref() {
304 if inner == '}' { break; }
305 key.push(inner);
306 }
307 let key = key.trim();
308 if let Some(val) = self.args.get(key) {
309 result.push_str(val);
310 } else {
311 result.push('{');
312 result.push_str(key);
313 result.push('}');
314 }
315 } else {
316 result.push(ch);
317 }
318 }
319 result
320 }
321}
322
323impl Default for TranslationArgs {
324 fn default() -> Self { Self::new() }
325}
326
327#[derive(Debug, Clone)]
330pub struct LocalizationManager {
331 pub current_locale: Locale,
332 pub fallback_locale: Locale,
333 bundles: HashMap<(String, String), TranslationBundle>,
334 pub missing_key_policy: MissingKeyPolicy,
335 pub debug_mode: bool,
336 missing_keys: Vec<String>,
337}
338
339#[derive(Debug, Clone, Copy, PartialEq, Eq)]
340pub enum MissingKeyPolicy {
341 ReturnKey,
343 Empty,
345 Panic,
347 Decorated,
349}
350
351impl LocalizationManager {
352 pub fn new(locale: Locale) -> Self {
353 Self {
354 current_locale: locale.clone(),
355 fallback_locale: Locale::new("en"),
356 bundles: HashMap::new(),
357 missing_key_policy: MissingKeyPolicy::Decorated,
358 debug_mode: false,
359 missing_keys: Vec::new(),
360 }
361 }
362
363 pub fn load_bundle(&mut self, bundle: TranslationBundle) {
364 let key = (bundle.locale.tag(), bundle.namespace.clone());
365 self.bundles.insert(key, bundle);
366 }
367
368 pub fn set_locale(&mut self, locale: Locale) {
369 self.current_locale = locale;
370 }
371
372 pub fn translate(&mut self, namespace: &str, key: &str) -> String {
373 self.translate_n(namespace, key, None, None)
374 }
375
376 pub fn translate_args(&mut self, namespace: &str, key: &str, args: &TranslationArgs) -> String {
377 let base = self.translate_n(namespace, key, None, None);
378 args.interpolate(&base)
379 }
380
381 pub fn translate_n(&mut self, namespace: &str, key: &str, n: Option<u64>, args: Option<&TranslationArgs>) -> String {
382 let val = self.lookup(namespace, key, &self.current_locale.clone(), n);
384 if let Some(s) = val {
385 return if let Some(a) = args { a.interpolate(&s) } else { s };
386 }
387 let val = self.lookup(namespace, key, &self.fallback_locale.clone(), n);
389 if let Some(s) = val {
390 return if let Some(a) = args { a.interpolate(&s) } else { s };
391 }
392 self.missing_keys.push(format!("{}/{}", namespace, key));
394 self.missing_string(key)
395 }
396
397 fn lookup(&self, namespace: &str, key: &str, locale: &Locale, n: Option<u64>) -> Option<String> {
398 let bkey = (locale.tag(), namespace.to_string());
399 let bundle = self.bundles.get(&bkey)?;
400 let value = bundle.get(key)?;
401 Some(value.get(n).to_string())
402 }
403
404 fn missing_string(&self, key: &str) -> String {
405 match self.missing_key_policy {
406 MissingKeyPolicy::ReturnKey => key.to_string(),
407 MissingKeyPolicy::Empty => String::new(),
408 MissingKeyPolicy::Decorated => format!("[{}]", key),
409 MissingKeyPolicy::Panic => {
410 #[cfg(debug_assertions)]
411 panic!("Missing translation key: {}", key);
412 #[cfg(not(debug_assertions))]
413 key.to_string()
414 }
415 }
416 }
417
418 pub fn missing_keys(&self) -> &[String] { &self.missing_keys }
419 pub fn clear_missing_keys(&mut self) { self.missing_keys.clear(); }
420
421 pub fn available_locales(&self) -> Vec<Locale> {
422 self.bundles.keys()
423 .map(|(tag, _)| tag)
424 .collect::<std::collections::HashSet<_>>()
425 .into_iter()
426 .map(|tag| {
427 let parts: Vec<&str> = tag.splitn(3, '-').collect();
428 match parts.len() {
429 1 => Locale::new(parts[0]),
430 2 => Locale::with_region(parts[0], parts[1]),
431 3 => {
432 let mut l = Locale::with_script(parts[0], parts[1]);
433 l.region = Some(parts[2].to_string());
434 l
435 }
436 _ => Locale::new(parts[0]),
437 }
438 })
439 .collect()
440 }
441
442 pub fn coverage_report(&self, namespace: &str) -> LocaleCoverageReport {
443 let en_key = (Locale::new("en").tag(), namespace.to_string());
444 let total = self.bundles.get(&en_key)
445 .map(|b| b.entry_count())
446 .unwrap_or(0);
447
448 let mut reports = Vec::new();
449 for ((locale_tag, ns), bundle) in &self.bundles {
450 if ns != namespace || *locale_tag == "en" { continue; }
451 let count = bundle.entry_count();
452 let pct = if total > 0 { count * 100 / total } else { 0 };
453 reports.push(LocaleReport {
454 locale: locale_tag.clone(),
455 translated: count,
456 total,
457 percent: pct,
458 });
459 }
460 reports.sort_by(|a, b| b.percent.cmp(&a.percent));
461 LocaleCoverageReport { namespace: namespace.to_string(), reports }
462 }
463}
464
465#[derive(Debug, Clone)]
466pub struct LocaleReport {
467 pub locale: String,
468 pub translated: usize,
469 pub total: usize,
470 pub percent: usize,
471}
472
473impl fmt::Display for LocaleReport {
474 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
475 write!(f, "{}: {}/{} ({}%)", self.locale, self.translated, self.total, self.percent)
476 }
477}
478
479#[derive(Debug, Clone)]
480pub struct LocaleCoverageReport {
481 pub namespace: String,
482 pub reports: Vec<LocaleReport>,
483}
484
485impl fmt::Display for LocaleCoverageReport {
486 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
487 writeln!(f, "Coverage report for namespace '{}':", self.namespace)?;
488 for r in &self.reports { writeln!(f, " {}", r)?; }
489 Ok(())
490 }
491}
492
493pub fn build_english_bundle() -> TranslationBundle {
496 let mut b = TranslationBundle::new(Locale::new("en"), "editor");
497 b.insert_simple("menu.file", "File");
498 b.insert_simple("menu.edit", "Edit");
499 b.insert_simple("menu.view", "View");
500 b.insert_simple("menu.window", "Window");
501 b.insert_simple("menu.help", "Help");
502 b.insert_simple("menu.file.new", "New Scene");
503 b.insert_simple("menu.file.open", "Open Scene...");
504 b.insert_simple("menu.file.save", "Save Scene");
505 b.insert_simple("menu.file.save_as", "Save Scene As...");
506 b.insert_simple("menu.file.export", "Export...");
507 b.insert_simple("menu.file.quit", "Quit");
508 b.insert_simple("menu.edit.undo", "Undo");
509 b.insert_simple("menu.edit.redo", "Redo");
510 b.insert_simple("menu.edit.cut", "Cut");
511 b.insert_simple("menu.edit.copy", "Copy");
512 b.insert_simple("menu.edit.paste", "Paste");
513 b.insert_simple("menu.edit.delete", "Delete");
514 b.insert_simple("menu.edit.select_all", "Select All");
515 b.insert_simple("menu.edit.deselect", "Deselect");
516 b.insert_simple("menu.edit.preferences", "Preferences...");
517 b.insert_simple("panel.hierarchy", "Hierarchy");
518 b.insert_simple("panel.inspector", "Inspector");
519 b.insert_simple("panel.scene", "Scene");
520 b.insert_simple("panel.assets", "Assets");
521 b.insert_simple("panel.console", "Console");
522 b.insert_simple("panel.timeline", "Timeline");
523 b.insert_simple("panel.kit_params", "Kit Parameters");
524 b.insert_simple("panel.shader_graph", "Shader Graph");
525 b.insert_simple("panel.perf", "Performance");
526 b.insert_simple("action.play", "Play");
527 b.insert_simple("action.pause", "Pause");
528 b.insert_simple("action.stop", "Stop");
529 b.insert_simple("action.step", "Step Frame");
530 b.insert_simple("gizmo.translate", "Translate");
531 b.insert_simple("gizmo.rotate", "Rotate");
532 b.insert_simple("gizmo.scale", "Scale");
533 b.insert_simple("gizmo.universal", "Universal");
534 b.insert_simple("gizmo.space.local", "Local");
535 b.insert_simple("gizmo.space.world", "World");
536 b.insert_simple("view.wireframe", "Wireframe");
537 b.insert_simple("view.lit", "Lit");
538 b.insert_simple("view.unlit", "Unlit");
539 b.insert_simple("view.normals", "Normals");
540 b.insert_simple("toolbar.new_entity", "New Entity");
541 b.insert_simple("toolbar.delete_selected", "Delete Selected");
542 b.insert_simple("toolbar.frame_selected", "Frame Selected");
543 b.insert_simple("toolbar.frame_all", "Frame All");
544 b.insert_simple("toolbar.snap_to_grid", "Snap to Grid");
545 b.insert_simple("toolbar.show_grid", "Show Grid");
546 b.insert_simple("toolbar.show_axes", "Show Axes");
547 b.insert_simple("toolbar.local_coords", "Local Coordinates");
548 b.insert("entity.count", TranslationValue::plural_en("{count} entity", "{count} entities"));
549 b.insert("keyframe.count", TranslationValue::plural_en("{count} keyframe", "{count} keyframes"));
550 b.insert("bone.count", TranslationValue::plural_en("{count} bone", "{count} bones"));
551 b.insert("particle.count", TranslationValue::plural_en("{count} particle", "{count} particles"));
552 b.insert("error.count", TranslationValue::plural_en("{count} error", "{count} errors"));
553 b.insert("warning.count", TranslationValue::plural_en("{count} warning", "{count} warnings"));
554 b.insert_simple("status.saved", "Scene saved");
555 b.insert_simple("status.save_failed", "Failed to save scene: {error}");
556 b.insert_simple("status.loaded", "Scene loaded: {name}");
557 b.insert_simple("status.compile_ok", "Shader compiled successfully");
558 b.insert_simple("status.compile_failed", "Shader compilation failed: {count} errors");
559 b.insert_simple("status.unsaved_changes", "Unsaved changes");
560 b.insert_simple("dialog.confirm_delete", "Delete {count} selected object(s)?");
561 b.insert_simple("dialog.unsaved_changes", "Scene has unsaved changes. Save before closing?");
562 b.insert_simple("dialog.save", "Save");
563 b.insert_simple("dialog.discard", "Don't Save");
564 b.insert_simple("dialog.cancel", "Cancel");
565 b.insert_simple("dialog.ok", "OK");
566 b.insert_simple("dialog.yes", "Yes");
567 b.insert_simple("dialog.no", "No");
568 b.insert_simple("inspector.transform", "Transform");
569 b.insert_simple("inspector.position", "Position");
570 b.insert_simple("inspector.rotation", "Rotation");
571 b.insert_simple("inspector.scale", "Scale");
572 b.insert_simple("inspector.visible", "Visible");
573 b.insert_simple("inspector.locked", "Locked");
574 b.insert_simple("inspector.static", "Static");
575 b.insert_simple("inspector.cast_shadows", "Cast Shadows");
576 b.insert_simple("inspector.receive_shadows", "Receive Shadows");
577 b.insert_simple("inspector.tag", "Tag");
578 b.insert_simple("inspector.layer", "Layer");
579 b.insert_simple("node.category.math", "Math");
580 b.insert_simple("node.category.vector", "Vector");
581 b.insert_simple("node.category.texture", "Texture");
582 b.insert_simple("node.category.color", "Color");
583 b.insert_simple("node.category.pbr", "PBR");
584 b.insert_simple("node.category.noise", "Noise");
585 b.insert_simple("node.category.input", "Input");
586 b.insert_simple("node.category.output", "Output");
587 b.insert_simple("node.category.sdf", "SDF");
588 b.insert_simple("shortcut.undo", "Ctrl+Z");
589 b.insert_simple("shortcut.redo", "Ctrl+Y");
590 b.insert_simple("shortcut.save", "Ctrl+S");
591 b.insert_simple("shortcut.open", "Ctrl+O");
592 b.insert_simple("shortcut.new", "Ctrl+N");
593 b.insert_simple("shortcut.delete", "Del");
594 b.insert_simple("shortcut.copy", "Ctrl+C");
595 b.insert_simple("shortcut.paste", "Ctrl+V");
596 b.insert_simple("shortcut.select_all", "Ctrl+A");
597 b.insert_simple("shortcut.frame_selected", "F");
598 b.insert_simple("shortcut.frame_all", "Shift+F");
599 b.insert_simple("shortcut.play", "Space");
600 b.insert_simple("shortcut.gizmo.translate", "G");
601 b.insert_simple("shortcut.gizmo.rotate", "R");
602 b.insert_simple("shortcut.gizmo.scale", "S");
603 b.insert_simple("tooltip.add_node", "Click to add this node to the graph");
604 b.insert_simple("tooltip.connect_ports", "Drag to connect ports");
605 b.insert_simple("tooltip.disconnect", "Right-click to disconnect");
606 b.insert_simple("tooltip.reset_value", "Double-click to reset to default");
607 b.insert_simple("tooltip.preview", "Toggle node preview");
608 b.insert_simple("tooltip.collapse", "Collapse node");
609 b.insert_simple("asset.kind.scene", "Scene");
610 b.insert_simple("asset.kind.mesh", "Mesh");
611 b.insert_simple("asset.kind.material", "Material");
612 b.insert_simple("asset.kind.texture", "Texture");
613 b.insert_simple("asset.kind.audio", "Audio");
614 b.insert_simple("asset.kind.script", "Script");
615 b.insert_simple("asset.kind.animation", "Animation");
616 b.insert_simple("asset.kind.shader", "Shader");
617 b.insert_simple("asset.kind.font", "Font");
618 b.insert_simple("asset.kind.config", "Config");
619 b.insert_simple("perf.fps", "FPS");
620 b.insert_simple("perf.frame_time", "Frame Time");
621 b.insert_simple("perf.gpu_time", "GPU Time");
622 b.insert_simple("perf.cpu_time", "CPU Time");
623 b.insert_simple("perf.draw_calls", "Draw Calls");
624 b.insert_simple("perf.triangles", "Triangles");
625 b.insert_simple("perf.memory_vram", "VRAM");
626 b.insert_simple("perf.memory_ram", "RAM");
627 b.insert_simple("perf.bottleneck", "Bottleneck");
628 b
629}
630
631pub fn build_french_bundle() -> TranslationBundle {
632 let mut b = TranslationBundle::new(Locale::new("fr"), "editor");
633 b.insert_simple("menu.file", "Fichier");
634 b.insert_simple("menu.edit", "Édition");
635 b.insert_simple("menu.view", "Affichage");
636 b.insert_simple("menu.window", "Fenêtre");
637 b.insert_simple("menu.help", "Aide");
638 b.insert_simple("menu.file.new", "Nouveau projet");
639 b.insert_simple("menu.file.open", "Ouvrir le projet...");
640 b.insert_simple("menu.file.save", "Enregistrer");
641 b.insert_simple("menu.file.save_as", "Enregistrer sous...");
642 b.insert_simple("menu.file.quit", "Quitter");
643 b.insert_simple("menu.edit.undo", "Annuler");
644 b.insert_simple("menu.edit.redo", "Rétablir");
645 b.insert_simple("menu.edit.cut", "Couper");
646 b.insert_simple("menu.edit.copy", "Copier");
647 b.insert_simple("menu.edit.paste", "Coller");
648 b.insert_simple("menu.edit.delete", "Supprimer");
649 b.insert_simple("menu.edit.select_all", "Tout sélectionner");
650 b.insert_simple("panel.hierarchy", "Hiérarchie");
651 b.insert_simple("panel.inspector", "Inspecteur");
652 b.insert_simple("panel.scene", "Scène");
653 b.insert_simple("panel.assets", "Ressources");
654 b.insert_simple("panel.console", "Console");
655 b.insert_simple("panel.timeline", "Chronologie");
656 b.insert_simple("action.play", "Jouer");
657 b.insert_simple("action.pause", "Pause");
658 b.insert_simple("action.stop", "Arrêter");
659 b.insert_simple("gizmo.translate", "Déplacer");
660 b.insert_simple("gizmo.rotate", "Pivoter");
661 b.insert_simple("gizmo.scale", "Redimensionner");
662 b.insert_simple("gizmo.space.local", "Local");
663 b.insert_simple("gizmo.space.world", "Monde");
664 b.insert_simple("dialog.save", "Enregistrer");
665 b.insert_simple("dialog.discard", "Ne pas enregistrer");
666 b.insert_simple("dialog.cancel", "Annuler");
667 b.insert_simple("dialog.ok", "OK");
668 b.insert_simple("inspector.transform", "Transformation");
669 b.insert_simple("inspector.position", "Position");
670 b.insert_simple("inspector.rotation", "Rotation");
671 b.insert_simple("inspector.scale", "Échelle");
672 b.insert_simple("inspector.visible", "Visible");
673 b.insert_simple("status.saved", "Scène enregistrée");
674 b.insert_simple("status.loaded", "Scène chargée : {name}");
675 b.insert_simple("shortcut.undo", "Ctrl+Z");
676 b.insert_simple("shortcut.redo", "Ctrl+Y");
677 b.insert_simple("shortcut.save", "Ctrl+S");
678 b.insert_simple("shortcut.frame_selected", "F");
679 b.insert_simple("shortcut.play", "Espace");
680 b
681}
682
683pub fn build_german_bundle() -> TranslationBundle {
684 let mut b = TranslationBundle::new(Locale::new("de"), "editor");
685 b.insert_simple("menu.file", "Datei");
686 b.insert_simple("menu.edit", "Bearbeiten");
687 b.insert_simple("menu.view", "Ansicht");
688 b.insert_simple("menu.window", "Fenster");
689 b.insert_simple("menu.help", "Hilfe");
690 b.insert_simple("menu.file.new", "Neue Szene");
691 b.insert_simple("menu.file.open", "Szene öffnen...");
692 b.insert_simple("menu.file.save", "Speichern");
693 b.insert_simple("menu.file.save_as", "Speichern als...");
694 b.insert_simple("menu.file.quit", "Beenden");
695 b.insert_simple("menu.edit.undo", "Rückgängig");
696 b.insert_simple("menu.edit.redo", "Wiederholen");
697 b.insert_simple("menu.edit.cut", "Ausschneiden");
698 b.insert_simple("menu.edit.copy", "Kopieren");
699 b.insert_simple("menu.edit.paste", "Einfügen");
700 b.insert_simple("menu.edit.delete", "Löschen");
701 b.insert_simple("menu.edit.select_all", "Alles auswählen");
702 b.insert_simple("panel.hierarchy", "Hierarchie");
703 b.insert_simple("panel.inspector", "Inspektor");
704 b.insert_simple("panel.scene", "Szene");
705 b.insert_simple("panel.assets", "Ressourcen");
706 b.insert_simple("panel.console", "Konsole");
707 b.insert_simple("panel.timeline", "Zeitachse");
708 b.insert_simple("action.play", "Abspielen");
709 b.insert_simple("action.pause", "Pause");
710 b.insert_simple("action.stop", "Stopp");
711 b.insert_simple("gizmo.translate", "Verschieben");
712 b.insert_simple("gizmo.rotate", "Drehen");
713 b.insert_simple("gizmo.scale", "Skalieren");
714 b.insert_simple("gizmo.space.local", "Lokal");
715 b.insert_simple("gizmo.space.world", "Welt");
716 b.insert_simple("dialog.save", "Speichern");
717 b.insert_simple("dialog.discard", "Nicht speichern");
718 b.insert_simple("dialog.cancel", "Abbrechen");
719 b.insert_simple("dialog.ok", "OK");
720 b.insert_simple("inspector.transform", "Transformation");
721 b.insert_simple("inspector.position", "Position");
722 b.insert_simple("inspector.rotation", "Rotation");
723 b.insert_simple("inspector.scale", "Skalierung");
724 b.insert_simple("status.saved", "Szene gespeichert");
725 b.insert_simple("status.loaded", "Szene geladen: {name}");
726 b
727}
728
729pub fn build_japanese_bundle() -> TranslationBundle {
730 let mut b = TranslationBundle::new(Locale::new("ja"), "editor");
731 b.insert_simple("menu.file", "ファイル");
732 b.insert_simple("menu.edit", "編集");
733 b.insert_simple("menu.view", "表示");
734 b.insert_simple("menu.window", "ウィンドウ");
735 b.insert_simple("menu.help", "ヘルプ");
736 b.insert_simple("menu.file.new", "新規シーン");
737 b.insert_simple("menu.file.open", "シーンを開く...");
738 b.insert_simple("menu.file.save", "保存");
739 b.insert_simple("menu.file.save_as", "名前を付けて保存...");
740 b.insert_simple("menu.file.quit", "終了");
741 b.insert_simple("menu.edit.undo", "元に戻す");
742 b.insert_simple("menu.edit.redo", "やり直す");
743 b.insert_simple("menu.edit.copy", "コピー");
744 b.insert_simple("menu.edit.paste", "貼り付け");
745 b.insert_simple("menu.edit.delete", "削除");
746 b.insert_simple("panel.hierarchy", "ヒエラルキー");
747 b.insert_simple("panel.inspector", "インスペクター");
748 b.insert_simple("panel.scene", "シーン");
749 b.insert_simple("panel.assets", "アセット");
750 b.insert_simple("panel.console", "コンソール");
751 b.insert_simple("panel.timeline", "タイムライン");
752 b.insert_simple("action.play", "再生");
753 b.insert_simple("action.pause", "一時停止");
754 b.insert_simple("action.stop", "停止");
755 b.insert_simple("gizmo.translate", "移動");
756 b.insert_simple("gizmo.rotate", "回転");
757 b.insert_simple("gizmo.scale", "スケール");
758 b.insert_simple("dialog.save", "保存");
759 b.insert_simple("dialog.discard", "保存しない");
760 b.insert_simple("dialog.cancel", "キャンセル");
761 b.insert_simple("dialog.ok", "OK");
762 b.insert_simple("inspector.transform", "トランスフォーム");
763 b.insert_simple("inspector.position", "位置");
764 b.insert_simple("inspector.rotation", "回転");
765 b.insert_simple("inspector.scale", "スケール");
766 b
767}
768
769pub fn build_spanish_bundle() -> TranslationBundle {
770 let mut b = TranslationBundle::new(Locale::new("es"), "editor");
771 b.insert_simple("menu.file", "Archivo");
772 b.insert_simple("menu.edit", "Editar");
773 b.insert_simple("menu.view", "Vista");
774 b.insert_simple("menu.window", "Ventana");
775 b.insert_simple("menu.help", "Ayuda");
776 b.insert_simple("menu.file.new", "Nueva escena");
777 b.insert_simple("menu.file.open", "Abrir escena...");
778 b.insert_simple("menu.file.save", "Guardar");
779 b.insert_simple("menu.file.save_as", "Guardar como...");
780 b.insert_simple("menu.file.quit", "Salir");
781 b.insert_simple("menu.edit.undo", "Deshacer");
782 b.insert_simple("menu.edit.redo", "Rehacer");
783 b.insert_simple("menu.edit.copy", "Copiar");
784 b.insert_simple("menu.edit.paste", "Pegar");
785 b.insert_simple("menu.edit.delete", "Eliminar");
786 b.insert_simple("panel.hierarchy", "Jerarquía");
787 b.insert_simple("panel.inspector", "Inspector");
788 b.insert_simple("panel.scene", "Escena");
789 b.insert_simple("panel.assets", "Recursos");
790 b.insert_simple("panel.console", "Consola");
791 b.insert_simple("panel.timeline", "Línea de tiempo");
792 b.insert_simple("action.play", "Reproducir");
793 b.insert_simple("action.pause", "Pausa");
794 b.insert_simple("action.stop", "Detener");
795 b.insert_simple("gizmo.translate", "Mover");
796 b.insert_simple("gizmo.rotate", "Rotar");
797 b.insert_simple("gizmo.scale", "Escalar");
798 b.insert_simple("dialog.save", "Guardar");
799 b.insert_simple("dialog.discard", "No guardar");
800 b.insert_simple("dialog.cancel", "Cancelar");
801 b.insert_simple("dialog.ok", "Aceptar");
802 b.insert_simple("inspector.transform", "Transformación");
803 b.insert_simple("inspector.position", "Posición");
804 b.insert_simple("inspector.rotation", "Rotación");
805 b.insert_simple("inspector.scale", "Escala");
806 b
807}
808
809pub fn build_chinese_simplified_bundle() -> TranslationBundle {
810 let mut b = TranslationBundle::new(
811 Locale::with_script("zh", "Hans"),
812 "editor",
813 );
814 b.insert_simple("menu.file", "文件");
815 b.insert_simple("menu.edit", "编辑");
816 b.insert_simple("menu.view", "视图");
817 b.insert_simple("menu.window", "窗口");
818 b.insert_simple("menu.help", "帮助");
819 b.insert_simple("menu.file.new", "新建场景");
820 b.insert_simple("menu.file.open", "打开场景...");
821 b.insert_simple("menu.file.save", "保存");
822 b.insert_simple("menu.file.save_as", "另存为...");
823 b.insert_simple("menu.file.quit", "退出");
824 b.insert_simple("menu.edit.undo", "撤销");
825 b.insert_simple("menu.edit.redo", "重做");
826 b.insert_simple("menu.edit.copy", "复制");
827 b.insert_simple("menu.edit.paste", "粘贴");
828 b.insert_simple("menu.edit.delete", "删除");
829 b.insert_simple("panel.hierarchy", "层级");
830 b.insert_simple("panel.inspector", "检查器");
831 b.insert_simple("panel.scene", "场景");
832 b.insert_simple("panel.assets", "资源");
833 b.insert_simple("panel.console", "控制台");
834 b.insert_simple("panel.timeline", "时间轴");
835 b.insert_simple("action.play", "播放");
836 b.insert_simple("action.pause", "暂停");
837 b.insert_simple("action.stop", "停止");
838 b.insert_simple("gizmo.translate", "移动");
839 b.insert_simple("gizmo.rotate", "旋转");
840 b.insert_simple("gizmo.scale", "缩放");
841 b.insert_simple("dialog.save", "保存");
842 b.insert_simple("dialog.discard", "不保存");
843 b.insert_simple("dialog.cancel", "取消");
844 b.insert_simple("dialog.ok", "确定");
845 b.insert_simple("inspector.transform", "变换");
846 b.insert_simple("inspector.position", "位置");
847 b.insert_simple("inspector.rotation", "旋转");
848 b.insert_simple("inspector.scale", "缩放");
849 b
850}
851
852pub fn default_localization() -> LocalizationManager {
854 let mut mgr = LocalizationManager::new(Locale::new("en"));
855 mgr.load_bundle(build_english_bundle());
856 mgr.load_bundle(build_french_bundle());
857 mgr.load_bundle(build_german_bundle());
858 mgr.load_bundle(build_japanese_bundle());
859 mgr.load_bundle(build_spanish_bundle());
860 mgr.load_bundle(build_chinese_simplified_bundle());
861 mgr
862}
863
864#[macro_export]
868macro_rules! t {
869 ($mgr:expr, $key:literal) => {{
870 $mgr.translate("editor", $key)
871 }};
872 ($mgr:expr, $ns:literal, $key:literal) => {{
873 $mgr.translate($ns, $key)
874 }};
875 ($mgr:expr, $key:literal, $($arg_k:literal = $arg_v:expr),* $(,)?) => {{
876 let args = $crate::editor::localization::TranslationArgs::new()
877 $(.set($arg_k, $arg_v))*;
878 $mgr.translate_args("editor", $key, &args)
879 }};
880}
881
882#[cfg(test)]
885mod tests {
886 use super::*;
887
888 #[test]
889 fn english_translate() {
890 let mut mgr = default_localization();
891 assert_eq!(mgr.translate("editor", "menu.file"), "File");
892 assert_eq!(mgr.translate("editor", "action.play"), "Play");
893 }
894
895 #[test]
896 fn french_translate() {
897 let mut mgr = default_localization();
898 mgr.set_locale(Locale::new("fr"));
899 assert_eq!(mgr.translate("editor", "menu.file"), "Fichier");
900 assert_eq!(mgr.translate("editor", "action.play"), "Jouer");
901 }
902
903 #[test]
904 fn fallback_to_english() {
905 let mut mgr = default_localization();
906 mgr.set_locale(Locale::new("fr"));
907 let s = mgr.translate("editor", "shortcut.frame_all");
909 assert_eq!(s, "Shift+F");
910 }
911
912 #[test]
913 fn missing_key_decorated() {
914 let mut mgr = default_localization();
915 let s = mgr.translate("editor", "nonexistent.key");
916 assert_eq!(s, "[nonexistent.key]");
917 }
918
919 #[test]
920 fn interpolation() {
921 let mut mgr = default_localization();
922 let args = TranslationArgs::new().set("name", "test_scene");
923 let s = mgr.translate_args("editor", "status.loaded", &args);
924 assert!(s.contains("test_scene"));
925 }
926
927 #[test]
928 fn locale_rtl() {
929 let arabic = Locale::new("ar");
930 assert!(arabic.is_rtl());
931 let english = Locale::new("en");
932 assert!(!english.is_rtl());
933 }
934
935 #[test]
936 fn number_format() {
937 let de = Locale::new("de");
938 let formatted = de.number_format(1234.56, 2);
939 assert!(formatted.contains(','), "DE should use comma decimal: {}", formatted);
940 }
941
942 #[test]
943 fn coverage_report() {
944 let mgr = default_localization();
945 let report = mgr.coverage_report("editor");
946 assert!(!report.reports.is_empty());
947 }
948}