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/main.rs | |
| parent | 90b12c34debaa4aa816d4e6862cdb4e39c7181e4 (diff) | |
Add localization
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 134 |
1 files changed, 51 insertions, 83 deletions
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 |