diff options
| author | Rubén Beltrán del Río <jj@r.bdr.sh> | 2026-01-17 00:36:49 +0100 |
|---|---|---|
| committer | Rubén Beltrán del Río <jj@r.bdr.sh> | 2026-01-17 02:43:21 +0100 |
| commit | 17898fbabde35ab346c133114e78614e707c0eca (patch) | |
| tree | 90f0fd36211c036a507a0af092aac3c5fb8e7ac8 /src | |
| parent | 90b12c34debaa4aa816d4e6862cdb4e39c7181e4 (diff) | |
Add localization
Diffstat (limited to 'src')
| -rw-r--r-- | src/dialogs.rs | 42 | ||||
| -rw-r--r-- | src/handlers/export.rs | 8 | ||||
| -rw-r--r-- | src/handlers/file.rs | 5 | ||||
| -rw-r--r-- | src/i18n.rs | 61 | ||||
| -rw-r--r-- | src/main.rs | 134 | ||||
| -rw-r--r-- | src/preferences/pages/editor.rs | 13 | ||||
| -rw-r--r-- | src/preferences/pages/general.rs | 30 | ||||
| -rw-r--r-- | src/preferences/pages/map.rs | 17 | ||||
| -rw-r--r-- | src/preferences/pages/stages.rs | 22 | ||||
| -rw-r--r-- | src/preferences/pages/templates.rs | 77 | ||||
| -rw-r--r-- | src/preferences/window.rs | 15 | ||||
| -rw-r--r-- | src/stages.rs | 159 |
12 files changed, 374 insertions, 209 deletions
diff --git a/src/dialogs.rs b/src/dialogs.rs index f7fb9cf..8f5b951 100644 --- a/src/dialogs.rs +++ b/src/dialogs.rs @@ -25,13 +25,14 @@ use crate::AppModel; use crate::actions::{Action, ImageFormat}; use crate::constants; use crate::file_registry; +use crate::tr; /// Creates a file filter for .wmap files in file pickers. pub fn create_file_filter() -> gtk::FileFilter { let filter = gtk::FileFilter::new(); filter.add_pattern(&format!("*.{}", constants::FILE_EXTENSION)); filter.add_mime_type(constants::MIME_TYPE); - filter.set_name(Some("Wardley Map Files")); + filter.set_name(Some(&tr!("dialog.file_type.wmap"))); filter } @@ -58,12 +59,12 @@ pub fn show_open_dialog( sender: ComponentSender<AppModel>, ) { let dialog = gtk::FileChooserDialog::new( - Some("Open Map"), + Some(&tr!("dialog.open.title")), Some(window), gtk::FileChooserAction::Open, &[ - ("Cancel", gtk::ResponseType::Cancel), - ("Open", gtk::ResponseType::Accept), + (&tr!("dialog.action.cancel"), gtk::ResponseType::Cancel), + (&tr!("dialog.action.open"), gtk::ResponseType::Accept), ], ); dialog.add_filter(&create_file_filter()); @@ -98,26 +99,27 @@ pub fn show_save_dialog( sender: ComponentSender<AppModel>, ) { let dialog = gtk::FileChooserDialog::new( - Some("Save Map As"), + Some(&tr!("dialog.save.title")), Some(window), gtk::FileChooserAction::Save, &[ - ("Cancel", gtk::ResponseType::Cancel), - ("Save", gtk::ResponseType::Accept), + (&tr!("dialog.action.cancel"), gtk::ResponseType::Cancel), + (&tr!("dialog.action.save"), gtk::ResponseType::Accept), ], ); dialog.add_filter(&create_file_filter()); + let default_filename = "map.wmap"; if let Some(ref current_path) = current_file { if let Some(parent) = current_path.parent() { let folder = gtk::gio::File::for_path(parent); dialog.set_current_folder(Some(&folder)).ok(); } if let Some(filename) = current_path.file_name() { - dialog.set_current_name(filename.to_str().unwrap_or("map.wmap")); + dialog.set_current_name(filename.to_str().unwrap_or(default_filename)); } } else { - dialog.set_current_name("map.wmap"); + dialog.set_current_name(default_filename); } dialog.connect_response(move |dialog, response| { @@ -143,12 +145,12 @@ pub fn show_unsaved_changes_dialog( gtk::DialogFlags::MODAL | gtk::DialogFlags::DESTROY_WITH_PARENT, gtk::MessageType::Warning, gtk::ButtonsType::None, - "Save changes before closing?", + tr!("dialog.unsaved_changes.message"), ); - dialog.set_title(Some("Unsaved Changes")); - dialog.add_button("Don't Save", gtk::ResponseType::Reject); - dialog.add_button("Cancel", gtk::ResponseType::Cancel); - dialog.add_button("Save", gtk::ResponseType::Accept); + dialog.set_title(Some(&tr!("dialog.unsaved_changes.title"))); + dialog.add_button(&tr!("dialog.action.dont_save"), gtk::ResponseType::Reject); + dialog.add_button(&tr!("dialog.action.cancel"), gtk::ResponseType::Cancel); + dialog.add_button(&tr!("dialog.action.save"), gtk::ResponseType::Accept); let window = window.clone(); dialog.connect_response(move |dialog, response| { @@ -179,10 +181,10 @@ fn create_image_filter(format: ImageFormat) -> gtk::FileFilter { filter.add_pattern(&format!("*.{}", format.extension())); filter.add_mime_type(format.mime_type()); let name = match format { - ImageFormat::Png => "PNG Image", - ImageFormat::Svg => "SVG Image", + ImageFormat::Png => tr!("dialog.file_type.png"), + ImageFormat::Svg => tr!("dialog.file_type.svg"), }; - filter.set_name(Some(name)); + filter.set_name(Some(&name)); filter } @@ -193,12 +195,12 @@ pub fn show_export_dialog( sender: ComponentSender<AppModel>, ) { let dialog = gtk::FileChooserDialog::new( - Some("Export Map as Image"), + Some(&tr!("dialog.export.title")), Some(window), gtk::FileChooserAction::Save, &[ - ("Cancel", gtk::ResponseType::Cancel), - ("Export", gtk::ResponseType::Accept), + (&tr!("dialog.action.cancel"), gtk::ResponseType::Cancel), + (&tr!("dialog.action.export"), gtk::ResponseType::Accept), ], ); diff --git a/src/handlers/export.rs b/src/handlers/export.rs index 0b64a73..a0a4451 100644 --- a/src/handlers/export.rs +++ b/src/handlers/export.rs @@ -18,24 +18,26 @@ use std::path::PathBuf; use crate::AppModel; use crate::actions::ImageFormat; use crate::dialogs; +use crate::tr; +use crate::stages::LocalizedStageType; use wmap_renderer::{render_to_png, render_to_svg}; /// Exports the map to an image file in the specified format. pub fn export_to_file(model: &AppModel, path: PathBuf, format: ImageFormat) { let result: Result<(), String> = match format { ImageFormat::Png => { - render_to_png(&model.map, &model.stage_type, &model.render_configuration) + render_to_png(&model.map, &model.stage_type.to_localized(), &model.render_configuration) .map_err(|e| e.to_string()) .and_then(|data| std::fs::write(&path, data).map_err(|e| e.to_string())) } ImageFormat::Svg => { - render_to_svg(&model.map, &model.stage_type, &model.render_configuration) + render_to_svg(&model.map, &model.stage_type.to_localized(), &model.render_configuration) .map_err(|e| e.to_string()) .and_then(|data| std::fs::write(&path, data).map_err(|e| e.to_string())) } }; if let Err(error) = result { - dialogs::show_error(&model.window, "Error Exporting Image", &error); + dialogs::show_error(&model.window, &tr!("error.file.export"), &error); } } diff --git a/src/handlers/file.rs b/src/handlers/file.rs index 615a1f7..bd501e8 100644 --- a/src/handlers/file.rs +++ b/src/handlers/file.rs @@ -23,6 +23,7 @@ use crate::actions::Action; use crate::constants; use crate::dialogs; use crate::file_registry; +use crate::tr; /// Loads a file into the current document, updating the file registry. pub fn load_file(model: &mut AppModel, path: PathBuf) { @@ -35,7 +36,7 @@ pub fn load_file(model: &mut AppModel, path: PathBuf) { model.modified = false; } Err(error) => { - dialogs::show_error(&model.window, "Error Opening File", &error.to_string()); + dialogs::show_error(&model.window, &tr!("errors.file.open"), &error.to_string()); } } } @@ -58,7 +59,7 @@ pub fn save_to_file(model: &mut AppModel, path: PathBuf, close_after: bool) { } } Err(error) => { - dialogs::show_error(&model.window, "Error Saving File", &error.to_string()); + dialogs::show_error(&model.window, &tr!("errors.file.save"), &error.to_string()); } } } diff --git a/src/i18n.rs b/src/i18n.rs new file mode 100644 index 0000000..1c574e0 --- /dev/null +++ b/src/i18n.rs @@ -0,0 +1,61 @@ +// Map, wardley map editor for linux +// Copyright (C) 2026 Rubén Beltrán del Río + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +//! Internationalization support using gettext. + +use gettextrs::{bindtextdomain, setlocale, textdomain, LocaleCategory}; + +/// The gettext domain for the application. +pub const GETTEXT_DOMAIN: &str = "systems.tranquil.Map"; + +/// Initializes the localization system. +pub fn init() { + setlocale(LocaleCategory::LcAll, ""); + + // Try standard locations for locale files + let locale_dirs = [ + // Development: local po directory + concat!(env!("CARGO_MANIFEST_DIR"), "/po"), + // Flatpak + "/app/share/locale", + // System-wide installation + "/usr/share/locale", + "/usr/local/share/locale", + ]; + + for dir in &locale_dirs { + if std::path::Path::new(dir).exists() { + let _ = bindtextdomain(GETTEXT_DOMAIN, *dir); + break; + } + } + + textdomain(GETTEXT_DOMAIN).expect("Failed to set gettext domain"); +} + +/// Macro for translating strings at runtime. +/// +/// Usage: +/// ```ignore +/// use crate::i18n::tr; +/// let translated = tr!("Hello, world!"); +/// ``` +#[macro_export] +macro_rules! tr { + ($msgid:expr) => { + gettextrs::gettext($msgid) + }; +} diff --git a/src/main.rs b/src/main.rs index 60be618..c15ffc2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,7 @@ mod constants; mod dialogs; mod file_registry; mod handlers; +mod i18n; mod preferences; mod stages; mod ui_helpers; @@ -50,7 +51,7 @@ use relm4::{ }; use sourceview5::LanguageManager; use sourceview5::prelude::{BufferExt, TextBufferExt, TextViewExt}; -use stages::ALL_STAGE_TYPES; +use stages::{ALL_STAGE_TYPES, LocalizedStageType}; use wmap_parser::{Map, parse}; use wmap_renderer::{Configuration, StageType, render_to_surface}; @@ -71,6 +72,15 @@ relm4::new_stateless_action!(ZoomInAction, WindowActionGroup, "zoom-in"); relm4::new_stateless_action!(ZoomOutAction, WindowActionGroup, "zoom-out"); relm4::new_stateless_action!(PreferencesAction, WindowActionGroup, "preferences"); +struct NewFromTemplateAction; +impl relm4::actions::ActionName for NewFromTemplateAction { + type Group = WindowActionGroup; + type Target = String; + type State = (); + + const NAME: &'static str = "new-from-template"; +} + /// Macro to reduce boilerplate when registering actions with accelerators. macro_rules! register_action { ($group:expr, $app:expr, $sender:expr, $action_type:ty, $message:expr, $accelerators:expr) => {{ @@ -168,29 +178,12 @@ fn setup_actions(window: gtk::Window, sender: ComponentSender<AppModel>) { &["<Primary>comma"] ); + let sender = sender.clone(); + let action: RelmAction<NewFromTemplateAction> = RelmAction::new_with_target_value(move |_, template_id| { + sender.input(Action::NewFromTemplateById(template_id)) + }); + action_group.add_action(action); action_group.register_for_widget(&window); - - // Register new-from-template action with string parameter (template ID) - let template_action = - gio::SimpleAction::new("new-from-template", Some(glib::VariantTy::STRING)); - { - let sender = sender.clone(); - template_action.connect_activate(move |_, param| { - if let Some(variant) = param - && let Some(template_id) = variant.get::<String>() - { - sender.input(Action::NewFromTemplateById(template_id)); - } - }); - } - window.insert_action_group( - "win", - Some(&{ - let group = gio::SimpleActionGroup::new(); - group.add_action(&template_action); - group - }), - ); } struct AppInit { @@ -228,7 +221,7 @@ struct AppModel { impl AppModel { fn update_image(&mut self) { if let Ok(surface) = - render_to_surface(&self.map, &self.stage_type, &self.render_configuration) + render_to_surface(&self.map, &self.stage_type.to_localized(), &self.render_configuration) { self.surface_width = (surface.width() as f64 * self.zoom).round() as i32; self.surface_height = (surface.height() as f64 * self.zoom).round() as i32; @@ -250,12 +243,13 @@ impl AppModel { } fn window_title(&self) -> String { + let default_name = tr!("document.untitled"); let filename = self .current_file .as_ref() .and_then(|path| path.file_name()) .and_then(|name| name.to_str()) - .unwrap_or("Untitled"); + .unwrap_or(&default_name); if self.modified { format!("{}* - {}", filename, constants::APP_NAME) @@ -279,7 +273,6 @@ impl AppModel { } fn apply_preferences(&mut self) { - // Apply editor preferences self.source_view .set_wrap_mode(if self.preferences.soft_wrap_lines { gtk::WrapMode::Word @@ -287,7 +280,6 @@ impl AppModel { gtk::WrapMode::None }); - // Build CSS for editor font let css = if self.preferences.use_custom_editor_font { self.source_view.set_monospace(false); format!( @@ -295,7 +287,6 @@ impl AppModel { self.preferences.custom_editor_font_name, self.preferences.editor_font_size as i32 ) } else { - // Use monospace but still apply font size self.source_view.set_monospace(true); format!( "textview {{ font-size: {}pt; }}", @@ -303,28 +294,22 @@ impl AppModel { ) }; - // Update the CSS provider (reusing the same one avoids accumulation) self.editor_css_provider.load_from_data(&css); - // Apply render configuration self.render_configuration.options.smart_label_positioning = self.preferences.use_smart_label_positioning; self.render_configuration.options.show_background = self.preferences.show_map_background; - // Apply map font if self.preferences.use_custom_font { self.render_configuration.theme.fonts.face = self.preferences.custom_font_name.clone(); } else { - // Reset to default font self.render_configuration.theme.fonts.face = String::from("sans-serif"); } } fn rebuild_stage_type_list(&mut self) { - // Build list: built-in stage types + custom stages from preferences let mut stage_types: Vec<StageType> = ALL_STAGE_TYPES.to_vec(); - // Add custom stages from preferences for custom in &self.preferences.custom_stages { stage_types.push(StageType::Custom { name: custom.name.clone(), @@ -335,15 +320,13 @@ impl AppModel { }); } - // Update the StringList for the dropdown - // Clear existing items while self.stage_type_list.n_items() > 0 { self.stage_type_list.remove(0); } - // Add all stage type names for stage_type in &stage_types { - self.stage_type_list.append(stage_type.name()); + self.stage_type_list + .append(&stage_type.localized_name()); } self.available_stage_types = stage_types; @@ -352,11 +335,10 @@ impl AppModel { fn rebuild_templates_menu(&self) { self.templates_menu.remove_all(); for template in &self.preferences.map_templates { - let item = gio::MenuItem::new( + self.templates_menu.append( Some(&template.name), - Some(&format!("win.new-from-template::{}", template.id)), + Some(&format!("window.new-from-template::{}", template.id)) ); - self.templates_menu.append_item(&item); } } @@ -400,7 +382,10 @@ impl SimpleComponent for AppModel { pack_start = >k::Box { set_orientation: gtk::Orientation::Horizontal, - gtk::Button::with_label("Change Orientation") { + gtk::Button::with_label(&match model.orientation { + gtk::Orientation::Horizontal => tr!("command.view.use_vertical_layout"), + _ => tr!("command.view.layout.use_horizontal_layout") + }) { #[watch] set_icon_name: match model.orientation { gtk::Orientation::Horizontal => SPLIT_VERTICAL_REGULAR, @@ -412,7 +397,7 @@ impl SimpleComponent for AppModel { pack_end = >k::Box { set_orientation: gtk::Orientation::Horizontal, - gtk::Button::with_label("Export as Image") { + gtk::Button::with_label(&tr!("command.file.export")) { set_icon_name: IMAGE_REGULAR, ActionablePlus::set_stateless_action::<ExportImageAction>: &(), }, @@ -470,7 +455,7 @@ impl SimpleComponent for AppModel { set_margin_end: 5 }, - gtk::Button::with_label("Zoom Out") { + gtk::Button::with_label(&tr!("command.view.zoom_out")) { set_icon_name: ZOOM_OUT_REGULAR, set_margin_end: 5, add_css_class: "flat", @@ -493,7 +478,7 @@ impl SimpleComponent for AppModel { } }, - gtk::Button::with_label("Zoom In") { + gtk::Button::with_label(&tr!("command.view.zoom_in")) { set_icon_name: ZOOM_IN_REGULAR, add_css_class: "flat", add_css_class: "zoom", @@ -511,11 +496,12 @@ impl SimpleComponent for AppModel { root: Self::Root, sender: ComponentSender<Self>, ) -> ComponentParts<Self> { - let stage_types: Vec<&str> = ALL_STAGE_TYPES + let stage_types: Vec<String> = ALL_STAGE_TYPES .iter() - .map(|stage_type| stage_type.name()) + .map(|stage_type| stage_type.localized_name()) .collect(); - let stage_type_list = gtk::StringList::new(&stage_types); + let stage_type_refs: Vec<&str> = stage_types.iter().map(|s| s.as_str()).collect(); + let stage_type_list = gtk::StringList::new(&stage_type_refs); let source = sourceview5::Buffer::new(None); source.set_highlight_syntax(true); @@ -545,11 +531,9 @@ impl SimpleComponent for AppModel { setup_actions(root.clone(), sender.clone()); - // Create source view let source_view = sourceview5::View::with_buffer(&source); source_view.set_monospace(true); - // Create CSS provider for editor font styling let editor_css_provider = gtk::CssProvider::new(); source_view.style_context().add_provider( &editor_css_provider, @@ -562,53 +546,48 @@ impl SimpleComponent for AppModel { let stage_type = StageType::Activities; render_configuration.options.smart_label_positioning = true; let surface = - render_to_surface(&map, &stage_type, &render_configuration).unwrap_or_else(|_| { + render_to_surface(&map, &stage_type.to_localized(), &render_configuration).unwrap_or_else(|_| { ImageSurface::create(cairo::Format::ARgb32, 1, 1) - .expect("Failed to create fallback surface") + .expect("errors.render.failed_to_create_surface") }); if let Some(ref path) = current_file { file_registry::register_file(path, &root); } - // Load preferences let prefs = preferences::storage::load(); let templates_menu = gio::Menu::new(); - - // Populate templates menu from preferences for template in &prefs.map_templates { - let item = gio::MenuItem::new( + templates_menu.append( Some(&template.name), - Some(&format!("win.new-from-template::{}", template.id)), + Some(&format!("window.new-from-template::{}", template.id)) ); - templates_menu.append_item(&item); } - // Build main menu let main_menu = gio::Menu::new(); - main_menu.append(Some("New Map"), Some("window.new")); - main_menu.append_submenu(Some("New from Template"), &templates_menu); - main_menu.append(Some("Open..."), Some("window.open")); - main_menu.append(Some("Save"), Some("window.save")); - main_menu.append(Some("Save As..."), Some("window.save-as")); - main_menu.append(Some("Close"), Some("window.close")); - main_menu.append(Some("Export Map as Image"), Some("window.export-image")); + main_menu.append(Some(&tr!("command.file.new")), Some("window.new")); + main_menu.append_submenu(Some(&tr!("command.file.new_from_template")), &templates_menu); + main_menu.append(Some(&tr!("command.file.open")), Some("window.open")); + main_menu.append(Some(&tr!("command.file.save")), Some("window.save")); + main_menu.append(Some(&tr!("command.file.save_as")), Some("window.save-as")); + main_menu.append(Some(&tr!("command.file.close")), Some("window.close")); + main_menu.append(Some(&tr!("command.file.export")), Some("window.export-image")); let layout_section = gio::Menu::new(); layout_section.append( - Some("Use Vertical Layout"), + Some(&tr!("command.view.use_vertical_layout")), Some("window.change-orientation"), ); main_menu.append_section(None, &layout_section); let zoom_section = gio::Menu::new(); - zoom_section.append(Some("Zoom In"), Some("window.zoom-in")); - zoom_section.append(Some("Zoom Out"), Some("window.zoom-out")); + zoom_section.append(Some(&tr!("command.view.zoom_in")), Some("window.zoom-in")); + zoom_section.append(Some(&tr!("command.view.zoom_out")), Some("window.zoom-out")); main_menu.append_section(None, &zoom_section); let prefs_section = gio::Menu::new(); - prefs_section.append(Some("Preferences"), Some("window.preferences")); + prefs_section.append(Some(&tr!("command.application.preferences")), Some("window.preferences")); main_menu.append_section(None, &prefs_section); let mut model = AppModel { @@ -637,12 +616,10 @@ impl SimpleComponent for AppModel { main_menu, }; - // Apply preferences and rebuild stage type list model.apply_preferences(); model.rebuild_stage_type_list(); model.update_image(); - // Register this window's sender for broadcasting file_registry::register_sender(sender.input_sender().clone()); { @@ -667,11 +644,9 @@ impl SimpleComponent for AppModel { fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) { match message { - // View actions Action::ChangeOrientation => handlers::view::change_orientation(self), Action::StageTypeSelected(index) => handlers::view::stage_type_selected(self, index), - // Source changes Action::SourceChanged => { let pending = *self.pending_load_events.borrow(); if pending > 0 { @@ -683,12 +658,10 @@ impl SimpleComponent for AppModel { self.update_image(); } - // Zoom actions Action::Zoom(level) => handlers::zoom::zoom(self, level), Action::ZoomIn => handlers::zoom::zoom_in(self), Action::ZoomOut => handlers::zoom::zoom_out(self), - // Export Action::ExportImage => { dialogs::show_export_dialog(&self.window, self.current_file.clone(), sender) } @@ -696,9 +669,7 @@ impl SimpleComponent for AppModel { handlers::export::export_to_file(self, path, format) } - // File actions Action::New => { - // Use default template if one exists let content = self .preferences .map_templates @@ -731,10 +702,8 @@ impl SimpleComponent for AppModel { } Action::CloseWindow => handlers::file::close_window(self, &sender), - // Internal actions Action::MarkInitialized => self.initialized = true, - // Preferences Action::ShowPreferences => { if self.preferences_window.is_none() { let controller = preferences::PreferencesWindow::builder() @@ -760,14 +729,12 @@ impl SimpleComponent for AppModel { self.rebuild_stage_type_list(); self.rebuild_templates_menu(); self.update_image(); - // Broadcast to all other windows to reload preferences file_registry::broadcast_to_all_windows(Action::ReloadPreferences); } Action::PreferencesWindowClosed => { self.preferences_window = None; } Action::ReloadPreferences => { - // Reload preferences from disk (another window changed them) self.preferences = preferences::storage::load(); self.apply_preferences(); self.rebuild_stage_type_list(); @@ -779,9 +746,10 @@ impl SimpleComponent for AppModel { } fn main() { + i18n::init(); + relm4_icons::initialize_icons(icon_names::GRESOURCE_BYTES, icon_names::RESOURCE_PREFIX); - // Load preferences to get default template for initial window let prefs = preferences::storage::load(); let initial_content = prefs .map_templates diff --git a/src/preferences/pages/editor.rs b/src/preferences/pages/editor.rs index 9f10d43..d4acb94 100644 --- a/src/preferences/pages/editor.rs +++ b/src/preferences/pages/editor.rs @@ -18,6 +18,7 @@ use gtk::prelude::*; use relm4::Sender; use relm4::gtk; +use crate::tr; use crate::ui_helpers::create_switch_row; use super::super::models::UserPreferences; @@ -41,7 +42,7 @@ impl EditorPage { container.set_valign(gtk::Align::Start); // Font Size section - let font_size_label = gtk::Label::new(Some("Font Size")); + let font_size_label = gtk::Label::new(Some(&tr!("preferences.editor.font_size.title"))); font_size_label.add_css_class("heading"); font_size_label.set_halign(gtk::Align::Start); container.append(&font_size_label); @@ -56,18 +57,18 @@ impl EditorPage { }); } font_size_box.append(&font_size_spin); - font_size_box.append(>k::Label::new(Some("pt"))); + font_size_box.append(>k::Label::new(Some(&tr!("preferences.editor.font_size.unit")))); container.append(&font_size_box); // Editor Style section - let style_label = gtk::Label::new(Some("Editor Style")); + let style_label = gtk::Label::new(Some(&tr!("preferences.editor.editor_style.title"))); style_label.add_css_class("heading"); style_label.set_halign(gtk::Align::Start); style_label.set_margin_top(10); container.append(&style_label); // Soft Wrap Lines - let soft_wrap_row = create_switch_row("Soft Wrap Lines", preferences.soft_wrap_lines); + let soft_wrap_row = create_switch_row(&tr!("preferences.editor.editor_style.soft_wrap_lines"), preferences.soft_wrap_lines); let soft_wrap_switch = soft_wrap_row.1.clone(); { let sender = sender.clone(); @@ -80,13 +81,13 @@ impl EditorPage { // Use Custom Font let custom_font_row = - create_switch_row("Use Custom Editor Font", preferences.use_custom_editor_font); + create_switch_row(&tr!("preferences.editor.editor_style.use_custom_font"), preferences.use_custom_editor_font); let custom_font_switch = custom_font_row.1.clone(); container.append(&custom_font_row.0); // Font selection let font_row = gtk::Box::new(gtk::Orientation::Horizontal, 10); - font_row.append(>k::Label::new(Some("Font"))); + font_row.append(>k::Label::new(Some(&tr!("preferences.editor.font")))); let font_button = gtk::FontButton::new(); font_button.set_font(&format!( diff --git a/src/preferences/pages/general.rs b/src/preferences/pages/general.rs index 43cd784..d70e9e6 100644 --- a/src/preferences/pages/general.rs +++ b/src/preferences/pages/general.rs @@ -19,6 +19,8 @@ use gtk::prelude::*; use relm4::Sender; use relm4::gtk; +use crate::tr; + use super::super::window::PreferencesInput; pub struct GeneralPage { @@ -35,7 +37,7 @@ impl GeneralPage { container.set_valign(gtk::Align::Start); // Preferences section - let prefs_label = gtk::Label::new(Some("Preferences")); + let prefs_label = gtk::Label::new(Some(&tr!("preferences.general.preferences.title"))); prefs_label.add_css_class("heading"); prefs_label.set_halign(gtk::Align::Start); container.append(&prefs_label); @@ -43,7 +45,7 @@ impl GeneralPage { let button_box = gtk::Box::new(gtk::Orientation::Horizontal, 10); // Export button - let export_button = gtk::Button::with_label("Export..."); + let export_button = gtk::Button::with_label(&tr!("preferences.general.preferences.export")); { let sender = sender.clone(); export_button.connect_clicked(move |_| { @@ -53,7 +55,7 @@ impl GeneralPage { button_box.append(&export_button); // Import button - let import_button = gtk::Button::with_label("Import..."); + let import_button = gtk::Button::with_label(&tr!("preferences.general.preferences.import")); { let sender = sender.clone(); import_button.connect_clicked(move |_| { @@ -63,7 +65,7 @@ impl GeneralPage { button_box.append(&import_button); // Reset button - let reset_button = gtk::Button::with_label("Reset to Defaults"); + let reset_button = gtk::Button::with_label(&tr!("preferences.general.preferences.reset")); reset_button.add_css_class("destructive-action"); { let sender = sender.clone(); @@ -76,9 +78,7 @@ impl GeneralPage { container.append(&button_box); // Help text - let help_label = gtk::Label::new(Some( - "Export your preferences to share with others or back up your settings.\nImport preferences from a file to restore settings.", - )); + let help_label = gtk::Label::new(Some(&tr!("preferences.general.explanation"))); help_label.add_css_class("dim-label"); help_label.set_halign(gtk::Align::Start); help_label.set_wrap(true); @@ -95,15 +95,15 @@ impl GeneralPage { pub fn show_export_dialog(sender: &Sender<PreferencesInput>, window: >k::Window) { let filter = FileFilter::new(); filter.add_pattern("*.json"); - filter.set_name(Some("JSON files")); + filter.set_name(Some(&tr!("dialog.file_type.json"))); let dialog = gtk::FileChooserDialog::new( - Some("Export Preferences"), + Some(&tr!("dialog.export_preferences.title")), Some(window), gtk::FileChooserAction::Save, &[ - ("Cancel", gtk::ResponseType::Cancel), - ("Save", gtk::ResponseType::Accept), + (&tr!("dialog.action.cancel"), gtk::ResponseType::Cancel), + (&tr!("dialog.action.save"), gtk::ResponseType::Accept), ], ); dialog.set_current_name("preferences.json"); @@ -127,15 +127,15 @@ pub fn show_export_dialog(sender: &Sender<PreferencesInput>, window: >k::Windo pub fn show_import_dialog(sender: &Sender<PreferencesInput>, window: >k::Window) { let filter = FileFilter::new(); filter.add_pattern("*.json"); - filter.set_name(Some("JSON files")); + filter.set_name(Some(&tr!("dialog.file_type.json"))); let dialog = gtk::FileChooserDialog::new( - Some("Import Preferences"), + Some(&tr!("dialog.import_preferences.title")), Some(window), gtk::FileChooserAction::Open, &[ - ("Cancel", gtk::ResponseType::Cancel), - ("Open", gtk::ResponseType::Accept), + (&tr!("dialog.action.cancel"), gtk::ResponseType::Cancel), + (&tr!("dialog.action.open"), gtk::ResponseType::Accept), ], ); dialog.add_filter(&filter); diff --git a/src/preferences/pages/map.rs b/src/preferences/pages/map.rs index 8deae20..5ffd04a 100644 --- a/src/preferences/pages/map.rs +++ b/src/preferences/pages/map.rs @@ -18,6 +18,7 @@ use gtk::prelude::*; use relm4::Sender; use relm4::gtk; +use crate::tr; use crate::ui_helpers::create_switch_row; use super::super::models::UserPreferences; @@ -42,14 +43,14 @@ impl MapPage { container.set_valign(gtk::Align::Start); // Map Style section - let style_label = gtk::Label::new(Some("Map Style")); + let style_label = gtk::Label::new(Some(&tr!("preferences.map.map_style.title"))); style_label.add_css_class("heading"); style_label.set_halign(gtk::Align::Start); container.append(&style_label); // Show Background let background_row = - create_switch_row("Show Map Background", preferences.show_map_background); + create_switch_row(&tr!("preferences.map.map_style.show_background"), preferences.show_map_background); let show_background_switch = background_row.1.clone(); { let sender = sender.clone(); @@ -62,7 +63,7 @@ impl MapPage { // Smart Label Positioning let smart_row = create_switch_row( - "Use Smart Label Positioning", + &tr!("preferences.map.map_style.use_smart_label_positioning"), preferences.use_smart_label_positioning, ); let smart_positioning_switch = smart_row.1.clone(); @@ -76,13 +77,13 @@ impl MapPage { container.append(&smart_row.0); // Use Custom Font - let custom_font_row = create_switch_row("Use Custom Font", preferences.use_custom_font); + let custom_font_row = create_switch_row(&tr!("preferences.map.map_style.use_custom_font"), preferences.use_custom_font); let custom_font_switch = custom_font_row.1.clone(); container.append(&custom_font_row.0); // Font selection let font_row = gtk::Box::new(gtk::Orientation::Horizontal, 10); - font_row.append(>k::Label::new(Some("Font"))); + font_row.append(>k::Label::new(Some(&tr!("preferences.map.font")))); let font_button = gtk::FontButton::new(); font_button.set_font(&preferences.custom_font_name); @@ -115,16 +116,16 @@ impl MapPage { } // Export section - let export_label = gtk::Label::new(Some("Export")); + let export_label = gtk::Label::new(Some(&tr!("preferences.map.export.title"))); export_label.add_css_class("heading"); export_label.set_halign(gtk::Align::Start); export_label.set_margin_top(10); container.append(&export_label); let export_row = gtk::Box::new(gtk::Orientation::Horizontal, 10); - export_row.append(>k::Label::new(Some("Default Export Format"))); + export_row.append(>k::Label::new(Some(&tr!("preferences.map.export.default_format")))); - let formats = gtk::StringList::new(&["PNG", "SVG"]); + let formats = gtk::StringList::new(&[&tr!("export_formats.png"), &tr!("export_formats.svg")]); let export_format_dropdown = gtk::DropDown::new(Some(formats), None::<gtk::Expression>); let selected = match preferences.default_export_format.as_str() { "svg" => 1, diff --git a/src/preferences/pages/stages.rs b/src/preferences/pages/stages.rs index 6f4121e..9f0685d 100644 --- a/src/preferences/pages/stages.rs +++ b/src/preferences/pages/stages.rs @@ -19,6 +19,8 @@ use relm4::Sender; use relm4::gtk; use uuid::Uuid; +use crate::tr; + use super::super::models::UserPreferences; use super::super::window::{PreferencesInput, StageLabel}; @@ -37,14 +39,12 @@ impl StagesPage { container.set_margin_end(20); // Header - let header_label = gtk::Label::new(Some("Custom Stages")); + let header_label = gtk::Label::new(Some(&tr!("preferences.stages.title"))); header_label.add_css_class("heading"); header_label.set_halign(gtk::Align::Start); container.append(&header_label); - let help_label = gtk::Label::new(Some( - "Custom stages allow you to define your own evolution axis labels. These will be available for rendering when the renderer supports custom stages.", - )); + let help_label = gtk::Label::new(Some(&tr!("preferences.stages.explanation"))); help_label.add_css_class("dim-label"); help_label.set_halign(gtk::Align::Start); help_label.set_wrap(true); @@ -55,27 +55,27 @@ impl StagesPage { let header_row = gtk::Box::new(gtk::Orientation::Horizontal, 5); header_row.add_css_class("dim-label"); - let name_header = gtk::Label::new(Some("Name")); + let name_header = gtk::Label::new(Some(&tr!("preferences.stages.column.name"))); name_header.set_width_chars(15); name_header.set_xalign(0.0); header_row.append(&name_header); - let i_header = gtk::Label::new(Some("Stage I")); + let i_header = gtk::Label::new(Some(&tr!("preferences.stages.column.stage_i"))); i_header.set_width_chars(12); i_header.set_xalign(0.0); header_row.append(&i_header); - let ii_header = gtk::Label::new(Some("Stage II")); + let ii_header = gtk::Label::new(Some(&tr!("preferences.stages.column.stage_ii"))); ii_header.set_width_chars(12); ii_header.set_xalign(0.0); header_row.append(&ii_header); - let iii_header = gtk::Label::new(Some("Stage III")); + let iii_header = gtk::Label::new(Some(&tr!("preferences.stages.column.stage_iii"))); iii_header.set_width_chars(12); iii_header.set_xalign(0.0); header_row.append(&iii_header); - let iv_header = gtk::Label::new(Some("Stage IV")); + let iv_header = gtk::Label::new(Some(&tr!("preferences.stages.column.stage_iv"))); iv_header.set_width_chars(12); iv_header.set_xalign(0.0); header_row.append(&iv_header); @@ -104,7 +104,7 @@ impl StagesPage { button_box.set_halign(gtk::Align::End); let add_button = gtk::Button::from_icon_name("list-add-symbolic"); - add_button.set_tooltip_text(Some("Add custom stage")); + add_button.set_tooltip_text(Some(&tr!("preferences.stages.help.add"))); { let sender = sender.clone(); add_button.connect_clicked(move |_| { @@ -238,7 +238,7 @@ impl StagesPage { // Delete button let delete_button = gtk::Button::from_icon_name("list-remove-symbolic"); delete_button.add_css_class("flat"); - delete_button.set_tooltip_text(Some("Remove stage")); + delete_button.set_tooltip_text(Some(&tr!("preferences.stages.help.remove"))); { let sender = self.sender.clone(); delete_button.connect_clicked(move |_| { diff --git a/src/preferences/pages/templates.rs b/src/preferences/pages/templates.rs index da3cafc..acc679f 100644 --- a/src/preferences/pages/templates.rs +++ b/src/preferences/pages/templates.rs @@ -22,6 +22,8 @@ use relm4::gtk; use sourceview5::prelude::*; use uuid::Uuid; +use crate::tr; + use super::super::models::{Template, UserPreferences}; use super::super::window::PreferencesInput; @@ -49,7 +51,7 @@ impl TemplatesPage { let sidebar = gtk::Box::new(gtk::Orientation::Vertical, 5); sidebar.set_width_request(200); - let sidebar_label = gtk::Label::new(Some("Templates")); + let sidebar_label = gtk::Label::new(Some(&tr!("preferences.templates.title"))); sidebar_label.add_css_class("heading"); sidebar_label.set_halign(gtk::Align::Start); sidebar_label.set_margin_start(5); @@ -72,18 +74,19 @@ impl TemplatesPage { button_box.set_margin_top(5); let add_button = gtk::Button::from_icon_name("list-add-symbolic"); - add_button.set_tooltip_text(Some("Add template")); + add_button.set_tooltip_text(Some(&tr!("preferences.templates.help.add"))); { let sender = sender.clone(); add_button.connect_clicked(move |_| { - // Show a simple dialog to get the template name - show_add_template_dialog(sender.clone()); + sender.emit(PreferencesInput::AddTemplate( + tr!("dialog.template.default_label"), + )); }); } button_box.append(&add_button); let remove_button = gtk::Button::from_icon_name("list-remove-symbolic"); - remove_button.set_tooltip_text(Some("Remove template")); + remove_button.set_tooltip_text(Some(&tr!("preferences.templates.help.remove"))); { let sender = sender.clone(); let selected_id = selected_id.clone(); @@ -108,7 +111,7 @@ impl TemplatesPage { let editor_box = gtk::Box::new(gtk::Orientation::Vertical, 5); editor_box.set_hexpand(true); - let editor_label = gtk::Label::new(Some("Template Content")); + let editor_label = gtk::Label::new(Some(&tr!("preferences.templates.content"))); editor_label.add_css_class("heading"); editor_label.set_halign(gtk::Align::Start); editor_box.append(&editor_label); @@ -245,10 +248,10 @@ impl TemplatesPage { let default_button = gtk::Button::new(); if template.is_default { default_button.set_icon_name("emblem-default-symbolic"); - default_button.set_tooltip_text(Some("Default template")); + default_button.set_tooltip_text(Some(&tr!("preferences.templates.help.default"))); } else { default_button.set_icon_name("radio-symbolic"); - default_button.set_tooltip_text(Some("Set as default")); + default_button.set_tooltip_text(Some(&tr!("preferences.templates.help.set_as_default"))); } default_button.add_css_class("flat"); { @@ -260,54 +263,20 @@ impl TemplatesPage { } hbox.append(&default_button); - // Template name - let name_label = gtk::Label::new(Some(&template.name)); - name_label.set_hexpand(true); - name_label.set_xalign(0.0); - hbox.append(&name_label); + // Template name (editable) + let name_entry = gtk::Entry::new(); + name_entry.set_text(&template.name); + name_entry.set_hexpand(true); + { + let sender = self.sender.clone(); + let id = template.id; + name_entry.connect_changed(move |entry| { + sender.emit(PreferencesInput::SetTemplateName(id, entry.text().to_string())); + }); + } + hbox.append(&name_entry); row.set_child(Some(&hbox)); row } } - -fn show_add_template_dialog(sender: Sender<PreferencesInput>) { - let dialog = gtk::Dialog::with_buttons( - Some("New Template"), - None::<>k::Window>, - gtk::DialogFlags::MODAL | gtk::DialogFlags::DESTROY_WITH_PARENT, - &[ - ("Cancel", gtk::ResponseType::Cancel), - ("Add", gtk::ResponseType::Accept), - ], - ); - dialog.set_default_response(gtk::ResponseType::Accept); - - let content = dialog.content_area(); - content.set_margin_top(10); - content.set_margin_bottom(10); - content.set_margin_start(10); - content.set_margin_end(10); - content.set_spacing(10); - - let label = gtk::Label::new(Some("Template name:")); - label.set_halign(gtk::Align::Start); - content.append(&label); - - let entry = gtk::Entry::new(); - entry.set_placeholder_text(Some("My Template")); - entry.set_activates_default(true); - content.append(&entry); - - dialog.connect_response(move |dialog, response| { - if response == gtk::ResponseType::Accept { - let name = entry.text().to_string(); - if !name.is_empty() { - sender.emit(PreferencesInput::AddTemplate(name)); - } - } - dialog.close(); - }); - - dialog.present(); -} diff --git a/src/preferences/window.rs b/src/preferences/window.rs index 8b38c29..2118056 100644 --- a/src/preferences/window.rs +++ b/src/preferences/window.rs @@ -22,6 +22,8 @@ use std::path::PathBuf; use gtk::prelude::*; use relm4::{ComponentParts, ComponentSender, SimpleComponent, gtk}; +use crate::tr; + use super::models::UserPreferences; use super::pages::{editor, general, map, stages, templates}; use super::storage; @@ -52,7 +54,6 @@ pub enum PreferencesInput { AddTemplate(String), RemoveTemplate(uuid::Uuid), SetTemplateContent(uuid::Uuid, String), - #[allow(dead_code)] SetTemplateName(uuid::Uuid, String), SetDefaultTemplate(uuid::Uuid), @@ -101,7 +102,7 @@ impl SimpleComponent for PreferencesWindow { view! { gtk::Window { - set_title: Some("Preferences"), + set_title: Some(&tr!("preferences.title")), set_default_width: 700, set_default_height: 500, set_modal: true, @@ -127,11 +128,11 @@ impl SimpleComponent for PreferencesWindow { set_transition_type: gtk::StackTransitionType::SlideLeftRight, set_vexpand: true, - add_titled[Some("general"), "General"] = &model.general_page.widget() -> gtk::Box {}, - add_titled[Some("editor"), "Editor"] = &model.editor_page.widget() -> gtk::Box {}, - add_titled[Some("map"), "Map"] = &model.map_page.widget() -> gtk::Box {}, - add_titled[Some("stages"), "Stages"] = &model.stages_page.widget() -> gtk::Box {}, - add_titled[Some("templates"), "Templates"] = &model.templates_page.widget() -> gtk::Box {}, + add_titled[Some("general"), &tr!("preferences.menu.general")] = &model.general_page.widget() -> gtk::Box {}, + add_titled[Some("editor"), &tr!("preferences.menu.editor")] = &model.editor_page.widget() -> gtk::Box {}, + add_titled[Some("map"), &tr!("preferences.menu.map")] = &model.map_page.widget() -> gtk::Box {}, + add_titled[Some("stages"), &tr!("preferences.menu.stages")] = &model.stages_page.widget() -> gtk::Box {}, + add_titled[Some("templates"), &tr!("preferences.menu.templates")] = &model.templates_page.widget() -> gtk::Box {}, }, }, } diff --git a/src/stages.rs b/src/stages.rs index fba86b3..8de8f89 100644 --- a/src/stages.rs +++ b/src/stages.rs @@ -15,6 +15,7 @@ // along with this program. If not, see <http://www.gnu.org/licenses/>. use wmap_renderer::StageType; +use crate::tr; /// All available stage types for rendering Wardley Maps. /// Order determines dropdown menu order. @@ -40,3 +41,161 @@ pub const ALL_STAGE_TYPES: &[StageType] = &[ StageType::DecisionDrivers, StageType::Behavior, ]; + +pub trait LocalizedStageType { + fn localized_name(&self) -> String; + fn localized_i(&self) -> String; + fn localized_ii(&self) -> String; + fn localized_iii(&self) -> String; + fn localized_iv(&self) -> String; + + fn to_localized(&self) -> StageType; +} + +impl LocalizedStageType for StageType { + fn localized_name(&self) -> String { + match self { + StageType::Activities => tr!("stage.activities.name"), + StageType::Practice => tr!("stage.practice.name"), + StageType::Data => tr!("stage.data.name"), + StageType::Knowledge => tr!("stages.knowledge.name"), + StageType::Ubiquity => tr!("stages.ubiquity.name"), + StageType::Certainty => tr!("stages.certainty.name"), + StageType::PublicationTypes => tr!("stages.publication_types.name"), + StageType::Market => tr!("stages.market.name"), + StageType::KnowledgeManagement => tr!("stages.knowledge_management.name"), + StageType::MarketPerception => tr!("stages.market_perception.name"), + StageType::UserPerception => tr!("stages.user_perception.name"), + StageType::PerceptionInIndustry => tr!("stages.perception_in_industry.name"), + StageType::FocusOfValue => tr!("stages.focus_of_value.name"), + StageType::Understanding => tr!("stages.understanding.name"), + StageType::Comparison => tr!("stages.comparison.name"), + StageType::Failure => tr!("stages.failure.name"), + StageType::MarketAction => tr!("stages.market_action.name"), + StageType::Efficiency => tr!("stages.efficiency.name"), + StageType::DecisionDrivers => tr!("stages.decision_drivers.name"), + StageType::Behavior => tr!("stages.behavior.name"), + StageType::Cynefin => tr!("stages.cynefin.name"), + StageType::EvolutionStage => tr!("stages.evolution_stage.name"), + StageType::Custom{name, i: _, ii: _, iii: _, iv: _} => name.to_string() + } + } + fn localized_i(&self) -> String { + match self { + StageType::Activities => tr!("stage.activities.i"), + StageType::Practice => tr!("stage.practice.i"), + StageType::Data => tr!("stage.data.i"), + StageType::Knowledge => tr!("stages.knowledge.i"), + StageType::Ubiquity => tr!("stages.ubiquity.i"), + StageType::Certainty => tr!("stages.certainty.i"), + StageType::PublicationTypes => tr!("stages.publication_types.i"), + StageType::Market => tr!("stages.market.i"), + StageType::KnowledgeManagement => tr!("stages.knowledge_management.i"), + StageType::MarketPerception => tr!("stages.market_perception.i"), + StageType::UserPerception => tr!("stages.user_perception.i"), + StageType::PerceptionInIndustry => tr!("stages.perception_in_industry.i"), + StageType::FocusOfValue => tr!("stages.focus_of_value.i"), + StageType::Understanding => tr!("stages.understanding.i"), + StageType::Comparison => tr!("stages.comparison.i"), + StageType::Failure => tr!("stages.failure.i"), + StageType::MarketAction => tr!("stages.market_action.i"), + StageType::Efficiency => tr!("stages.efficiency.i"), + StageType::DecisionDrivers => tr!("stages.decision_drivers.i"), + StageType::Behavior => tr!("stages.behavior.i"), + StageType::Cynefin => tr!("stages.cynefin.i"), + StageType::EvolutionStage => tr!("stages.evolution_stage.i"), + StageType::Custom{name: _, i, ii: _, iii: _, iv: _} => i.to_string() + } + } + fn localized_ii(&self) -> String { + match self { + StageType::Activities => tr!("stage.activities.ii"), + StageType::Practice => tr!("stage.practice.ii"), + StageType::Data => tr!("stage.data.ii"), + StageType::Knowledge => tr!("stages.knowledge.ii"), + StageType::Ubiquity => tr!("stages.ubiquity.ii"), + StageType::Certainty => tr!("stages.certainty.ii"), + StageType::PublicationTypes => tr!("stages.publication_types.ii"), + StageType::Market => tr!("stages.market.ii"), + StageType::KnowledgeManagement => tr!("stages.knowledge_management.ii"), + StageType::MarketPerception => tr!("stages.market_perception.ii"), + StageType::UserPerception => tr!("stages.user_perception.ii"), + StageType::PerceptionInIndustry => tr!("stages.perception_in_industry.ii"), + StageType::FocusOfValue => tr!("stages.focus_of_value.ii"), + StageType::Understanding => tr!("stages.understanding.ii"), + StageType::Comparison => tr!("stages.comparison.ii"), + StageType::Failure => tr!("stages.failure.ii"), + StageType::MarketAction => tr!("stages.market_action.ii"), + StageType::Efficiency => tr!("stages.efficiency.ii"), + StageType::DecisionDrivers => tr!("stages.decision_drivers.ii"), + StageType::Behavior => tr!("stages.behavior.ii"), + StageType::Cynefin => tr!("stages.cynefin.ii"), + StageType::EvolutionStage => tr!("stages.evolution_stage.ii"), + StageType::Custom{name: _, i: _, ii, iii: _, iv: _} => ii.to_string() + } + } + fn localized_iii(&self) -> String { + match self { + StageType::Activities => tr!("stage.activities.iii"), + StageType::Practice => tr!("stage.practice.iii"), + StageType::Data => tr!("stage.data.iii"), + StageType::Knowledge => tr!("stages.knowledge.iii"), + StageType::Ubiquity => tr!("stages.ubiquity.iii"), + StageType::Certainty => tr!("stages.certainty.iii"), + StageType::PublicationTypes => tr!("stages.publication_types.iii"), + StageType::Market => tr!("stages.market.iii"), + StageType::KnowledgeManagement => tr!("stages.knowledge_management.iii"), + StageType::MarketPerception => tr!("stages.market_perception.iii"), + StageType::UserPerception => tr!("stages.user_perception.iii"), + StageType::PerceptionInIndustry => tr!("stages.perception_in_industry.iii"), + StageType::FocusOfValue => tr!("stages.focus_of_value.iii"), + StageType::Understanding => tr!("stages.understanding.iii"), + StageType::Comparison => tr!("stages.comparison.iii"), + StageType::Failure => tr!("stages.failure.iii"), + StageType::MarketAction => tr!("stages.market_action.iii"), + StageType::Efficiency => tr!("stages.efficiency.iii"), + StageType::DecisionDrivers => tr!("stages.decision_drivers.iii"), + StageType::Behavior => tr!("stages.behavior.iii"), + StageType::Cynefin => tr!("stages.cynefin.iii"), + StageType::EvolutionStage => tr!("stages.evolution_stage.iii"), + StageType::Custom{name: _, i: _, ii: _, iii, iv: _} => iii.to_string() + } + } + fn localized_iv(&self) -> String { + match self { + StageType::Activities => tr!("stage.activities.iv"), + StageType::Practice => tr!("stage.practice.iv"), + StageType::Data => tr!("stage.data.iv"), + StageType::Knowledge => tr!("stages.knowledge.iv"), + StageType::Ubiquity => tr!("stages.ubiquity.iv"), + StageType::Certainty => tr!("stages.certainty.iv"), + StageType::PublicationTypes => tr!("stages.publication_types.iv"), + StageType::Market => tr!("stages.market.iv"), + StageType::KnowledgeManagement => tr!("stages.knowledge_management.iv"), + StageType::MarketPerception => tr!("stages.market_perception.iv"), + StageType::UserPerception => tr!("stages.user_perception.iv"), + StageType::PerceptionInIndustry => tr!("stages.perception_in_industry.iv"), + StageType::FocusOfValue => tr!("stages.focus_of_value.iv"), + StageType::Understanding => tr!("stages.understanding.iv"), + StageType::Comparison => tr!("stages.comparison.iv"), + StageType::Failure => tr!("stages.failure.iv"), + StageType::MarketAction => tr!("stages.market_action.iv"), + StageType::Efficiency => tr!("stages.efficiency.iv"), + StageType::DecisionDrivers => tr!("stages.decision_drivers.iv"), + StageType::Behavior => tr!("stages.behavior.iv"), + StageType::Cynefin => tr!("stages.cynefin.iv"), + StageType::EvolutionStage => tr!("stages.evolution_stage.iv"), + StageType::Custom{name: _, i: _, ii: _, iii: _, iv} => iv.to_string() + } + } + + fn to_localized(&self) -> StageType { + StageType::Custom{ + name: self.localized_name(), + i: self.localized_i(), + ii: self.localized_ii(), + iii: self.localized_iii(), + iv: self.localized_iv() + } + } +} |