feat(locale): localize full interface

This commit is contained in:
Hakan Seven committed 2026-08-02 22:09:25 +03:00
1 parent b90b3e3ecf
commit 3e6693b93f
205 files changed
+32369 -3810

No files matched your search

+10 -6
View File
@@ -1,8 +1,12 @@
use crate::app::Message;
use iced::widget::{button, column, container, row, text};
use iced::{Background, Element, Theme};
use crate::t;
fn info_row<'a>(label: &'static str, value: String) -> Element<'a, Message> {
fn info_row<'a>(
label: std::borrow::Cow<'static, str>,
value: String,
) -> Element<'a, Message> {
row![
text(label)
.size(11)
@@ -30,7 +34,7 @@ pub fn view_window<'a>() -> Element<'a, Message> {
.style(|theme: &Theme| iced::widget::text::Style {
color: Some(theme.palette().primary.base.color),
}),
text("CAD application for Architecture & Engineering")
text(t!("CAD application for Architecture & Engineering"))
.size(11)
.style(|theme: &Theme| iced::widget::text::Style {
color: Some(theme.palette().background.base.text.scale_alpha(0.68)),
@@ -49,16 +53,16 @@ pub fn view_window<'a>() -> Element<'a, Message> {
let info_block = container(
column![
info_row("Version", format!("v{}", version)),
info_row("Platform", os.to_string()),
info_row("Arch", arch.to_string()),
info_row(t!("Version"), format!("v{}", version)),
info_row(t!("Platform"), os.to_string()),
info_row(t!("Arch"), arch.to_string()),
]
.spacing(2)
.padding([12, 16]),
)
.style(container::bordered_box);
let copy_btn = button(text("Copy Info").size(11))
let copy_btn = button(text(t!("Copy Info")).size(11))
.on_press(Message::AboutCopyInfo)
.style(button::primary)
.padding([6, 16]);
+11 -11
View File
@@ -7,6 +7,7 @@
use crate::app::Message;
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
use iced::{Background, Element, Length, Theme};
use crate::t;
/// Which column of an alias row a text edit targets.
#[derive(Clone, Copy, Debug)]
@@ -29,11 +30,10 @@ pub fn view_window(
rows: &[(String, String)],
sizing: crate::ui::modal::ModalSizing,
) -> Element<'_, Message> {
let title = text("Command Aliases").size(15);
let hint = text(
"Type an alias and the command it runs (e.g. L → LINE). \
Apply to save to ocad.pgp; closing discards unapplied edits.",
)
let title = text(t!("Command Aliases")).size(15);
let hint = text(t!(
"Type an alias and the command it runs (e.g. L → LINE). Apply to save to ocad.pgp; closing discards unapplied edits."
))
.size(11)
.style(muted_style);
@@ -44,8 +44,8 @@ pub fn view_window(
let head = container(
row![
container(text("Alias").size(11).style(muted_style)).width(Length::Fixed(120.0)),
container(text("Command").size(11).style(muted_style)).width(sizing.width),
container(text(t!("Alias")).size(11).style(muted_style)).width(Length::Fixed(120.0)),
container(text(t!("Command")).size(11).style(muted_style)).width(sizing.width),
Space::new().width(Length::Fixed(30.0)),
]
.spacing(8),
@@ -54,12 +54,12 @@ pub fn view_window(
let mut list = column![].spacing(3);
for (idx, (alias, cmd)) in rows.iter().enumerate() {
let alias_box = text_input("alias", alias)
let alias_box = text_input(t!("alias").as_ref(), alias)
.on_input(move |v| Message::AliasEditorInput { idx, field: AliasField::Alias, value: v })
.size(13)
.padding([3, 6])
.width(Length::Fixed(120.0));
let cmd_box = text_input("command", cmd)
let cmd_box = text_input(t!("command").as_ref(), cmd)
.on_input(move |v| Message::AliasEditorInput { idx, field: AliasField::Command, value: v })
.size(13)
.padding([3, 6])
@@ -75,13 +75,13 @@ pub fn view_window(
);
}
let add = button(text("+ Add alias").size(12))
let add = button(text(t!("+ Add alias")).size(12))
.on_press(Message::AliasEditorAdd)
.padding([4, 10])
.style(button::secondary);
// Apply — primary action; commits the rows to ocad.pgp and stays open.
let apply = button(text("Apply").size(12))
let apply = button(text(t!("Apply")).size(12))
.on_press(Message::AliasEditorApply)
.padding([4, 16])
.style(button::primary);
+80 -34
View File
@@ -20,6 +20,9 @@ use iced::widget::{
button, checkbox, column, container, row, scrollable, text, text_input, Space,
};
use iced::{Background, Border, Element, Length, Theme};
use crate::t;
use std::borrow::Cow;
use std::fmt;
use crate::ui::properties::{lw_options, LwItem};
@@ -59,7 +62,8 @@ pub struct AttrRow {
}
/// Combined justification options (horizontal × vertical), matching the
/// standard text-justification list.
/// standard text-justification list. The stored labels are the English source
/// texts; `justify_label` / `justify_from_label` translate both directions.
pub const JUSTIFY: &[(&str, HorizontalAlignment, VerticalAlignment)] = &[
("Left", HorizontalAlignment::Left, VerticalAlignment::Baseline),
("Center", HorizontalAlignment::Center, VerticalAlignment::Baseline),
@@ -78,13 +82,15 @@ pub const JUSTIFY: &[(&str, HorizontalAlignment, VerticalAlignment)] = &[
("Bottom Right", HorizontalAlignment::Right, VerticalAlignment::Bottom),
];
/// The justification label for an (h, v) pair (defaults to "Left").
pub fn justify_label(h: HorizontalAlignment, v: VerticalAlignment) -> &'static str {
/// The justification label for an (h, v) pair (defaults to "Left"). Kept in
/// English (like the colour names) — the labels are standard CAD terms and
/// their keys would collide with ribbon tool labels such as "Right".
pub fn justify_label(h: HorizontalAlignment, v: VerticalAlignment) -> Cow<'static, str> {
JUSTIFY
.iter()
.find(|(_, jh, jv)| *jh == h && *jv == v)
.map(|(l, _, _)| *l)
.unwrap_or("Left")
.map(|(l, _, _)| Cow::Borrowed(*l))
.unwrap_or(Cow::Borrowed("Left"))
}
/// Resolve a justification label back to its (h, v) pair.
@@ -171,7 +177,7 @@ fn hdivider<'a>(width: Length) -> Element<'a, Message> {
/// One `label : widget` row with a fixed-width label column.
fn field_row<'a>(
label: &'a str,
label: Cow<'static, str>,
widget: Element<'a, Message>,
width: Length,
) -> Element<'a, Message> {
@@ -187,7 +193,7 @@ fn field_row<'a>(
/// A labelled editable text field for one of the selected row's string buffers.
fn edit_field<'a>(
label: &'a str,
label: Cow<'static, str>,
value: &'a str,
on_input: impl Fn(String) -> Message + 'a,
width: Length,
@@ -204,7 +210,7 @@ fn edit_field<'a>(
/// A labelled pick_list of owned string options.
fn pick_field<'a>(
label: &'a str,
label: Cow<'static, str>,
options: Vec<String>,
selected: Option<String>,
on_select: impl Fn(String) -> Message + 'a,
@@ -218,7 +224,47 @@ fn pick_field<'a>(
field_row(label, pl.into(), width)
}
fn tab_button<'a>(label: &'a str, this: AttrTab, active: AttrTab) -> Element<'a, Message> {
#[derive(Clone, Debug, PartialEq, Eq)]
struct LocalizedChoice {
raw: String,
label: String,
}
impl LocalizedChoice {
fn new(raw: String) -> Self {
let label = raw
.strip_prefix("Color ")
.map(|number| crate::tf!("Color {}", number).into_owned())
.unwrap_or_else(|| crate::i18n::translate(&raw).into_owned());
Self { raw, label }
}
}
impl fmt::Display for LocalizedChoice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.label)
}
}
/// A translated pick list which keeps the stored and emitted value unchanged.
fn localized_pick_field<'a>(
label: Cow<'static, str>,
options: Vec<String>,
selected: Option<String>,
on_select: impl Fn(String) -> Message + 'a,
width: Length,
) -> Element<'a, Message> {
let options = options.into_iter().map(LocalizedChoice::new).collect::<Vec<_>>();
let selected = selected.map(LocalizedChoice::new);
let pl = iced::widget::pick_list(selected, options, |value| value.to_string())
.on_select(move |choice: LocalizedChoice| on_select(choice.raw))
.text_size(13)
.padding([3, 6])
.width(width);
field_row(label, pl.into(), width)
}
fn tab_button<'a>(label: Cow<'static, str>, this: AttrTab, active: AttrTab) -> Element<'a, Message> {
let is_active = this == active;
button(text(label).size(11))
.padding([4, 12])
@@ -264,13 +310,13 @@ pub fn view_window<'a>(
let height = sizing.height;
// ── Top toolbar: block name on the left, Apply on the right ───────────
// Mirrors the style-manager windows (actions left, primary action right).
let apply = button(text("Apply").size(11))
let apply = button(text(t!("Apply")).size(11))
.padding([4, 14])
.on_press(Message::AttrEditorApply)
.style(button::primary);
let toolbar = container(
row![
text(format!("Block: {block}")).size(12).style(muted_style),
text(t!("Block: %{block}", block = block)).size(12).style(muted_style),
Space::new().width(width),
apply,
]
@@ -286,14 +332,14 @@ pub fn view_window<'a>(
.padding([5, 8]);
let tabs = row![
tab_button("Attribute", AttrTab::Attribute, tab),
tab_button("Text Options", AttrTab::TextOptions, tab),
tab_button("Properties", AttrTab::Properties, tab),
tab_button(t!("Attribute"), AttrTab::Attribute, tab),
tab_button(t!("Text Options"), AttrTab::TextOptions, tab),
tab_button(t!("Properties"), AttrTab::Properties, tab),
]
.spacing(2);
let body: Element<'_, Message> = if rows.is_empty() {
text("This block has no attributes.").size(13).style(muted_style).into()
text(t!("This block has no attributes.")).size(13).style(muted_style).into()
} else {
match tab {
AttrTab::Attribute => attribute_tab(rows, selected, width, height),
@@ -337,9 +383,9 @@ fn attribute_tab<'a>(
height: Length,
) -> Element<'a, Message> {
let head = row![
container(text("Tag").size(11).style(muted_style)).width(130),
container(text("Prompt").size(11).style(muted_style)).width(width),
container(text("Value").size(11).style(muted_style)).width(140),
container(text(t!("Tag")).size(11).style(muted_style)).width(130),
container(text(t!("Prompt")).size(11).style(muted_style)).width(width),
container(text(t!("Value")).size(11).style(muted_style)).width(140),
]
.spacing(6);
@@ -389,7 +435,7 @@ fn attribute_tab<'a>(
head,
scrollable(list).width(width).height(height),
Space::new().height(8),
field_row("Value:", value_box.into(), width),
field_row(t!("Value:"), value_box.into(), width),
]
.spacing(6)
.width(width)
@@ -409,24 +455,24 @@ fn text_options_tab<'a>(
} else {
Some(r.text_style.clone())
};
let justify_opts: Vec<String> = JUSTIFY.iter().map(|(l, _, _)| l.to_string()).collect();
let justify_opts: Vec<String> = JUSTIFY.iter().map(|(l, _, _)| (*l).to_string()).collect();
let justify_sel = Some(justify_label(r.h_align, r.v_align).to_string());
column![
pick_field("Text Style", styles, style_sel, |s| {
pick_field(t!("Text Style"), styles, style_sel, |s| {
Message::AttrEditorTextStyle(s)
}, width),
pick_field("Justification", justify_opts, justify_sel, |s| {
localized_pick_field(t!("Justification"), justify_opts, justify_sel, |s| {
Message::AttrEditorJustify(s)
}, width),
edit_field("Height", &r.height, Message::AttrEditorHeight, width),
edit_field("Rotation", &r.rotation, Message::AttrEditorRotation, width),
edit_field("Width Factor", &r.width_factor, Message::AttrEditorWidth, width),
edit_field("Oblique Angle", &r.oblique, Message::AttrEditorOblique, width),
edit_field(t!("Height"), &r.height, Message::AttrEditorHeight, width),
edit_field(t!("Rotation"), &r.rotation, Message::AttrEditorRotation, width),
edit_field(t!("Width Factor"), &r.width_factor, Message::AttrEditorWidth, width),
edit_field(t!("Oblique Angle"), &r.oblique, Message::AttrEditorOblique, width),
field_row(
"",
Cow::Borrowed(""),
checkbox(r.backwards)
.label("Backwards")
.label(t!("Backwards"))
.on_toggle(Message::AttrEditorBackwards)
.size(15)
.text_size(12)
@@ -434,9 +480,9 @@ fn text_options_tab<'a>(
width,
),
field_row(
"",
Cow::Borrowed(""),
checkbox(r.upside_down)
.label("Upside down")
.label(t!("Upside down"))
.on_toggle(Message::AttrEditorUpsideDown)
.size(15)
.text_size(12)
@@ -483,12 +529,12 @@ fn properties_tab<'a>(
.width(width);
column![
pick_field("Layer", layers, layer_sel, Message::AttrEditorLayer, width),
pick_field("Linetype", linetypes, lt_sel, Message::AttrEditorLinetype, width),
pick_field("Color", color_opts, color_sel, |s| {
pick_field(t!("Layer"), layers, layer_sel, Message::AttrEditorLayer, width),
localized_pick_field(t!("Linetype"), linetypes, lt_sel, Message::AttrEditorLinetype, width),
localized_pick_field(t!("Color"), color_opts, color_sel, |s| {
Message::AttrEditorColor(s)
}, width),
field_row("Lineweight", lw.into(), width),
field_row(t!("Lineweight"), lw.into(), width),
]
.spacing(8)
.width(width)
+12 -10
View File
@@ -1,6 +1,8 @@
use crate::app::Message;
use crate::t;
use iced::widget::{button, column, row, text, text_input, Space};
use iced::{Element, Fill, Length, Shrink};
use std::borrow::Cow;
pub const FIND_INPUT_ID: &str = "find-replace-search";
@@ -15,19 +17,19 @@ pub fn view_window<'a>(
} else {
Shrink
};
let find_input = text_input("Text to find", search)
let find_input = text_input("", search)
.id(iced::widget::Id::new(FIND_INPUT_ID))
.on_input(Message::FindReplaceSearchChanged)
.on_submit(Message::FindReplaceNext)
.padding([6, 8])
.size(13);
let replacement_input = text_input("Replacement text", replacement)
let replacement_input = text_input("", replacement)
.on_input(Message::FindReplaceReplacementChanged)
.padding([6, 8])
.size(13);
let enabled = !search.trim().is_empty();
let action = |label: &'static str, message: Message| {
let action = |label: Cow<'static, str>, message: Message| {
let button = button(text(label).size(12)).padding([6, 12]);
if enabled {
button.on_press(message)
@@ -38,29 +40,29 @@ pub fn view_window<'a>(
column![
row![
text("Find:").size(12).width(90),
text(t!("Find:")).size(12).width(90),
find_input.width(field_width),
]
.spacing(8)
.align_y(iced::Center),
row![
text("Replace with:").size(12).width(90),
text(t!("Replace with:")).size(12).width(90),
replacement_input.width(field_width),
]
.spacing(8)
.align_y(iced::Center),
text("Searches Text, MText, Attribute Definitions, and block attribute values.")
text(t!("Searches Text, MText, Attribute Definitions, and block attribute values."))
.size(11),
text(status).size(11),
row![
Space::new().width(field_width),
button(text("Close").size(12))
button(text(t!("Close")).size(12))
.on_press(Message::CloseModal)
.padding([6, 12])
.style(button::secondary),
action("Replace", Message::FindReplaceOne).style(button::secondary),
action("Replace All", Message::FindReplaceAll).style(button::danger),
action("Find Next", Message::FindReplaceNext).style(button::primary),
action(t!("Replace"), Message::FindReplaceOne).style(button::secondary),
action(t!("Replace All"), Message::FindReplaceAll).style(button::danger),
action(t!("Find Next"), Message::FindReplaceNext).style(button::primary),
]
.spacing(8)
.align_y(iced::Center),
+64 -62
View File
@@ -7,6 +7,8 @@ use iced::widget::{
button, checkbox, column, container, row, scrollable, text, text_input, Space,
};
use iced::{Background, Border, Element, Fill, Length, Theme};
use crate::t;
use std::borrow::Cow;
use std::fmt;
#[derive(Clone, PartialEq, Debug)]
@@ -15,7 +17,7 @@ struct TransparencyItem(Option<acadrust::types::Transparency>);
impl fmt::Display for TransparencyItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
None => write!(f, "Not set"),
None => write!(f, "{}", t!("Not set")),
Some(value) => write!(f, "{}%", (value.as_percent() * 100.0).round() as u8),
}
}
@@ -96,7 +98,7 @@ fn mask_summary(mask: LayerStateMask) -> String {
];
properties
.into_iter()
.filter_map(|(flag, label)| mask.contains(flag).then_some(label))
.filter_map(|(flag, label)| mask.contains(flag).then(|| t!(label)))
.collect::<Vec<_>>()
.join(", ")
}
@@ -126,7 +128,7 @@ pub fn view_window<'a>(
let is_selected =
selected.is_some_and(|selected| state.name.eq_ignore_ascii_case(selected));
let subtitle = if state.description.is_empty() {
format!("{} layers", state.layers.len())
t!("%{n} layers", n = state.layers.len()).to_string()
} else {
state.description.clone()
};
@@ -148,13 +150,13 @@ pub fn view_window<'a>(
let empty: Element<'_, Message> = container(
column![
text(if states.is_empty() {
"No layer states in this drawing"
t!("No layer states in this drawing")
} else {
"No matching layer states"
t!("No matching layer states")
})
.size(11)
.style(muted),
text("Choose New to capture the current layer settings.")
text(t!("Choose New to capture the current layer settings."))
.size(10)
.style(muted),
]
@@ -173,7 +175,7 @@ pub fn view_window<'a>(
let left = container(
column![
text_input("Search layer states…", filter)
text_input(t!("Search layer states…").as_ref(), filter)
.on_input(Message::LayerStateManagerFilter)
.size(11)
.padding([5, 8]),
@@ -204,14 +206,14 @@ pub fn view_window<'a>(
let details = if let Some(state) = selected_state {
column![
text("Saved state details").size(13),
text(t!("Saved state details")).size(13),
row![
text("Layers").size(10).style(muted).width(92),
text(t!("Layers")).size(10).style(muted).width(92),
text(state.layers.len().to_string()).size(11),
]
.spacing(8),
row![
text("Current layer").size(10).style(muted).width(92),
text(t!("Current layer")).size(10).style(muted).width(92),
text(if state.current_layer.is_empty() {
"—".to_string()
} else {
@@ -220,14 +222,14 @@ pub fn view_window<'a>(
.size(11),
]
.spacing(8),
text("Restored properties").size(10).style(muted),
text(t!("Restored properties")).size(10).style(muted),
text(mask_summary(state.mask)).size(11),
]
.spacing(7)
} else {
column![
text("New layer state").size(13),
text("Save captures the current settings of every layer in the drawing.")
text(t!("New layer state")).size(13),
text(t!("Save captures the current settings of every layer in the drawing."))
.size(11)
.style(muted),
]
@@ -235,31 +237,31 @@ pub fn view_window<'a>(
};
let restore = if selected_state.is_some() {
button(text("Restore").size(11))
button(text(t!("Restore")).size(11))
.on_press(Message::LayerStateManagerRestore)
.style(button_style(true))
} else {
button(text("Restore").size(11)).style(button_style(true))
button(text(t!("Restore")).size(11)).style(button_style(true))
};
let delete = if selected_state.is_some() {
button(text("Delete").size(11))
button(text(t!("Delete")).size(11))
.on_press(Message::LayerStateManagerDelete)
.style(button::danger)
} else {
button(text("Delete").size(11)).style(button::danger)
button(text(t!("Delete")).size(11)).style(button::danger)
};
let edit = if selected_state.is_some() {
button(text("Edit").size(11))
button(text(t!("Edit")).size(11))
.on_press(Message::LayerStateManagerEdit)
.style(button_style(false))
} else {
button(text("Edit").size(11)).style(button_style(false))
button(text(t!("Edit")).size(11)).style(button_style(false))
};
let right = container(
column![
row![
button(text("New").size(11))
button(text(t!("New")).size(11))
.on_press(Message::LayerStateManagerNew)
.style(button_style(false))
.padding([5, 12]),
@@ -271,27 +273,27 @@ pub fn view_window<'a>(
.spacing(6)
.align_y(iced::Center),
divider(sizing.width),
text("Name").size(10).style(muted),
text_input("Layer state name", name)
text(t!("Name")).size(10).style(muted),
text_input(t!("Layer state name").as_ref(), name)
.on_input(Message::LayerStateManagerName)
.on_submit(Message::LayerStateManagerSave)
.size(11)
.padding([5, 8]),
text("Description").size(10).style(muted),
text_input("Optional description", description)
text(t!("Description")).size(10).style(muted),
text_input(t!("Optional description").as_ref(), description)
.on_input(Message::LayerStateManagerDescription)
.size(11)
.padding([5, 8]),
Space::new().height(8),
details,
Space::new().height(sizing.height),
text("Layer states are stored inside the drawing and remain available after reopening it.")
text(t!("Layer states are stored inside the drawing and remain available after reopening it."))
.size(10)
.style(muted),
button(text(if selected_state.is_some() {
"Update from Drawing"
t!("Update from Drawing")
} else {
"Save New State"
t!("Save New State")
})
.size(11))
.on_press(Message::LayerStateManagerSave)
@@ -328,7 +330,7 @@ fn mask_for(property: LayerStateProperty) -> LayerStateMask {
fn mask_button<'a>(
state: &LayerState,
label: &'a str,
label: Cow<'static, str>,
property: LayerStateProperty,
) -> Element<'a, Message> {
let enabled = state.mask.contains(mask_for(property));
@@ -365,17 +367,17 @@ fn bool_cell<'a>(
fn editor_header<'a>() -> Element<'a, Message> {
container(
row![
text("Layer").size(10).style(muted).width(Length::Fixed(170.0)),
text("On").size(10).style(muted).width(Length::Fixed(44.0)),
text("Freeze").size(10).style(muted).width(Length::Fixed(54.0)),
text("Lock").size(10).style(muted).width(Length::Fixed(44.0)),
text("Plot").size(10).style(muted).width(Length::Fixed(44.0)),
text("New VP").size(10).style(muted).width(Length::Fixed(54.0)),
text("Color").size(10).style(muted).width(Length::Fixed(135.0)),
text("Linetype").size(10).style(muted).width(Length::Fixed(150.0)),
text("Lineweight").size(10).style(muted).width(Length::Fixed(115.0)),
text("Plot style").size(10).style(muted).width(Length::Fixed(135.0)),
text("Transparency").size(10).style(muted).width(Length::Fixed(105.0)),
text(t!("Layer")).size(10).style(muted).width(Length::Fixed(170.0)),
text(t!("On")).size(10).style(muted).width(Length::Fixed(44.0)),
text(t!("Freeze")).size(10).style(muted).width(Length::Fixed(54.0)),
text(t!("Lock")).size(10).style(muted).width(Length::Fixed(44.0)),
text(t!("Plot")).size(10).style(muted).width(Length::Fixed(44.0)),
text(t!("New VP")).size(10).style(muted).width(Length::Fixed(54.0)),
text(t!("Color")).size(10).style(muted).width(Length::Fixed(135.0)),
text(t!("Linetype")).size(10).style(muted).width(Length::Fixed(150.0)),
text(t!("Lineweight")).size(10).style(muted).width(Length::Fixed(115.0)),
text(t!("Plot style")).size(10).style(muted).width(Length::Fixed(135.0)),
text(t!("Transparency")).size(10).style(muted).width(Length::Fixed(105.0)),
]
.spacing(4)
.align_y(iced::Center),
@@ -455,7 +457,7 @@ fn editor_layer_row<'a>(
.text_size(11)
.padding([3, 5])
.width(Length::Fixed(115.0)),
text_input("Default", &layer.plot_style)
text_input(t!("Default").as_ref(), &layer.plot_style)
.on_input(move |value| Message::LayerStateEditorLayerPlotStyle(index, value))
.size(11)
.padding([3, 5])
@@ -492,16 +494,16 @@ pub fn view_editor<'a>(
sizing: crate::ui::modal::ModalSizing,
) -> Element<'a, Message> {
let properties = [
("On / Off", LayerStateProperty::On),
("Freeze", LayerStateProperty::Frozen),
("Lock", LayerStateProperty::Locked),
("Plot", LayerStateProperty::Plot),
("New VP", LayerStateProperty::NewViewport),
("Color", LayerStateProperty::Color),
("Linetype", LayerStateProperty::LineType),
("Lineweight", LayerStateProperty::LineWeight),
("Plot style", LayerStateProperty::PlotStyle),
("Transparency", LayerStateProperty::Transparency),
(t!("On / Off"), LayerStateProperty::On),
(t!("Freeze"), LayerStateProperty::Frozen),
(t!("Lock"), LayerStateProperty::Locked),
(t!("Plot"), LayerStateProperty::Plot),
(t!("New VP"), LayerStateProperty::NewViewport),
(t!("Color"), LayerStateProperty::Color),
(t!("Linetype"), LayerStateProperty::LineType),
(t!("Lineweight"), LayerStateProperty::LineWeight),
(t!("Plot style"), LayerStateProperty::PlotStyle),
(t!("Transparency"), LayerStateProperty::Transparency),
];
let mask_controls = properties
.into_iter()
@@ -534,14 +536,14 @@ pub fn view_editor<'a>(
container(
column![
row![
text("Name").size(10).style(muted),
text_input("Layer state name", &state.name)
text(t!("Name")).size(10).style(muted),
text_input(t!("Layer state name").as_ref(), &state.name)
.on_input(Message::LayerStateEditorName)
.size(11)
.padding([3, 6])
.width(Length::Fixed(220.0)),
text("Description").size(10).style(muted),
text_input("Optional description", &state.description)
text(t!("Description")).size(10).style(muted),
text_input(t!("Optional description").as_ref(), &state.description)
.on_input(Message::LayerStateEditorDescription)
.size(11)
.padding([3, 6])
@@ -550,11 +552,11 @@ pub fn view_editor<'a>(
.spacing(8)
.align_y(iced::Center),
row![
text(format!("{} saved layers", state.layers.len()))
text(t!("%{n} saved layers", n = state.layers.len()))
.size(10)
.style(muted),
Space::new().width(sizing.width),
text("Current layer").size(10).style(muted),
text(t!("Current layer")).size(10).style(muted),
iced::widget::pick_list(
Some(state.current_layer.clone()),
layer_names,
@@ -568,12 +570,12 @@ pub fn view_editor<'a>(
.spacing(8)
.align_y(iced::Center),
divider(sizing.width),
text("Properties restored by this state").size(10).style(muted),
text(t!("Properties restored by this state")).size(10).style(muted),
mask_controls,
row![
text("Saved layer values").size(12),
text(t!("Saved layer values")).size(12),
Space::new().width(sizing.width),
text_input("Search layers…", filter)
text_input(t!("Search layers…").as_ref(), filter)
.on_input(Message::LayerStateEditorFilter)
.size(11)
.padding([4, 7])
@@ -595,15 +597,15 @@ pub fn view_editor<'a>(
..Default::default()
}),
row![
text("Changes affect the saved state only; the drawing is unchanged until Restore.")
text(t!("Changes affect the saved state only; the drawing is unchanged until Restore."))
.size(10)
.style(muted),
Space::new().width(sizing.width),
button(text("Cancel").size(11))
button(text(t!("Cancel")).size(11))
.on_press(Message::LayerStateEditorCancel)
.style(button_style(false))
.padding([5, 12]),
button(text("Save Changes").size(11))
button(text(t!("Save Changes")).size(11))
.on_press(Message::LayerStateEditorSave)
.style(button_style(true))
.padding([5, 12]),
+20 -18
View File
@@ -13,6 +13,8 @@ use iced::widget::{
};
use iced::Padding;
use iced::{Background, Border, Color, Element, Fill, Length, Theme};
use crate::t;
use std::borrow::Cow;
// ── Per-viewport column descriptor ───────────────────────────────────────────
@@ -330,22 +332,22 @@ impl LayerPanel {
// ── Toolbar ───────────────────────────────────────────────────────
let toolbar = container(
row![
toolbar_btn(crate::ui::icons::PLUS, "New", Message::LayerNew),
toolbar_btn(crate::ui::icons::PLUS, t!("New"), Message::LayerNew),
toolbar_btn_cond(
crate::ui::icons::TRASH,
"Delete",
t!("Delete"),
Message::LayerDelete,
has_sel && !sel_is_zero,
),
toolbar_btn_cond(
crate::ui::icons::CHECK,
"Set Current",
t!("Set Current"),
Message::LayerSetCurrent,
has_sel,
),
iced::widget::Space::new().width(sizing.width),
// Search box: filters rows by name as the user types (#343).
text_input("Search…", &self.filter)
text_input(t!("Search…").as_ref(), &self.filter)
.on_input(Message::LayerManagerFilterChanged)
.size(FONT_SZ)
.padding([3, 6])
@@ -368,8 +370,8 @@ impl LayerPanel {
let sc = self.sort_col;
let sa = self.sort_asc;
let mut header_row = row![
text("Status").size(10).style(muted_style).width(50),
sortable_header("Name", LayerSortCol::Name, Length::Fixed(name_col_w), sc, sa),
text(t!("Status")).size(10).style(muted_style).width(50),
sortable_header(t!("Name"), LayerSortCol::Name, Length::Fixed(name_col_w), sc, sa),
// Draggable divider: adjusts the Name column width (#359).
iced::widget::mouse_area(
container(iced::widget::Space::new().width(2).height(14)).style(
@@ -383,14 +385,14 @@ impl LayerPanel {
)
.on_press(Message::LayerNameColGrab)
.interaction(iced::mouse::Interaction::ResizingHorizontally),
sortable_header("On", LayerSortCol::On, Length::Fixed(COL_ICON), sc, sa),
sortable_header("Freeze", LayerSortCol::Freeze, Length::Fixed(COL_ICON), sc, sa),
sortable_header("Lock", LayerSortCol::Lock, Length::Fixed(COL_ICON), sc, sa),
sortable_header("Color", LayerSortCol::Color, Length::Fixed(COL_COLOR), sc, sa),
sortable_header("Linetype", LayerSortCol::Linetype, Length::Fixed(COL_LT), sc, sa),
sortable_header("Lineweight", LayerSortCol::Lineweight, Length::Fixed(COL_LW), sc, sa),
sortable_header(t!("On"), LayerSortCol::On, Length::Fixed(COL_ICON), sc, sa),
sortable_header(t!("Freeze"), LayerSortCol::Freeze, Length::Fixed(COL_ICON), sc, sa),
sortable_header(t!("Lock"), LayerSortCol::Lock, Length::Fixed(COL_ICON), sc, sa),
sortable_header(t!("Color"), LayerSortCol::Color, Length::Fixed(COL_COLOR), sc, sa),
sortable_header(t!("Linetype"), LayerSortCol::Linetype, Length::Fixed(COL_LT), sc, sa),
sortable_header(t!("Lineweight"), LayerSortCol::Lineweight, Length::Fixed(COL_LW), sc, sa),
sortable_header(
"Transparency",
t!("Transparency"),
LayerSortCol::Transparency,
Length::Fixed(COL_TRANS),
sc,
@@ -535,7 +537,7 @@ fn color_sort_key(c: AcadColor) -> u32 {
/// A clickable column header that sorts the table by `col`. Shows an up/down
/// SVG arrow when it is the active sort column (#133).
fn sortable_header<'a>(
label: &'a str,
label: Cow<'static, str>,
col: LayerSortCol,
width: Length,
active: Option<LayerSortCol>,
@@ -566,7 +568,7 @@ fn sortable_header<'a>(
// ── Toolbar buttons ───────────────────────────────────────────────────────
fn toolbar_btn<'a>(icon: &'static [u8], label: &'a str, msg: Message) -> Element<'a, Message> {
fn toolbar_btn<'a>(icon: &'static [u8], label: Cow<'static, str>, msg: Message) -> Element<'a, Message> {
button(
row![
crate::ui::icons::themed(icon, 12.0),
@@ -601,7 +603,7 @@ fn toolbar_btn<'a>(icon: &'static [u8], label: &'a str, msg: Message) -> Element
fn toolbar_btn_cond<'a>(
icon: &'static [u8],
label: &'a str,
label: Cow<'static, str>,
msg: Message,
enabled: bool,
) -> Element<'a, Message> {
@@ -791,7 +793,7 @@ fn layer_row<'a>(
let lt_cell: Element<'_, Message> = if let Some(state) = lt_combo {
combo_box(
state,
"linetype",
t!("linetype").as_ref(),
Some(&cur_lt_item),
|item: LinetypeItem| Message::LayerLinetypeSet(item.name),
)
@@ -816,7 +818,7 @@ fn layer_row<'a>(
// Lineweight cell
let cur_lw_item = LwItem(layer.lineweight);
let lw_cell: Element<'_, Message> = if let Some(state) = lw_combo_state {
combo_box(state, "lineweight", Some(&cur_lw_item), |item: LwItem| {
combo_box(state, t!("lineweight").as_ref(), Some(&cur_lw_item), |item: LwItem| {
Message::LayerLineweightSet(item.0)
})
.size(FONT_SZ)
+16 -15
View File
@@ -3,6 +3,7 @@
use crate::app::Message;
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
use iced::{Background, Element, Theme};
use crate::t;
fn muted_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
@@ -74,11 +75,11 @@ pub fn view_window<'a>(
// ── Toolbar ───────────────────────────────────────────────────────────
let toolbar = container(
row![
button(text("New Layout").size(11))
button(text(t!("New Layout")).size(11))
.on_press(Message::LayoutManagerNew)
.style(btn_s(false))
.padding([4, 10]),
button(text("Delete").size(11))
button(text(t!("Delete")).size(11))
.on_press(Message::LayoutManagerDelete)
.style(move |theme: &Theme, status| {
if is_model {
@@ -92,7 +93,7 @@ pub fn view_window<'a>(
button(
row![
crate::ui::icons::themed_arrow_left(9.0),
text("Move Left").size(11),
text(t!("Move Left")).size(11),
]
.spacing(4)
.align_y(iced::Center),
@@ -102,7 +103,7 @@ pub fn view_window<'a>(
.padding([4, 8]),
button(
row![
text("Move Right").size(11),
text(t!("Move Right")).size(11),
crate::ui::icons::themed_arrow_right(9.0),
]
.spacing(4)
@@ -111,7 +112,7 @@ pub fn view_window<'a>(
.on_press(Message::LayoutManagerMoveRight)
.style(btn_s(false))
.padding([4, 8]),
button(text("Set Current").size(11))
button(text(t!("Set Current")).size(11))
.on_press(Message::LayoutManagerSetCurrent)
.style(btn_s(true))
.padding([4, 10]),
@@ -151,7 +152,7 @@ pub fn view_window<'a>(
let layout_list = container(
column![
text("Layouts").size(10).style(muted_style),
text(t!("Layouts")).size(10).style(muted_style),
container(scrollable(column(list_items).spacing(2)).height(sizing.height))
.style(container::bordered_box)
.width(sizing.width)
@@ -174,24 +175,24 @@ pub fn view_window<'a>(
let details = container(
column![
text(if is_model {
"Model Space"
t!("Model Space")
} else {
"Paper Space Layout"
t!("Paper Space Layout")
})
.size(13),
Space::new().height(8),
row![
text("Name:").size(11).style(muted_style).width(80),
text(t!("Name:")).size(11).style(muted_style).width(80),
text(selected).size(11),
]
.spacing(8)
.align_y(iced::Center),
row![
text("Status:").size(11).style(muted_style).width(80),
text(t!("Status:")).size(11).style(muted_style).width(80),
text(if selected == current.as_str() {
"Active"
t!("Active")
} else {
"Inactive"
t!("Inactive")
})
.size(11)
.style(move |theme: &Theme| {
@@ -205,14 +206,14 @@ pub fn view_window<'a>(
.spacing(8)
.align_y(iced::Center),
Space::new().height(16),
text("Rename").size(10).style(muted_style),
text(t!("Rename")).size(10).style(muted_style),
row![
text_input("New name…", rename_buf)
text_input(t!("New name…").as_ref(), rename_buf)
.on_input(Message::LayoutManagerRenameBuf)
.on_submit(Message::LayoutManagerRenameCommit)
.size(11)
.padding([4, 8]),
button(text("OK").size(11))
button(text(t!("OK")).size(11))
.on_press(Message::LayoutManagerRenameCommit)
.style(btn_s(true))
.padding([4, 10]),
+11 -9
View File
@@ -6,6 +6,8 @@
use iced::time::Instant;
use iced::widget::{button, column, container, row, stack, text, Space};
use iced::{Background, Border, Element, Fill, Length, Theme};
use crate::t;
use std::borrow::Cow;
use std::sync::atomic::Ordering;
use crate::app::{
@@ -17,14 +19,14 @@ const CARD_WIDTH: f32 = 420.0;
const BAR_TRACK_WIDTH: f32 = 380.0;
const BAR_TRACK_HEIGHT: f32 = 6.0;
fn phase_label(phase: u8) -> &'static str {
fn phase_label(phase: u8) -> Cow<'static, str> {
match phase {
OPEN_PHASE_READING => "Reading file…",
OPEN_PHASE_PARSING => "Parsing entities…",
OPEN_PHASE_XREF => "Loading references…",
OPEN_PHASE_CACHING => "Building scene caches…",
OPEN_PHASE_FINALIZING => "Finalizing…",
_ => "Working…",
OPEN_PHASE_READING => t!("Reading file…"),
OPEN_PHASE_PARSING => t!("Parsing entities…"),
OPEN_PHASE_XREF => t!("Loading references…"),
OPEN_PHASE_CACHING => t!("Building scene caches…"),
OPEN_PHASE_FINALIZING => t!("Finalizing…"),
_ => t!("Working…"),
}
}
@@ -101,7 +103,7 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag
.into();
// ── Card body ────────────────────────────────────────────────────────
let title = text("Opening file").size(15);
let title = text(t!("Opening file")).size(15);
let name_line = text(format!(
"{} ({})",
@@ -123,7 +125,7 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag
color: Some(theme.palette().primary.base.color),
});
let cancel_btn: Element<'_, Message> = button(text("Cancel").size(12))
let cancel_btn: Element<'_, Message> = button(text(t!("Cancel")).size(12))
.on_press(Message::OpenCancel)
.style(button::danger)
.padding([4, 14])
+19 -2
View File
@@ -4,6 +4,22 @@ use iced::widget::{
button, column, container, row, scrollable, text, text_input, Space,
};
use iced::{Background, Border, Element, Theme};
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
struct ThemeChoice(String);
impl fmt::Display for ThemeChoice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self.0.as_str() {
"Light" => crate::t!("Light"),
"Dark" => crate::t!("Dark"),
"Custom" => crate::t!("Custom"),
_ => std::borrow::Cow::Borrowed(self.0.as_str()),
};
f.write_str(label.as_ref())
}
}
pub fn view_window<'a>(
default_save_format: &'a str,
@@ -21,8 +37,9 @@ pub fn view_window<'a>(
.iter()
.map(ToString::to_string)
.chain(std::iter::once("Custom".to_string()))
.map(ThemeChoice)
.collect::<Vec<_>>();
let selected_theme = Some(ui_theme.name.clone());
let selected_theme = Some(ThemeChoice(ui_theme.name.clone()));
let palette = ui_theme.palette.to_iced();
let colors = [
@@ -110,7 +127,7 @@ pub fn view_window<'a>(
theme_options,
|value| value.to_string(),
)
.on_select(Message::OptionsThemeChanged)
.on_select(|choice| Message::OptionsThemeChanged(choice.0))
.width(sizing.width),
]
.spacing(12)
+139 -83
View File
@@ -11,6 +11,9 @@ use iced::widget::{
Space,
};
use iced::{Background, Border, Element, Fit, Length, Theme};
use crate::t;
use std::borrow::Cow;
use std::fmt;
/// Sentinel entries in the printer dropdown (not real printer names).
pub const OUT_DEFAULT: &str = "System default printer";
@@ -22,6 +25,35 @@ pub const SETUP_NONE: &str = "<none>";
pub const SETUP_PREV: &str = "<previous>";
pub const STYLE_NONE: &str = "<none>";
/// A plot dropdown value keeps its persisted/raw value separate from the
/// localized label shown by Iced. Printer names, scale names, paper sizes, and
/// style-table file names remain verbatim; built-in choices use the catalog.
#[derive(Clone, Debug, Eq, PartialEq)]
struct PlotChoice {
raw: String,
localized: bool,
}
impl PlotChoice {
fn raw(value: impl Into<String>) -> Self {
Self { raw: value.into(), localized: false }
}
fn localized(value: impl Into<String>) -> Self {
Self { raw: value.into(), localized: true }
}
}
impl fmt::Display for PlotChoice {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.localized {
formatter.write_str(crate::i18n::translate(&self.raw).as_ref())
} else {
formatter.write_str(&self.raw)
}
}
}
/// One of the many boolean plot options (folded into a single message so the
/// dialog needn't carry a variant per checkbox).
#[derive(Debug, Clone, Copy)]
@@ -280,7 +312,7 @@ fn hdivider<'a>(width: Length) -> Element<'a, Message> {
.into()
}
fn section_label<'a>(s: &'static str) -> Element<'a, Message> {
fn section_label<'a>(s: Cow<'static, str>) -> Element<'a, Message> {
text(s).size(11).style(muted_style).into()
}
@@ -314,7 +346,12 @@ fn setup_row<'a>(
.into();
}
let is_selected = name == selected;
let cell = container(text(name.to_string()).size(11))
let display_name = if name == SETUP_NONE || name == SETUP_PREV {
crate::i18n::translate(name)
} else {
Cow::Owned(name.to_string())
};
let cell = container(text(display_name).size(11))
.padding([4, 8])
.width(Fit)
.style(move |theme: &Theme| {
@@ -334,14 +371,14 @@ fn setup_row<'a>(
/// A `label : dropdown` row. `ctor` turns the picked string into a dialog
/// message.
fn drop_row<'a>(
label: &'a str,
options: Vec<String>,
selected: Option<String>,
label: Cow<'static, str>,
options: Vec<PlotChoice>,
selected: Option<PlotChoice>,
ctor: fn(String) -> PlotDlgMsg,
width: Length,
) -> Element<'a, Message> {
let pl = iced::widget::pick_list(selected, options, |value| value.to_string())
.on_select(move |s| Message::PlotDlg(ctor(s)))
.on_select(move |choice| Message::PlotDlg(ctor(choice.raw)))
.text_size(12)
.padding([3, 6])
.width(width);
@@ -352,9 +389,9 @@ fn drop_row<'a>(
}
fn drop_row_enabled<'a>(
label: &'a str,
options: Vec<String>,
selected: Option<String>,
label: Cow<'static, str>,
options: Vec<PlotChoice>,
selected: Option<PlotChoice>,
ctor: fn(String) -> PlotDlgMsg,
width: Length,
enabled: bool,
@@ -364,7 +401,7 @@ fn drop_row_enabled<'a>(
}
row![
text(label).size(11).style(muted_style).width(92),
container(text(selected.unwrap_or_default()).size(12).style(muted_style))
container(text(selected.map(|choice| choice.to_string()).unwrap_or_default()).size(12).style(muted_style))
.padding([4, 7])
.width(width),
]
@@ -375,7 +412,7 @@ fn drop_row_enabled<'a>(
/// A `label : text field` row.
fn field_row<'a>(
label: &'a str,
label: Cow<'static, str>,
value: &'a str,
ctor: fn(String) -> PlotDlgMsg,
width: u16,
@@ -394,7 +431,7 @@ fn field_row<'a>(
}
fn field_row_enabled<'a>(
label: &'a str,
label: Cow<'static, str>,
value: &'a str,
ctor: fn(String) -> PlotDlgMsg,
width: u16,
@@ -415,7 +452,7 @@ fn field_row_enabled<'a>(
}
/// A single option checkbox bound to a `PlotFlag`.
fn check<'a>(label: &'a str, on: bool, flag: PlotFlag) -> Element<'a, Message> {
fn check<'a>(label: Cow<'static, str>, on: bool, flag: PlotFlag) -> Element<'a, Message> {
checkbox(on)
.label(label)
.on_toggle(move |_| Message::PlotDlg(PlotDlgMsg::Flag(flag)))
@@ -425,7 +462,7 @@ fn check<'a>(label: &'a str, on: bool, flag: PlotFlag) -> Element<'a, Message> {
}
fn check_enabled<'a>(
label: &'a str,
label: Cow<'static, str>,
on: bool,
flag: PlotFlag,
enabled: bool,
@@ -447,8 +484,8 @@ fn panel<'a>(content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
.into()
}
fn strs(items: &[&str]) -> Vec<String> {
items.iter().map(|s| s.to_string()).collect()
fn choices(items: &[&str]) -> Vec<PlotChoice> {
items.iter().map(|&value| PlotChoice::localized(value)).collect()
}
pub fn view_window(
@@ -457,7 +494,7 @@ pub fn view_window(
) -> Element<'_, Message> {
let width = sizing.width;
let height = sizing.height;
let action = if s.to_file { "Export PDF" } else { "Print" };
let action = if s.to_file { t!("Export PDF") } else { t!("Print") };
let is_special = s.selected_setup == SETUP_NONE || s.selected_setup == SETUP_PREV;
let sel_is_layout = s.selected_setup.len() >= 2
&& s.selected_setup.starts_with('*')
@@ -478,7 +515,7 @@ pub fn view_window(
.map(|name| setup_row(name, &s.selected_setup, renaming, rename_buf))
.collect();
let list_body: Element<'_, Message> = if rows.is_empty() {
container(text("(no page setups)").size(11).style(muted_style))
container(text(t!("(no page setups)")).size(11).style(muted_style))
.padding([6, 8])
.into()
} else {
@@ -494,7 +531,7 @@ pub fn view_window(
.size(11)
.padding([4, 8])
.width(Length::Fill),
button(text("Save").size(11))
button(text(t!("Save")).size(11))
.on_press(Message::PlotDlg(PlotDlgMsg::NameCommit))
.style(btn(true))
.padding([4, 8]),
@@ -510,7 +547,7 @@ pub fn view_window(
};
let list_panel = container(
column![
text("Page setups").size(10).style(muted_style),
text(t!("Page setups")).size(10).style(muted_style),
new_name,
container(list_body)
.style(|theme: &Theme| {
@@ -541,20 +578,20 @@ pub fn view_window(
left: 12.0,
});
let mut copy_button = button(text("Copy").size(11))
let mut copy_button = button(text(t!("Copy")).size(11))
.style(btn(false))
.padding([4, 12]);
if can_copy {
copy_button = copy_button.on_press(Message::PlotDlg(PlotDlgMsg::CopySetup));
}
let mut delete_button = button(text("Delete").size(11))
let mut delete_button = button(text(t!("Delete")).size(11))
.style(btn(false))
.padding([4, 12]);
if is_named {
delete_button = delete_button.on_press(Message::PlotDlg(PlotDlgMsg::DeleteSetup));
}
let left_bar = row![
button(text("New").size(11))
button(text(t!("New")).size(11))
.on_press(Message::PlotDlg(PlotDlgMsg::NewSetup))
.style(btn(false))
.padding([4, 12]),
@@ -564,17 +601,20 @@ pub fn view_window(
.spacing(4);
// ── Printer / plotter ─────────────────────────────────────────────────
let mut printer_opts = vec![OUT_DEFAULT.to_string()];
printer_opts.extend(s.printers.iter().cloned());
printer_opts.push(OUT_PDF.to_string());
let mut printer_opts = vec![PlotChoice::localized(OUT_DEFAULT)];
printer_opts.extend(s.printers.iter().cloned().map(PlotChoice::raw));
printer_opts.push(PlotChoice::localized(OUT_PDF));
let printer_sel = if s.to_file {
Some(OUT_PDF.to_string())
Some(PlotChoice::localized(OUT_PDF))
} else {
Some(s.printer.clone().unwrap_or_else(|| OUT_DEFAULT.to_string()))
Some(match &s.printer {
Some(printer) => PlotChoice::raw(printer.clone()),
None => PlotChoice::localized(OUT_DEFAULT),
})
};
let paper_opts: Vec<String> = PaperSize::ALL.iter().map(|p| p.label().to_string()).collect();
let paper_note: Element<'_, Message> = if s.area == "Layout" {
text("Layout plots the current sheet using the selected paper size.")
text(t!("Layout plots the current sheet using the selected paper size."))
.size(10)
.style(muted_style)
.width(width)
@@ -583,9 +623,9 @@ pub fn view_window(
Space::new().height(0).into()
};
let mut output_row = row![
text("Output").size(11).style(muted_style).width(92),
text(t!("Output")).size(11).style(muted_style).width(92),
iced::widget::pick_list(printer_sel, printer_opts, |value| value.to_string())
.on_select(|value| Message::PlotDlg(PlotDlgMsg::Printer(value)))
.on_select(|choice| Message::PlotDlg(PlotDlgMsg::Printer(choice.raw)))
.text_size(12)
.padding([3, 6])
.width(Length::Fill),
@@ -594,7 +634,7 @@ pub fn view_window(
.align_y(iced::Center);
if !s.to_file {
output_row = output_row.push(
button(text("Properties…").size(11))
button(text(t!("Properties…")).size(11))
.on_press(Message::PlotDlg(PlotDlgMsg::PrinterProperties))
.style(btn(false))
.padding([4, 8]),
@@ -603,11 +643,11 @@ pub fn view_window(
let copies_row: Element<'_, Message> = if s.to_file {
Space::new().height(0).into()
} else {
field_row("Copies", &s.copies, PlotDlgMsg::Copies, 60)
field_row(t!("Copies"), &s.copies, PlotDlgMsg::Copies, 60)
};
let printer_panel = panel(
column![
section_label("Printer / plotter"),
section_label(t!("Printer / plotter")),
output_row,
copies_row,
]
@@ -616,19 +656,29 @@ pub fn view_window(
// ── Paper, area, offset, scale ────────────────────────────────────────
let paper_panel = panel(column![
section_label("Paper"),
drop_row("Size", paper_opts, Some(s.paper.clone()), PlotDlgMsg::Paper, width),
section_label(t!("Paper")),
drop_row(
t!("Size"),
paper_opts.into_iter().map(PlotChoice::raw).collect(),
Some(PlotChoice::raw(s.paper.clone())),
PlotDlgMsg::Paper,
width,
),
paper_note,
].spacing(7));
let mut area_options = strs(&["Extents", "Display", "Window"]);
let mut area_options = choices(&["Extents", "Display", "Window"]);
if s.paper_space {
area_options.insert(0, "Layout".to_string());
area_options.insert(0, PlotChoice::localized("Layout"));
}
let mut area_row = row![
text("What to plot").size(11).style(muted_style).width(92),
iced::widget::pick_list(Some(s.area.clone()), area_options, |value| value.to_string())
.on_select(|value| Message::PlotDlg(PlotDlgMsg::Area(value)))
text(t!("What to plot")).size(11).style(muted_style).width(92),
iced::widget::pick_list(
Some(PlotChoice::localized(s.area.clone())),
area_options,
|value| value.to_string(),
)
.on_select(|choice| Message::PlotDlg(PlotDlgMsg::Area(choice.raw)))
.text_size(12)
.padding([3, 6])
.width(Length::Fill),
@@ -637,7 +687,7 @@ pub fn view_window(
.align_y(iced::Center);
if s.area == "Window" {
area_row = area_row.push(
button(text("Pick…").size(11))
button(text(t!("Pick…")).size(11))
.on_press(Message::PlotDlg(PlotDlgMsg::PickWindow))
.style(btn(false))
.padding([4, 10]),
@@ -645,35 +695,41 @@ pub fn view_window(
}
let common_area = s.area != "Layout";
let area_panel = panel(column![
section_label("Plot area"),
section_label(t!("Plot area")),
area_row,
section_label("Plot offset"),
section_label(t!("Plot offset")),
column![
field_row_enabled("X (mm)", &s.offset_x, PlotDlgMsg::OffsetX, 70, common_area && !s.center),
field_row_enabled("Y (mm)", &s.offset_y, PlotDlgMsg::OffsetY, 70, common_area && !s.center),
field_row_enabled(t!("X (mm)"), &s.offset_x, PlotDlgMsg::OffsetX, 70, common_area && !s.center),
field_row_enabled(t!("Y (mm)"), &s.offset_y, PlotDlgMsg::OffsetY, 70, common_area && !s.center),
]
.spacing(7),
check_enabled("Center the plot", s.center, PlotFlag::Center, common_area),
check_enabled(t!("Center the plot"), s.center, PlotFlag::Center, common_area),
].spacing(7));
let scale_options = s.scales.iter().map(|(name, _)| name.clone()).collect();
let scale_options = s
.scales
.iter()
.map(|(name, _)| PlotChoice::raw(name.clone()))
.collect();
let scale_panel = panel(column![
section_label("Plot scale"),
section_label(t!("Plot scale")),
check_enabled(
"Fit to paper",
t!("Fit to paper"),
s.fit_to_paper,
PlotFlag::FitToPaper,
common_area,
),
// "Scale" intentionally stays untranslated: it would clash with the
// ribbon's zoom tool label under the same lookup key.
drop_row_enabled(
"Scale",
Cow::Borrowed("Scale"),
scale_options,
Some(s.scale.clone()),
Some(PlotChoice::raw(s.scale.clone())),
PlotDlgMsg::Scale,
width,
common_area && !s.fit_to_paper,
),
check_enabled(
"Scale lineweights",
t!("Scale lineweights"),
s.scale_lw && !s.fit_to_paper,
PlotFlag::ScaleLw,
!s.fit_to_paper,
@@ -681,35 +737,35 @@ pub fn view_window(
].spacing(7));
// ── Style and shaded viewport settings ───────────────────────────────
let mut style_options = vec![STYLE_NONE.to_string()];
style_options.extend(s.plot_styles.iter().cloned());
let mut style_options = vec![PlotChoice::localized(STYLE_NONE)];
style_options.extend(s.plot_styles.iter().cloned().map(PlotChoice::raw));
if !s.style_name.is_empty()
&& !style_options
.iter()
.any(|name| name.eq_ignore_ascii_case(&s.style_name))
.any(|choice| choice.raw.eq_ignore_ascii_case(&s.style_name))
{
style_options.push(s.style_name.clone());
style_options.push(PlotChoice::raw(s.style_name.clone()));
}
let style_selected = if s.style_name.is_empty() {
STYLE_NONE.to_string()
PlotChoice::localized(STYLE_NONE)
} else {
s.style_name.clone()
PlotChoice::raw(s.style_name.clone())
};
let style_panel = panel(column![
section_label("Plot style table (pen assignments)"),
section_label(t!("Plot style table (pen assignments)")),
drop_row(
"Table",
t!("Table"),
style_options,
Some(style_selected),
PlotDlgMsg::Style,
width,
),
row![
button(text("Load…").size(11))
button(text(t!("Load…")).size(11))
.on_press(Message::PlotDlg(PlotDlgMsg::LoadStyle))
.style(btn(false))
.padding([4, 10]),
button(text("Save…").size(11))
button(text(t!("Save…")).size(11))
.on_press(Message::PlotDlg(PlotDlgMsg::SaveStyle))
.style(btn(false))
.padding([4, 10]),
@@ -719,10 +775,10 @@ pub fn view_window(
].spacing(7));
let shaded_panel = panel(column![
section_label("Shaded viewport options"),
section_label(t!("Shaded viewport options")),
drop_row(
"Shade plot",
strs(&[
t!("Shade plot"),
choices(&[
"As displayed",
"2D Wireframe",
"3D Wireframe",
@@ -732,14 +788,14 @@ pub fn view_window(
"Flat Shaded + Edges",
"Gouraud Shaded + Edges",
]),
Some(s.shade.clone()),
Some(PlotChoice::localized(s.shade.clone())),
PlotDlgMsg::Shade,
width,
),
drop_row(
"Quality",
strs(&["Low", "Normal", "High"]),
Some(s.quality.clone()),
t!("Quality"),
choices(&["Low", "Normal", "High"]),
Some(PlotChoice::localized(s.quality.clone())),
PlotDlgMsg::Quality,
width,
),
@@ -748,7 +804,7 @@ pub fn view_window(
// ── Output options and orientation ────────────────────────────────────
let paper_order_option: Element<'_, Message> = if s.paper_space {
check(
"Paper space last",
t!("Paper space last"),
s.paperspace_last,
PlotFlag::PaperspaceLast,
)
@@ -756,19 +812,19 @@ pub fn view_window(
Space::new().height(0).into()
};
let options_panel = panel(column![
section_label("Plot options"),
section_label(t!("Plot options")),
row![
column![
check("Plot in background", s.background, PlotFlag::Background),
check("Object lineweights", s.lineweights, PlotFlag::Lineweights),
check("Plot transparency", s.transparency, PlotFlag::Transparency),
check(t!("Plot in background"), s.background, PlotFlag::Background),
check(t!("Object lineweights"), s.lineweights, PlotFlag::Lineweights),
check(t!("Plot transparency"), s.transparency, PlotFlag::Transparency),
]
.spacing(6)
.width(width),
column![
paper_order_option,
check("Merge overlapping lines", s.merge_lines, PlotFlag::MergeLines),
check("Plot stamp", s.stamp, PlotFlag::Stamp),
check(t!("Merge overlapping lines"), s.merge_lines, PlotFlag::MergeLines),
check(t!("Plot stamp"), s.stamp, PlotFlag::Stamp),
]
.spacing(6)
.width(width),
@@ -778,13 +834,13 @@ pub fn view_window(
let orientation_panel = panel(column![
drop_row(
"Orientation",
strs(&["Portrait", "Landscape"]),
Some(s.orientation.clone()),
t!("Orientation"),
choices(&["Portrait", "Landscape"]),
Some(PlotChoice::localized(s.orientation.clone())),
PlotDlgMsg::Orientation,
width,
),
check("Plot upside-down", s.upside_down, PlotFlag::UpsideDown),
check(t!("Plot upside-down"), s.upside_down, PlotFlag::UpsideDown),
].spacing(7));
let left = column![
@@ -820,12 +876,12 @@ pub fn view_window(
row![
left_bar,
Space::new().width(width),
button(text("Set current").size(11))
button(text(t!("Set current")).size(11))
.on_press(Message::PlotDlg(PlotDlgMsg::SetCurrent))
.style(btn(false))
.padding([4, 12]),
Space::new().width(6),
button(text("Preview").size(11))
button(text(t!("Preview")).size(11))
.on_press(Message::PlotDlg(PlotDlgMsg::Preview))
.style(btn(false))
.padding([4, 12]),
+86 -77
View File
@@ -12,7 +12,9 @@ use iced::widget::{
button, column, container, markdown, row, rule, scrollable, text, text_input, Space,
};
use iced::{Background, Border, Element, Fill, Length, Theme};
use crate::t;
use rustc_hash::{FxHashMap, FxHashSet};
use std::borrow::Cow;
/// Empty lane kept at the right edge of scroll content so the vertical
/// scrollbar never covers card controls.
@@ -79,9 +81,9 @@ fn badge<'a>(label: String) -> Element<'a, Message> {
fn toggle_button<'a>(id: &str, disabled: bool) -> Element<'a, Message> {
// Label shows the action the click performs.
let label = if disabled {
"Enable"
t!("Enable")
} else {
"Disable"
t!("Disable")
};
let want_enabled = disabled; // clicking flips the state
let id_owned = id.to_string();
@@ -101,8 +103,8 @@ enum StatusKind {
}
/// Coloured status pill for a discovered external package.
fn status_badge<'a>(label: &str, kind: StatusKind) -> Element<'a, Message> {
container(text(label.to_string()).size(11))
fn status_badge<'a>(label: Cow<'static, str>, kind: StatusKind) -> Element<'a, Message> {
container(text(label).size(11))
.padding([2, 8])
.style(move |theme: &Theme| {
let palette = theme.palette();
@@ -219,17 +221,17 @@ fn external_card<'a>(
let failed_old_api =
load_error.is_some() && p.api_version != ocs_plugin_api::API_VERSION;
let (status, kind) = if loaded && disabled {
("Disabled", StatusKind::Muted)
(t!("Disabled"), StatusKind::Muted)
} else if loaded {
("Loaded", StatusKind::Success)
(t!("Loaded"), StatusKind::Success)
} else if !p.api_compatible() || failed_old_api {
("API incompatible", StatusKind::Danger)
(t!("API incompatible"), StatusKind::Danger)
} else if load_error.is_some() {
("Load failed", StatusKind::Danger)
(t!("Load failed"), StatusKind::Danger)
} else if !p.lib_present {
("No library", StatusKind::Warning)
(t!("No library"), StatusKind::Warning)
} else {
("Restart to load", StatusKind::Warning)
(t!("Restart to load"), StatusKind::Warning)
};
let header = row![
text(p.name.clone()).size(15),
@@ -249,21 +251,23 @@ fn external_card<'a>(
}
if !p.command_prefixes.is_empty() {
info_body = info_body.push(
text(format!("Commands: {}", p.command_prefixes.join(", ")))
.size(11)
.style(muted_style),
text(t!(
"Commands: %{cmds}",
cmds = p.command_prefixes.join(", ")
))
.size(11)
.style(muted_style),
);
}
if let Some(error) = load_error {
let detail = if failed_old_api {
format!(
"Plugin API {} is incompatible with this build's API {} ribbon ABI. \
Update or rebuild the plugin.",
p.api_version,
ocs_plugin_api::API_VERSION,
t!(
"Plugin API %{plugin} is incompatible with this build's API %{build} ribbon ABI. Update or rebuild the plugin.",
plugin = p.api_version,
build = ocs_plugin_api::API_VERSION,
)
} else {
format!("Load failed: {error}")
t!("Load failed: %{error}", error = error)
};
info_body = info_body.push(
text(detail)
@@ -288,9 +292,8 @@ fn external_card<'a>(
let mut actions = row![Space::new().width(Fill)].align_y(iced::Center);
if let (Some(repo), Some(tag)) = (repository, update_tag) {
let label = format!("Update to {tag}");
actions = actions.push(pill_button(
&label,
t!("Update to %{tag}", tag = tag),
Message::PluginUpdate(repo, tag),
button::primary,
));
@@ -302,7 +305,7 @@ fn external_card<'a>(
actions = actions.push(Space::new().width(6));
}
actions = actions.push(pill_button(
"Uninstall",
t!("Uninstall"),
Message::PluginUninstall(p.id.clone()),
button::danger,
));
@@ -314,11 +317,11 @@ fn external_card<'a>(
}
fn pill_button<'a>(
label: &str,
label: impl Into<Cow<'static, str>>,
msg: Message,
style: fn(&Theme, button::Status) -> button::Style,
) -> Element<'a, Message> {
button(text(label.to_string()).size(12))
button(text(label.into()).size(12))
.padding([4, 12])
.on_press(msg)
.style(style)
@@ -357,7 +360,7 @@ fn install_controls<'a>(
.map(|release| release.api_version)
});
let picker: Element<'_, Message> = if tags.is_empty() {
text("no releases").size(11).style(muted_style).into()
text(t!("no releases")).size(11).style(muted_style).into()
} else {
let r = repo_s.clone();
iced::widget::pick_list(selected, tags, |value| value.to_string())
@@ -368,13 +371,13 @@ fn install_controls<'a>(
let action = match selected_api {
Some(api_version) if api_version == ocs_plugin_api::API_VERSION => {
pill_button(
"Install",
t!("Install"),
Message::PluginInstall(repo_s.clone()),
button::success,
)
}
Some(_) => status_badge("Incompatible", StatusKind::Danger),
None => status_badge("Unavailable", StatusKind::Muted),
Some(_) => status_badge(t!("Incompatible"), StatusKind::Danger),
None => status_badge(t!("Unavailable"), StatusKind::Muted),
};
let mut controls = row![picker, Space::new().width(8), action]
.align_y(iced::Center)
@@ -424,14 +427,14 @@ fn repository_display_name(repository: &str) -> String {
fn add_repository_card<'a>(m: &MarketView) -> Element<'a, Message> {
let form = row![
text_input("GitHub URL or owner/repository", m.input)
text_input(t!("GitHub URL or owner/repository").as_ref(), m.input)
.on_input(Message::PluginRepoInput)
.on_submit(Message::PluginRepoAdd)
.size(13)
.width(Fill),
Space::new().width(8),
pill_button(
"Add repository",
t!("Add repository"),
Message::PluginRepoAdd,
button::primary,
),
@@ -440,11 +443,10 @@ fn add_repository_card<'a>(m: &MarketView) -> Element<'a, Message> {
market_card(
column![
text("Add from GitHub").size(14),
text(
"Paste a public repository URL. Compatible releases and the README \
are detected automatically.",
)
text(t!("Add from GitHub")).size(14),
text(t!(
"Paste a public repository URL. Compatible releases and the README are detected automatically."
))
.size(11)
.style(muted_style),
Space::new().height(3),
@@ -455,28 +457,31 @@ fn add_repository_card<'a>(m: &MarketView) -> Element<'a, Message> {
)
}
fn registry_error_message(error: &str) -> (&'static str, &'static str) {
fn registry_error_message(error: &str) -> (Cow<'static, str>, Cow<'static, str>) {
let error = error.to_ascii_lowercase();
if error.contains("certificate")
|| error.contains("unknownissuer")
|| error.contains("unknown issuer")
{
(
"Unable to verify the server certificate",
"Open CAD Studio could not trust the certificate presented for the plugin registry. \
Check your system certificate and proxy settings, then retry.",
t!("Unable to verify the server certificate"),
t!(
"Open CAD Studio could not trust the certificate presented for the plugin registry. Check your system certificate and proxy settings, then retry."
),
)
} else if error.contains("timed out") || error.contains("timeout") {
(
"Plugin registry request timed out",
"Check your internet or proxy connection, then retry. Manually added repositories \
remain available.",
t!("Plugin registry request timed out"),
t!(
"Check your internet or proxy connection, then retry. Manually added repositories remain available."
),
)
} else {
(
"Unable to load the plugin registry",
"Check your internet or proxy connection, then retry. Manually added repositories \
remain available.",
t!("Unable to load the plugin registry"),
t!(
"Check your internet or proxy connection, then retry. Manually added repositories remain available."
),
)
}
}
@@ -486,23 +491,23 @@ fn registry_notice<'a>(m: &MarketView) -> Option<Element<'a, Message>> {
let (title, message) = registry_error_message(error);
let actions = row![
pill_button(
"Retry",
t!("Retry"),
Message::PluginRegistryRetry,
button::primary,
),
Space::new().width(6),
pill_button(
if m.registry_error_details_open {
"Hide details"
t!("Hide details")
} else {
"Show details"
t!("Show details")
},
Message::PluginRegistryErrorDetailsToggle,
button::secondary,
),
Space::new().width(6),
pill_button(
"Copy details",
t!("Copy details"),
Message::PluginRegistryCopyDiagnostics,
button::secondary,
),
@@ -545,8 +550,8 @@ fn registry_notice<'a>(m: &MarketView) -> Option<Element<'a, Message>> {
(m.registry_loading && m.registry.is_empty()).then(|| {
container(
column![
text("Loading plugin catalog…").size(13),
text("Connecting securely using your system certificate settings.")
text(t!("Loading plugin catalog…")).size(13),
text(t!("Connecting securely using your system certificate settings."))
.size(11)
.style(muted_style),
]
@@ -563,7 +568,7 @@ fn marketplace_section<'a>(
m: &MarketView,
externals: &[ExternalPlugin],
) -> Element<'a, Message> {
let mut col = column![text("Available plugins").size(13).style(primary_style)].spacing(6);
let mut col = column![text(t!("Available plugins")).size(13).style(primary_style)].spacing(6);
let mut visible = 0usize;
if let Some(notice) = registry_notice(m) {
col = col.push(notice);
@@ -617,7 +622,7 @@ fn marketplace_section<'a>(
let info = button(
column![
text(display_name).size(14),
text("Added from GitHub").size(11).style(muted_style),
text(t!("Added from GitHub")).size(11).style(muted_style),
]
.spacing(4)
.width(Fill),
@@ -642,9 +647,9 @@ fn marketplace_section<'a>(
&& !(m.registry_loading && m.registry.is_empty())
{
let message = if m.search.trim().is_empty() {
"No additional plugins are available."
t!("No additional plugins are available.")
} else {
"No available plugins match your search."
t!("No available plugins match your search.")
};
col = col.push(text(message).size(12).style(muted_style));
}
@@ -684,8 +689,8 @@ fn readme_panel<'a>(
let Some(repo) = market.selected_repo else {
return container(
column![
text("Plugin details").size(16),
text("Select a plugin to read its GitHub README.")
text(t!("Plugin details")).size(16),
text(t!("Select a plugin to read its GitHub README."))
.size(12)
.style(muted_style),
]
@@ -712,7 +717,7 @@ fn readme_panel<'a>(
.spacing(3)
.width(width),
pill_button(
"View on GitHub",
t!("View on GitHub"),
Message::OpenUrl(format!("https://github.com/{repo}")),
button::secondary,
),
@@ -722,8 +727,8 @@ fn readme_panel<'a>(
let content: Element<'a, Message> = if market.readme_loading.contains(repo) {
container(
column![
text("Loading README…").size(13),
text("Fetching the default branch from GitHub.")
text(t!("Loading README…")).size(13),
text(t!("Fetching the default branch from GitHub."))
.size(11)
.style(muted_style),
]
@@ -748,11 +753,11 @@ fn readme_panel<'a>(
}
Some(Err(error)) => container(
column![
text("README could not be loaded").size(14),
text(t!("README could not be loaded")).size(14),
text(error.clone()).size(11).style(muted_style),
Space::new().height(4),
pill_button(
"Retry",
t!("Retry"),
Message::PluginReadmeSelect(repo.to_string()),
button::secondary,
),
@@ -765,7 +770,7 @@ fn readme_panel<'a>(
.height(height)
.into(),
None => container(
text("Select the plugin again to load its README.")
text(t!("Select the plugin again to load its README."))
.size(12)
.style(muted_style),
)
@@ -811,15 +816,16 @@ pub fn view_window<'a>(
) -> Element<'a, Message> {
let width = sizing.width;
let height = sizing.height;
let title = text("Plugins").size(20);
let subtitle = text("Browse, install, and manage add-ons. Select one to view its README.")
.size(12)
.style(muted_style);
let title = text(t!("Plugins")).size(20);
let subtitle =
text(t!("Browse, install, and manage add-ons. Select one to view its README."))
.size(12)
.style(muted_style);
let mut list = column![].spacing(10);
// Installed external packages (from the plugins folder).
if externals.is_empty() {
list = list.push(text("No plugins installed yet.").size(13).style(muted_style));
list = list.push(text(t!("No plugins installed yet.")).size(13).style(muted_style));
} else {
let mut visible_installed = 0usize;
for p in externals {
@@ -828,7 +834,7 @@ pub fn view_window<'a>(
continue;
}
if visible_installed == 0 {
list = list.push(text("Installed").size(13).style(primary_style));
list = list.push(text(t!("Installed")).size(13).style(primary_style));
}
visible_installed += 1;
let selected = repository.as_deref() == market.selected_repo;
@@ -857,7 +863,7 @@ pub fn view_window<'a>(
}
if visible_installed == 0 && !market.search.trim().is_empty() {
list = list.push(
text("No installed plugins match your search.")
text(t!("No installed plugins match your search."))
.size(12)
.style(muted_style),
);
@@ -875,7 +881,7 @@ pub fn view_window<'a>(
let catalog = scrollable(container(list.width(Fill)).padding(gutter))
.height(height)
.width(Fill);
let search = text_input("Search plugins…", market.search)
let search = text_input(t!("Search plugins…").as_ref(), market.search)
.on_input(Message::PluginSearchInput)
.size(13)
.padding([7, 10])
@@ -916,7 +922,7 @@ pub fn view_window<'a>(
/// Browser replacement for the native Plugin Manager.
#[cfg(target_arch = "wasm32")]
pub fn view_web_notice<'a>() -> Element<'a, Message> {
let download = button(text("Download desktop app").size(13))
let download = button(text(t!("Download desktop app")).size(13))
.on_press(Message::OpenUrl(DESKTOP_DOWNLOAD_URL.to_string()))
.padding([9, 18])
.style(|theme: &Theme, status| {
@@ -944,14 +950,13 @@ pub fn view_web_notice<'a>() -> Element<'a, Message> {
let notice = container(
column![
icon,
text("Plugins are available in the desktop app")
text(t!("Plugins are available in the desktop app"))
.size(20)
.width(Length::Fit)
.align_x(iced::alignment::Horizontal::Center),
text(
"Open CAD Studio plugins are native packages and cannot run inside a browser. \
Download the desktop app to browse, install, and use plugins.",
)
text(t!(
"Open CAD Studio plugins are native packages and cannot run inside a browser. Download the desktop app to browse, install, and use plugins."
))
.size(13)
.width(Length::Fit)
.align_x(iced::alignment::Horizontal::Center)
@@ -998,6 +1003,10 @@ mod tests {
#[test]
fn certificate_errors_get_user_friendly_copy() {
// The registry message is localised; pin English so the assertion
// does not depend on the ambient process locale.
crate::i18n::set_language(crate::i18n::Language::EnUs)
.expect("English locale must be available");
let (title, message) =
registry_error_message("io: invalid peer certificate: UnknownIssuer");
+33 -31
View File
@@ -3,6 +3,7 @@
use crate::app::Message;
use iced::widget::{column, container, row, scrollable, text, Space};
use iced::{Background, Element, Theme};
use crate::t;
use std::borrow::Cow;
/// Display name of the primary accelerator modifier on this platform.
@@ -41,7 +42,7 @@ fn hdivider<'a>(width: iced::Length) -> Element<'a, Message> {
fn shortcut_row<'a>(
key: impl Into<Cow<'static, str>>,
action: &'static str,
action: Cow<'static, str>,
) -> Element<'a, Message> {
row![
text(key.into())
@@ -75,7 +76,7 @@ pub fn view_window<'a>(
// ── Toolbar ───────────────────────────────────────────────────────────
let toolbar = container(
row![
text("Type SHORTCUTS SET <key> <cmd> to add custom shortcuts.")
text(t!("Type SHORTCUTS SET <key> <cmd> to add custom shortcuts."))
.size(10)
.style(muted_style),
]
@@ -92,41 +93,42 @@ pub fn view_window<'a>(
// ── Shortcut entries ──────────────────────────────────────────────────
let mut rows: Vec<Element<'_, Message>> = vec![
section("── Function Keys ──────────────────────────────────────"),
shortcut_row("F3", "Toggle Object Snap"),
shortcut_row("F7", "Toggle Grid"),
shortcut_row("F8", "Toggle Ortho"),
shortcut_row("F9", "Toggle Grid Snap"),
shortcut_row("F10", "Toggle Polar Tracking"),
shortcut_row("F11", "Toggle Object Snap Tracking"),
shortcut_row("F12", "Toggle Dynamic Input"),
section(format!(
"── {MOD} Shortcuts ──────────────────────────────────────"
section(t!("── Function Keys ──────────────────────────────────────")),
shortcut_row("F3", t!("Toggle Object Snap")),
shortcut_row("F7", t!("Toggle Grid")),
shortcut_row("F8", t!("Toggle Ortho")),
shortcut_row("F9", t!("Toggle Grid Snap")),
shortcut_row("F10", t!("Toggle Polar Tracking")),
shortcut_row("F11", t!("Toggle Object Snap Tracking")),
shortcut_row("F12", t!("Toggle Dynamic Input")),
section(t!(
"── %{MOD} Shortcuts ──────────────────────────────────────",
MOD = MOD
)),
shortcut_row(format!("{MOD}+N"), "New Drawing"),
shortcut_row(format!("{MOD}+O"), "Open File"),
shortcut_row(format!("{MOD}+S"), "Save"),
shortcut_row(format!("{MOD}+Shift+S"), "Save As"),
shortcut_row(format!("{MOD}+Z"), "Undo"),
shortcut_row(format!("{MOD}+Shift+Z / {MOD}+Y"), "Redo"),
shortcut_row(format!("{MOD}+F / {MOD}+H"), "Find and Replace"),
shortcut_row(format!("{MOD}+C"), "Copy to Clipboard"),
shortcut_row(format!("{MOD}+X"), "Cut to Clipboard"),
shortcut_row(format!("{MOD}+V"), "Paste from Clipboard"),
section("── Other Keys ──────────────────────────────────────────"),
shortcut_row("Enter / Space", "Finalize command / Repeat last"),
shortcut_row("Escape", "Cancel active command"),
shortcut_row("Delete", "Delete selected entities"),
shortcut_row("↑ / ↓", "Command history navigation"),
shortcut_row(format!("{MOD}+N"), t!("New Drawing")),
shortcut_row(format!("{MOD}+O"), t!("Open File")),
shortcut_row(format!("{MOD}+S"), t!("Save")),
shortcut_row(format!("{MOD}+Shift+S"), t!("Save As")),
shortcut_row(format!("{MOD}+Z"), t!("Undo")),
shortcut_row(format!("{MOD}+Shift+Z / {MOD}+Y"), t!("Redo")),
shortcut_row(format!("{MOD}+F / {MOD}+H"), t!("Find and Replace")),
shortcut_row(format!("{MOD}+C"), t!("Copy to Clipboard")),
shortcut_row(format!("{MOD}+X"), t!("Cut to Clipboard")),
shortcut_row(format!("{MOD}+V"), t!("Paste from Clipboard")),
section(t!("── Other Keys ──────────────────────────────────────────")),
shortcut_row("Enter / Space", t!("Finalize command / Repeat last")),
shortcut_row("Escape", t!("Cancel active command")),
shortcut_row("Delete", t!("Delete selected entities")),
shortcut_row("↑ / ↓", t!("Command history navigation")),
];
// Custom overrides section
rows.push(section(
"── Custom Overrides (SHORTCUTS SET) ──────────────────",
t!("── Custom Overrides (SHORTCUTS SET) ──────────────────"),
));
if overrides.is_empty() {
rows.push(
text(" (none — use: SHORTCUTS SET <key> <command>)")
text(t!(" (none — use: SHORTCUTS SET <key> <command>)"))
.size(11)
.style(muted_style)
.into(),
@@ -160,8 +162,8 @@ pub fn view_window<'a>(
// ── Header row with accent ────────────────────────────────────────────
let header = container(
row![
text("Key").size(10).style(primary_style).width(160),
text("Action").size(10).style(primary_style),
text(t!("Key")).size(10).style(primary_style).width(160),
text(t!("Action")).size(10).style(primary_style),
]
.spacing(8)
.padding([4, 16]),
+11 -9
View File
@@ -1,6 +1,8 @@
use crate::app::Message;
use iced::widget::{button, column, container, row, scrollable, text, Space};
use iced::{Background, Border, Element, Theme};
use crate::t;
use std::borrow::Cow;
fn muted_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
@@ -18,7 +20,7 @@ fn primary_style(theme: &Theme) -> iced::widget::text::Style {
/// flag tints the border + label with the accent colour, making the new
/// version the visual anchor of the row.
fn version_card<'a>(
label: &'static str,
label: Cow<'static, str>,
value: String,
highlight: bool,
width: iced::Length,
@@ -132,8 +134,8 @@ pub fn view_window<'a>(
) -> Element<'a, Message> {
let header = container(
column![
text("New Release Available").size(20).style(primary_style),
text("A newer Open CAD Studio version is published on GitHub.")
text(t!("New Release Available")).size(20).style(primary_style),
text(t!("A newer Open CAD Studio version is published on GitHub."))
.size(11)
.style(muted_style),
]
@@ -153,12 +155,12 @@ pub fn view_window<'a>(
// one accent-tinted to draw the eye. Replaces the previous label/value
// row layout. The arrow between them is purely decorative.
let installed = version_card(
"Installed",
t!("Installed"),
format!("v{}", env!("CARGO_PKG_VERSION")),
false,
sizing.width,
);
let latest_card = version_card("Latest", format!("v{}", latest), true, sizing.width);
let latest_card = version_card(t!("Latest"), format!("v{}", latest), true, sizing.width);
let arrow = container(crate::ui::icons::themed_secondary(
crate::ui::icons::ARROW_LONG_RIGHT,
20.0,
@@ -172,12 +174,12 @@ pub fn view_window<'a>(
.align_y(iced::Center)
.width(sizing.width);
let later_btn = button(text("Later").size(11))
let later_btn = button(text(t!("Later")).size(11))
.on_press(Message::UpdateNoticeClose)
.style(button::secondary)
.padding([6, 16]);
let open_btn = button(text("Open Release Page").size(11))
let open_btn = button(text(t!("Open Release Page")).size(11))
.on_press(Message::UpdateNoticeOpenRelease)
.style(button::primary)
.padding([6, 16]);
@@ -195,7 +197,7 @@ pub fn view_window<'a>(
// Release notes panel. Rendered as a light-markdown column inside a
// bordered scrollable so long bodies stay contained and don't
// explode the window. Empty body → "No release notes provided."
let notes_heading = container(text("What's new").size(11).style(muted_style))
let notes_heading = container(text(t!("What's new")).size(11).style(muted_style))
.padding(iced::Padding {
top: 10.0,
right: 0.0,
@@ -204,7 +206,7 @@ pub fn view_window<'a>(
});
let notes_body: Element<'a, Message> = if body.trim().is_empty() {
text("No release notes provided.")
text(t!("No release notes provided."))
.size(11)
.style(muted_style)
.into()