aboutsummaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authorRubén Beltrán del Río <jj@r.bdr.sh>2026-01-17 00:36:49 +0100
committerRubén Beltrán del Río <jj@r.bdr.sh>2026-01-17 02:43:21 +0100
commit17898fbabde35ab346c133114e78614e707c0eca (patch)
tree90f0fd36211c036a507a0af092aac3c5fb8e7ac8 /src/main.rs
parent90b12c34debaa4aa816d4e6862cdb4e39c7181e4 (diff)
Add localization
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs134
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 = &gtk::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 = &gtk::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