feat(locale): localize full interface

This commit is contained in:
Hakan Seven 2026-08-02 22:09:25 +03:00
commit 3e6693b93f
205 changed files with 32367 additions and 3808 deletions

View file

@ -14,6 +14,7 @@ use iced::widget::{
use iced::window;
use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme};
use iced_aw::ContextMenu;
use crate::t;
mod controls;
mod modal;
@ -1210,7 +1211,7 @@ impl OpenCADStudio {
let copy_btn = button(
row![
crate::ui::icons::themed_primary(crate::ui::icons::COPY, 11.0),
text("Copy").size(11),
text(t!("Copy")).size(11),
]
.spacing(4)
.align_y(iced::Center),
@ -1221,7 +1222,7 @@ impl OpenCADStudio {
let clear_btn = button(
row![
crate::ui::icons::themed_danger(crate::ui::icons::TRASH, 11.0),
text("Clear").size(11),
text(t!("Clear")).size(11),
]
.spacing(4)
.align_y(iced::Center),
@ -1230,7 +1231,7 @@ impl OpenCADStudio {
.style(perf_button_style)
.padding([2, 6]);
let header = row![
text("PERF").size(12).style(|theme: &Theme| iced::widget::text::Style {
text(t!("PERF")).size(12).style(|theme: &Theme| iced::widget::text::Style {
color: Some(theme.palette().success.base.color),
}),
Space::new().width(iced::Length::Fill),

View file

@ -1,6 +1,8 @@
use super::super::{Message, OpenCADStudio};
use crate::t;
use iced::widget::{button, column, container, row, text, Space};
use iced::{Background, Element, Fill, Fit, Theme};
use std::borrow::Cow;
impl OpenCADStudio {
/// Title shown in the active modal's title bar. Keep in sync with the
@ -221,7 +223,7 @@ impl OpenCADStudio {
)
} else {
automatic_flow(ex, |flow| {
container(text("The selected layer state is no longer available."))
container(text(t!("The selected layer state is no longer available.")))
.padding(16)
.width(flow.width)
.height(flow.height)
@ -941,12 +943,12 @@ fn automatic_flow<'a>(
)
}
fn dialog_button(
label: &'static str,
fn dialog_button<'a>(
label: impl Into<Cow<'a, str>>,
message: Message,
style: fn(&Theme, button::Status) -> button::Style,
) -> Element<'static, Message> {
button(text(label).size(13))
) -> Element<'a, Message> {
button(text(label.into()).size(13))
.on_press(message)
.style(style)
.padding([6, 18])
@ -980,7 +982,7 @@ fn save_as_dialog_window<'a>(
.iter()
.copied()
.find(|&s| s == format);
let label = |s: &'static str| text(s).size(11).style(dialog_muted_text_style);
let label = |s: Cow<'static, str>| text(s).size(11).style(dialog_muted_text_style);
let field_width = if matches!(sizing.width, iced::Length::Fill) {
Fill
} else {
@ -988,7 +990,7 @@ fn save_as_dialog_window<'a>(
};
let mut items: Vec<Element<'a, Message>> = Vec::new();
items.push(text("Save Drawing As").size(14).into());
items.push(text(t!("Save Drawing As")).size(14).into());
items.push(Space::new().height(12).into());
// Web has no native file dialog, so the file name is typed here. On native
@ -997,7 +999,7 @@ fn save_as_dialog_window<'a>(
{
items.push(
row![
label("File name:").width(70),
label(t!("File name:")).width(70),
iced::widget::text_input("drawing.dwg", filename)
.on_input(Message::SaveDialogFilenameChanged)
.size(13)
@ -1016,7 +1018,7 @@ fn save_as_dialog_window<'a>(
items.push(
row![
label("Format:").width(70),
label(t!("Format:")).width(70),
iced::widget::pick_list(
sel_fmt,
crate::io::SAVE_FORMAT_OPTIONS,
@ -1034,9 +1036,9 @@ fn save_as_dialog_window<'a>(
items.push(
row![
Space::new().width(Fit),
dialog_button("Save as...", Message::SaveDialogConfirm, button::primary),
dialog_button(t!("Save as..."), Message::SaveDialogConfirm, button::primary),
Space::new().width(8),
dialog_button("Cancel", Message::SaveDialogCancel, button::secondary),
dialog_button(t!("Cancel"), Message::SaveDialogCancel, button::secondary),
]
.into(),
);
@ -1058,18 +1060,21 @@ fn unsaved_changes_dialog_window(
name: &str,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'static, Message> {
let body_text = format!("Do you want to save changes to \"{}\"?", name);
let body_text = t!(
"Do you want to save changes to \"%{name}\"?",
name = name
);
container(
column![
text(body_text).size(13),
iced::widget::Space::new().height(20),
row![
dialog_button("Save", Message::UnsavedDialogSave, button::primary),
dialog_button(t!("Save"), Message::UnsavedDialogSave, button::primary),
iced::widget::Space::new().width(8),
dialog_button("Discard", Message::UnsavedDialogDiscard, button::danger),
dialog_button(t!("Discard"), Message::UnsavedDialogDiscard, button::danger),
iced::widget::Space::new().width(8),
dialog_button("Cancel", Message::UnsavedDialogCancel, button::secondary),
dialog_button(t!("Cancel"), Message::UnsavedDialogCancel, button::secondary),
],
]
.spacing(0),
@ -1090,19 +1095,18 @@ fn file_in_use_dialog_window(
let file_name = std::path::Path::new(path)
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| "Drawing".to_string());
let heading = format!("\"{file_name}\" could not be saved.");
let path_line = format!("Path: {path}");
let details = format!("Details: {error}");
.unwrap_or_else(|| t!("Drawing").into_owned());
let heading = t!("\"%{file_name}\" could not be saved.", file_name = file_name);
let path_line = t!("Path: %{path}", path = path);
let details = t!("Details: %{error}", error = error);
container(
column![
text(heading).size(14),
Space::new().height(8),
text(
"The file is open or being used by another application. \
Close it there and retry, or save this drawing under a different name."
)
text(t!(
"The file is open or being used by another application. Close it there and retry, or save this drawing under a different name."
))
.size(13)
.width(Fit),
Space::new().height(12),
@ -1112,19 +1116,19 @@ fn file_in_use_dialog_window(
Space::new().height(18),
row![
dialog_button(
"Retry",
t!("Retry"),
Message::SaveFileInUseRetry,
button::primary
),
Space::new().width(8),
dialog_button(
"Save As",
t!("Save As"),
Message::SaveFileInUseSaveAs,
button::secondary
),
Space::new().width(8),
dialog_button(
"Cancel",
t!("Cancel"),
Message::SaveFileInUseCancel,
button::secondary
),
@ -1147,18 +1151,17 @@ fn external_change_dialog_window(
let file_name = std::path::Path::new(path)
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| "Drawing".to_string());
let heading = format!("\"{file_name}\" was changed by another application.");
let path_line = format!("Path: {path}");
.unwrap_or_else(|| t!("Drawing").into_owned());
let heading = t!("\"%{file_name}\" was changed by another application.", file_name = file_name);
let path_line = t!("Path: %{path}", path = path);
container(
column![
text(heading).size(14),
Space::new().height(8),
text(
"Saving now could destroy those external changes. Reload the disk copy, \
save your local work elsewhere, or explicitly overwrite it."
)
text(t!(
"Saving now could destroy those external changes. Reload the disk copy, save your local work elsewhere, or explicitly overwrite it."
))
.size(13)
.width(Fit),
Space::new().height(12),
@ -1166,25 +1169,25 @@ fn external_change_dialog_window(
Space::new().height(18),
row![
dialog_button(
"Reload from Disk",
t!("Reload from Disk"),
Message::ExternalChangeReload,
button::primary
),
Space::new().width(8),
dialog_button(
"Save As",
t!("Save As"),
Message::ExternalChangeSaveAs,
button::secondary
),
Space::new().width(8),
dialog_button(
"Overwrite",
t!("Overwrite"),
Message::ExternalChangeOverwrite,
button::danger
),
Space::new().width(8),
dialog_button(
"Cancel",
t!("Cancel"),
Message::ExternalChangeCancel,
button::secondary
),
@ -1209,10 +1212,11 @@ fn aec_drop_dialog_window(
src_version: &str,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'static, Message> {
let body_text = format!(
"This drawing contains {count} AEC/Civil objects that \"{target}\" \
cannot store, so they will not be saved.\n\n\
To keep them, save in the source version ({src_version})."
let body_text = t!(
"This drawing contains %{count} AEC/Civil objects that \"%{target}\" cannot store, so they will not be saved.\n\nTo keep them, save in the source version (%{src_version}).",
count = count,
target = target,
src_version = src_version,
);
container(
@ -1221,14 +1225,14 @@ fn aec_drop_dialog_window(
iced::widget::Space::new().height(20),
row![
dialog_button(
"Save in source version",
t!("Save in source version"),
Message::AecDropSameVersion,
button::primary
),
iced::widget::Space::new().width(8),
dialog_button("Save anyway", Message::AecDropProceed, button::warning),
dialog_button(t!("Save anyway"), Message::AecDropProceed, button::warning),
iced::widget::Space::new().width(8),
dialog_button("Back", Message::AecDropBack, button::secondary),
dialog_button(t!("Back"), Message::AecDropBack, button::secondary),
],
]
.spacing(0),
@ -1249,16 +1253,25 @@ fn layer_delete_warning_window(
count: usize,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'static, Message> {
let obj = if count == 1 { "object" } else { "objects" };
let subject = if names.len() == 1 {
format!("Layer \"{}\"", names[0])
let obj = if count == 1 { t!("object") } else { t!("objects") };
let has = if names.len() == 1 { t!("has") } else { t!("hold") };
let those = if count == 1 {
t!("that object")
} else {
format!("{} selected layers", names.len())
t!("those objects")
};
let body_text = format!(
"{subject} still {} {count} {obj}.\n\nDeleting will also remove {} from the drawing. Continue?",
if names.len() == 1 { "has" } else { "hold" },
if count == 1 { "that object" } else { "those objects" }
let subject = if names.len() == 1 {
t!("Layer \"%{name}\"", name = names[0]).into_owned()
} else {
t!("%{count} selected layers", count = names.len()).into_owned()
};
let body_text = t!(
"%{subject} still %{has} %{count} %{obj}.\n\nDeleting will also remove %{those} from the drawing. Continue?",
subject = subject,
has = has,
count = count,
obj = obj,
those = those,
);
container(
@ -1267,12 +1280,12 @@ fn layer_delete_warning_window(
iced::widget::Space::new().height(20),
row![
dialog_button(
"Delete Objects",
t!("Delete Objects"),
Message::LayerDeleteConfirm,
button::danger
),
iced::widget::Space::new().width(8),
dialog_button("Cancel", Message::CloseModal, button::secondary),
dialog_button(t!("Cancel"), Message::CloseModal, button::secondary),
],
]
.spacing(0),
@ -1293,19 +1306,19 @@ fn default_assoc_dialog_window(
) -> Element<'static, Message> {
container(
column![
text("Make Open CAD Studio your default CAD app?")
text(t!("Make Open CAD Studio your default CAD app?"))
.size(15),
iced::widget::Space::new().height(10),
text("Open .dwg and .dxf drawings in Open CAD Studio by default. You can change this later in your system settings.")
text(t!("Open .dwg and .dxf drawings in Open CAD Studio by default. You can change this later in your system settings."))
.size(12)
.style(dialog_muted_text_style),
iced::widget::Space::new().height(22),
row![
iced::widget::Space::new().width(Fit),
dialog_button("Not now", Message::AssocPromptNo, button::secondary),
dialog_button(t!("Not now"), Message::AssocPromptNo, button::secondary),
iced::widget::Space::new().width(8),
dialog_button(
"Yes, set as default",
t!("Yes, set as default"),
Message::AssocPromptYes,
button::primary
),

View file

@ -4,6 +4,7 @@ use iced::widget::{
Space,
};
use iced::{Background, Border, Color, Element, Fill, Theme};
use crate::t;
pub(super) fn position_canvas_overlay<'a>(
anchor: iced::Point,
@ -28,7 +29,7 @@ pub(super) fn text_inline_overlay(
ed: &super::super::text_inline::TextInlineState,
canvas: (f32, f32),
) -> Element<'_, Message> {
let field = text_input("Text", &ed.value)
let field = text_input(t!("Text").as_ref(), &ed.value)
.id(iced::widget::Id::new(TEXT_INLINE_ID))
.on_input(Message::TextInlineInput)
.on_submit(Message::TextInlineOk)
@ -366,7 +367,7 @@ pub(super) fn mtext_editor_overlay<'a>(
crate::ui::modal::modal(
iced::widget::Space::new().width(Fill).height(Fill),
"Text Editor",
t!("Text Editor"),
content,
Message::MTextCancel,
modal_offset,
@ -577,7 +578,7 @@ fn mtext_editor_content<'a>(
f64::from(writing_area_px / preview_scale).max(slider_min * 2.0);
let width_slider = column![
row![
text(format!("Width: {:.3}", ed.rect_width)).size(11),
text(t!("Width: %{value}", value = format!("{:.3}", ed.rect_width))).size(11),
Space::new().width(width),
text(format!("{:.0}%", ed.rect_width / slider_max * 100.0)).size(11),
]
@ -709,7 +710,7 @@ fn mtext_editor_content<'a>(
let action_bar = container(
row![
iced::widget::Space::new().width(width),
crate::ui::style::style_manager::tb_button("Apply", Message::MTextApply, true),
crate::ui::style::style_manager::tb_button(t!("Apply"), Message::MTextApply, true),
]
.align_y(iced::Alignment::Center),
)
@ -784,13 +785,13 @@ pub(super) fn viewport_context_menu_overlay(
let mut items: Vec<Element<'static, Message>> = Vec::new();
if has_cmd {
items.push(item("Cancel".to_string(), Message::CommandEscape));
items.push(item("Enter".to_string(), Message::CommandFinalize));
items.push(item(t!("Cancel").into_owned(), Message::CommandEscape));
items.push(item(t!("Enter").into_owned(), Message::CommandFinalize));
} else {
if !last_cmds.is_empty() {
let last = last_cmds[0].clone();
items.push(item(
format!("Repeat {last}"),
t!("Repeat %{last}", last = last).into_owned(),
Message::Command(last.to_uppercase()),
));
if last_cmds.len() > 1 {
@ -802,13 +803,13 @@ pub(super) fn viewport_context_menu_overlay(
items.push(sep());
}
if has_selection {
items.push(item("Delete".to_string(), Message::DeleteSelected));
items.push(item(t!("Delete").into_owned(), Message::DeleteSelected));
items.push(item(
"Move".to_string(),
t!("Move").into_owned(),
Message::Command("MOVE".to_string()),
));
items.push(item(
"Copy".to_string(),
t!("Copy").into_owned(),
Message::Command("COPY".to_string()),
));
items.push(sep());
@ -820,7 +821,7 @@ pub(super) fn viewport_context_menu_overlay(
items.push(
button(
row![
text("Draw Order").size(12),
text(t!("Draw Order").into_owned()).size(12),
iced::widget::Space::new().width(Fill),
do_caret,
]
@ -834,51 +835,51 @@ pub(super) fn viewport_context_menu_overlay(
);
if draworder_open {
items.push(subitem(
"Bring to Front".to_string(),
t!("Bring to Front").into_owned(),
Message::Command("DRAWORDER F".to_string()),
));
items.push(subitem(
"Send to Back".to_string(),
t!("Send to Back").into_owned(),
Message::Command("DRAWORDER B".to_string()),
));
items.push(subitem(
"Bring Above Object".to_string(),
t!("Bring Above Object").into_owned(),
Message::DrawOrderPickRef(true),
));
items.push(subitem(
"Send Under Object".to_string(),
t!("Send Under Object").into_owned(),
Message::DrawOrderPickRef(false),
));
}
items.push(sep());
items.push(item(
"Isolate Objects".to_string(),
t!("Isolate Objects").into_owned(),
Message::Command("ISOLATEOBJECTS".to_string()),
));
items.push(item(
"Hide Objects".to_string(),
t!("Hide Objects").into_owned(),
Message::Command("HIDEOBJECTS".to_string()),
));
items.push(sep());
items.push(item("Select Similar".to_string(), Message::SelectSimilar));
items.push(item(t!("Select Similar").into_owned(), Message::SelectSimilar));
items.push(item(
"Invert Selection".to_string(),
t!("Invert Selection").into_owned(),
Message::InvertSelection,
));
}
if isolation_active {
items.push(item(
"End Object Isolation".to_string(),
t!("End Object Isolation").into_owned(),
Message::Command("UNISOLATEOBJECTS".to_string()),
));
}
items.push(item(
"Select All".to_string(),
t!("Select All").into_owned(),
Message::Command("SELECTALL".to_string()),
));
items.push(item("Quick Select...".to_string(), Message::QSelectOpen));
items.push(item(t!("Quick Select...").into_owned(), Message::QSelectOpen));
items.push(item(
"Zoom Extents".to_string(),
t!("Zoom Extents").into_owned(),
Message::Command("ZOOM EXTENTS".to_string()),
));
}
@ -1020,7 +1021,7 @@ pub(super) fn qselect_overlay<'a>(
crate::ui::modal::modal(
Space::new().width(Fill).height(Fill),
"Quick Select",
t!("Quick Select"),
content,
Message::QSelectClose,
modal_offset,
@ -1081,13 +1082,13 @@ fn qselect_content<'a>(
iced::Length::Shrink
};
let label = |s: &'static str| {
let label = |s: std::borrow::Cow<'static, str>| {
text(s)
.size(12)
.width(iced::Length::Fixed(90.0))
};
let btn = |lbl: &'static str, msg: Message, primary: bool| {
let btn = |lbl: std::borrow::Cow<'static, str>, msg: Message, primary: bool| {
button(text(lbl).size(12))
.on_press(msg)
.style(move |theme: &Theme, st| {
@ -1124,7 +1125,7 @@ fn qselect_content<'a>(
let panel_body = column![
row![
label("Object type:"),
label(t!("Object type:")),
iced::widget::pick_list(
Some(type_sel),
type_options,
@ -1144,7 +1145,7 @@ fn qselect_content<'a>(
.width(sizing.width),
Space::new().height(6),
row![
label("Property:"),
label(t!("Property:")),
iced::widget::pick_list(
Some(prop_sel),
prop_options,
@ -1164,7 +1165,7 @@ fn qselect_content<'a>(
.width(sizing.width),
Space::new().height(6),
row![
label("Operator:"),
label(t!("Operator:")),
iced::widget::pick_list(
Some(state.operator),
op_options,
@ -1177,7 +1178,7 @@ fn qselect_content<'a>(
.spacing(8)
.width(sizing.width),
Space::new().height(6),
row![label("Value:"), value_input,]
row![label(t!("Value:")), value_input,]
.align_y(iced::Alignment::Center)
.spacing(8)
.width(sizing.width),
@ -1187,15 +1188,15 @@ fn qselect_content<'a>(
.on_toggle(Message::QSelectSetAppend)
.size(14),
Space::new().width(6),
text("Append to current selection").size(12),
text(t!("Append to current selection")).size(12),
]
.align_y(iced::Alignment::Center),
Space::new().height(14),
row![
Space::new().width(field_width),
btn("Cancel", Message::QSelectClose, false),
btn(t!("Cancel"), Message::QSelectClose, false),
Space::new().width(8),
btn("Apply", Message::QSelectApply, true),
btn(t!("Apply"), Message::QSelectApply, true),
]
.align_y(iced::Alignment::Center),
]