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

@ -8,6 +8,7 @@ use crate::ui::properties::acad_color_display;
use acadrust::types::Color as AcadColor;
use iced::widget::{button, column, container, row, text};
use iced::{Background, Border, Color, Element, Length, Theme};
use crate::t;
/// Which "logical" entries the colour list offers besides the standard ACI
/// colours.
@ -86,7 +87,7 @@ pub fn color_display_name(c: AcadColor) -> String {
_ => label.to_string(),
}
} else {
label.to_string()
t!(label).into_owned()
}
}
@ -196,7 +197,7 @@ pub fn color_list<'a>(
let named_row = |color: AcadColor| -> Element<'a, Message> {
let (bg, name) = acad_color_display(color);
button(
row![swatch(bg), text(name).size(11)]
row![swatch(bg), text(t!(name)).size(11)]
.spacing(5)
.align_y(iced::Center),
)
@ -218,7 +219,7 @@ pub fn color_list<'a>(
list = list.push(named_row(AcadColor::Index(i)));
}
list = list.push(
button(text("More…").size(11))
button(text(t!("More…")).size(11))
.on_press(on_more)
.style(list_row_style)
.padding([2, 4])

View file

@ -4,6 +4,7 @@ use iced::time::Instant;
use crate::app::Message;
use crate::command::CmdOption;
use crate::t;
use iced::widget::{
button, column, container, opaque, row, rule, text, text_editor, text_input, tooltip, Space,
};
@ -194,20 +195,20 @@ impl CommandLine {
}
pub fn push_command(&mut self, cmd: &str) {
self.push(EntryKind::Command, format!("Command: {cmd}"));
self.push(EntryKind::Command, format!("{} {cmd}", t!("Command:")));
}
pub fn push_output(&mut self, msg: &str) {
self.push(EntryKind::Output, msg.to_string());
}
pub fn push_error(&mut self, msg: &str) {
self.push(EntryKind::Error, format!("*Invalid* {msg}"));
self.push(EntryKind::Error, format!("*{}* {msg}", t!("Invalid")));
}
/// Append an error unless it is already the latest history line. Repeated
/// retry failures should refresh the concise message, not flood history
/// with identical copies (#498).
#[cfg(not(target_arch = "wasm32"))]
pub fn push_error_once(&mut self, msg: &str) {
let text = format!("*Invalid* {msg}");
let text = format!("*{}* {msg}", t!("Invalid"));
if let Some(last) = self
.history
.last_mut()
@ -579,7 +580,7 @@ impl CommandLine {
let copy_btn = button(
row![
crate::ui::icons::themed_success(crate::ui::icons::COPY, 11.0),
text("Copy").size(11),
text(t!("Copy")).size(11),
]
.spacing(4)
.align_y(iced::Center),
@ -590,7 +591,7 @@ impl CommandLine {
let clear_btn = button(
row![
crate::ui::icons::themed_warning(crate::ui::icons::TRASH, 11.0),
text("Clear").size(11),
text(t!("Clear")).size(11),
]
.spacing(4)
.align_y(iced::Center),
@ -814,8 +815,8 @@ mod tests {
fn issue_498_repeated_save_error_is_not_duplicated() {
let mut line = CommandLine::new();
let initial_len = line.history.len();
line.push_error_once("Unable to save: file is in use.");
line.push_error_once("Unable to save: file is in use.");
line.push_error_once(t!("Unable to save: file is in use.").as_ref());
line.push_error_once(t!("Unable to save: file is in use.").as_ref());
assert_eq!(line.history.len(), initial_len + 1);
}
}

View file

@ -322,7 +322,16 @@ pub fn modal<'a>(
// The first stack layer dictates its intrinsic size. Its top spacer reserves
// room for the title, while the actual title bar and resize grip overlay it
// without influencing the modal dimensions.
let measured_content = sensor(content).on_resize(Message::ModalContentResized);
// Iced draws vertical scrollbars inside a scrollable's bounds. Reserve a
// shared end gutter at the modal boundary so a scrollbar cannot cover the
// last control column in Options or any other dialog.
let measured_content = container(sensor(content).on_resize(Message::ModalContentResized))
.padding(Padding {
top: 0.0,
right: 12.0,
bottom: 0.0,
left: 0.0,
});
let body_base = column![
Space::new().height(Length::Fixed(24.0)),
measured_content,

View file

@ -5,6 +5,7 @@ use iced::{Element, Fill};
use crate::app::Message;
use crate::ui::statusbar::status_menu::Entry;
use crate::t;
/// - `has_selection`: enables Isolate / Hide (they act on the selection).
/// - `isolation_active`: enables End Isolation (something is hidden).
@ -41,7 +42,7 @@ fn action_entry(label: &'static str, enabled: bool, msg: Message) -> Entry<'stat
}
fn action_row(label: &'static str, enabled: bool, msg: Message) -> Element<'static, Message> {
let lbl = text(label).size(11);
let lbl = text(t!(label)).size(11);
let content = row![lbl].align_y(iced::Center);
let mut btn = button(content)

View file

@ -2,6 +2,7 @@
use iced::widget::{button, container, row, text, text_input};
use iced::{Element, Fill, Length};
use crate::t;
use crate::app::Message;
use crate::ui::statusbar::status_menu::Entry;
@ -32,7 +33,7 @@ pub fn menu_entries<'a>(
.collect();
// Free-entry custom angle: type a value and press Enter to apply.
let custom_field = text_input("Custom…", custom)
let custom_field = text_input(t!("Custom…").as_ref(), custom)
.on_input(Message::PolarCustomInput)
.on_submit(Message::SubmitPolarCustom)
.size(11)

View file

@ -2,6 +2,7 @@
use iced::widget::{button, row, text};
use iced::{Element, Fill};
use crate::t;
use crate::app::Message;
use crate::ui::statusbar::status_menu::Entry;
@ -63,7 +64,7 @@ fn scale_row(label: String, active: bool, msg: Message) -> Element<'static, Mess
}
fn manage_row() -> Element<'static, Message> {
button(text("Manage...").size(11))
button(text(t!("Manage...")).size(11))
.on_press(Message::ScaleManagerOpen)
.style(button::primary)
.width(Fill)

View file

@ -6,6 +6,7 @@ use rustc_hash::FxHashSet as HashSet;
use iced::widget::{button, container, row, text};
use iced::{Background, Element, Fill, Theme};
use crate::t;
use crate::app::Message;
use crate::ui::statusbar::status_menu::Entry;
@ -66,7 +67,7 @@ pub fn menu_entries(
fn type_row(name: String, included: bool) -> Element<'static, Message> {
let check = crate::ui::icons::themed_check_cell(included);
let lbl = text(name.clone()).size(11);
let lbl = text(t!(name.as_str())).size(11);
let content = row![check, lbl].spacing(6).align_y(iced::Center);
@ -80,7 +81,7 @@ fn type_row(name: String, included: bool) -> Element<'static, Message> {
fn empty_row() -> Element<'static, Message> {
container(
text("No objects").size(11).style(|theme: &Theme| text::Style {
text(t!("No objects")).size(11).style(|theme: &Theme| text::Style {
color: Some(
theme
.palette()
@ -96,7 +97,7 @@ fn empty_row() -> Element<'static, Message> {
}
fn header_btn(label: &str, msg: Message, enabled: bool) -> Element<'_, Message> {
let b = button(text(label).size(10));
let b = button(text(t!(label)).size(10));
let b = if enabled { b.on_press(msg) } else { b };
b.style(button::secondary)
.padding([3, 8])

View file

@ -6,6 +6,7 @@ use iced::{Background, Element, Fill, Length, Theme};
use crate::app::Message;
use crate::snap::{SnapType, Snapper, ALL_SNAP_MODES};
use crate::ui::statusbar::status_menu::Entry;
use crate::t;
pub fn menu_entries<'a>(snapper: &'a Snapper) -> Vec<Entry<'a>> {
let all_on = snapper.all_on();
@ -54,7 +55,7 @@ fn snap_row<'a>(snap_type: SnapType, label: &'a str, active: bool) -> Element<'a
.width(Length::Fixed(16.0))
.align_x(iced::Center);
let label_el = text(label).size(11);
let label_el = text(t!(label)).size(11);
let content = row![checkmark, icon_el, label_el]
.spacing(4)
@ -69,7 +70,7 @@ fn snap_row<'a>(snap_type: SnapType, label: &'a str, active: bool) -> Element<'a
}
fn header_btn(label: &str, msg: Message, enabled: bool) -> Element<'_, Message> {
let b = button(text(label).size(10));
let b = button(text(t!(label)).size(10));
let b = if enabled { b.on_press(msg) } else { b };
b.style(button::secondary)
.padding([3, 8])

View file

@ -5,6 +5,7 @@ use iced::{Element, Fill};
use crate::app::Message;
use crate::ui::statusbar::status_menu::Entry;
use crate::t;
/// Units offered in the picker: (INSUNITS code, menu label).
const UNITS: &[(i16, &str)] = &[
@ -51,7 +52,7 @@ pub fn menu_entries(current: i16) -> Vec<Entry<'static>> {
fn unit_row(label: &'static str, active: bool, msg: Message) -> Element<'static, Message> {
let check = crate::ui::icons::themed_check_cell(active);
let lbl = text(label).size(11);
let lbl = text(t!(label)).size(11);
let content = row![check, lbl].spacing(6).align_y(iced::Center);

View file

@ -19,6 +19,7 @@ use iced::widget::{
use iced::{
mouse, Background, Border, Color, Element, Length, Padding, Point, Rectangle, Size, Theme,
};
use crate::t;
// ── Row-height-derived constants ─────────────────────────────────────────
const FONT_SZ: f32 = ROW_H * 0.42; // ≈11 px
@ -60,9 +61,9 @@ pub struct LwItem(pub LineWeight);
impl fmt::Display for LwItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
LineWeight::ByLayer => write!(f, "ByLayer"),
LineWeight::ByBlock => write!(f, "ByBlock"),
LineWeight::Default => write!(f, "Default"),
LineWeight::ByLayer => write!(f, "{}", crate::t!("ByLayer")),
LineWeight::ByBlock => write!(f, "{}", crate::t!("ByBlock")),
LineWeight::Default => write!(f, "{}", crate::t!("Default")),
LineWeight::Value(v) => write!(f, "{:.2} mm", v as f64 / 100.0),
}
}
@ -244,6 +245,27 @@ pub fn attr_edit_key(tag: &str) -> String {
format!("\x01attr\x01{tag}")
}
/// A translated choice label paired with the unchanged value stored in the
/// drawing and emitted by the properties panel.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LocalizedChoice {
pub raw: String,
label: String,
}
impl LocalizedChoice {
pub fn new(raw: String) -> Self {
let label = 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)
}
}
// ── PropertiesPanel ───────────────────────────────────────────────────────
#[derive(Clone)]
@ -255,7 +277,7 @@ pub struct PropertiesPanel {
/// Linetype items (name + ASCII art) from the document — used for combo_box options.
pub linetype_items: Vec<LinetypeItem>,
pub selection_group_combo: combo_box::State<SelectionGroup>,
pub choice_combos: HashMap<String, combo_box::State<String>>,
pub choice_combos: HashMap<String, combo_box::State<LocalizedChoice>>,
pub layer_combo: combo_box::State<String>,
pub lineweight_combo: combo_box::State<LwItem>,
pub linetype_combo: combo_box::State<LinetypeItem>,
@ -333,7 +355,7 @@ impl Default for PropertiesPanel {
impl PropertiesPanel {
pub fn empty() -> Self {
Self {
title: "No Selection".into(),
title: t!("No selection").into_owned(),
..Default::default()
}
}
@ -347,7 +369,7 @@ impl PropertiesPanel {
pub fn view(&self) -> Element<'_, Message> {
// ── Header ──────────────────────────────────────────────────────────
let header = container(text("Properties").size(12))
let header = container(text(t!("Properties")).size(12))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.weak.color,
@ -397,7 +419,7 @@ impl PropertiesPanel {
// ── Content ─────────────────────────────────────────────────────────
let content: Element<'_, Message> = if self.sections.is_empty() {
container(
text("Select an object to view properties")
text(t!("Select an object to view properties"))
.size(10)
.style(hint_text_style),
)
@ -823,10 +845,13 @@ impl PropertiesPanel {
let selected = if current == VARIES_LABEL {
None
} else {
Some(current.to_string())
Some(LocalizedChoice::new(current.to_string()))
};
let combo = combo_box(state, VARIES_LABEL, selected.as_ref(), move |value| {
Message::PropGeomChoiceChanged { field, value }
let combo = combo_box(state, VARIES_LABEL, selected.as_ref(), move |choice| {
Message::PropGeomChoiceChanged {
field,
value: choice.raw,
}
})
.size(FONT_SZ)
.padding(Padding {
@ -1058,7 +1083,7 @@ impl PropertiesPanel {
return prop_row_widget(label, head.into());
}
let search = text_input("Search patterns…", &self.hatch_pattern_search)
let search = text_input(t!("Search patterns…").as_ref(), &self.hatch_pattern_search)
.id(iced::widget::Id::new("hatch-pattern-search"))
.on_input(Message::PropHatchPatternSearchChanged)
.on_submit(Message::PropHatchPatternConfirm)
@ -1127,7 +1152,7 @@ impl PropertiesPanel {
let results: Element<'_, Message> = if visible.is_empty() {
container(
text("No matching patterns")
text(t!("No matching patterns"))
.size(FONT_SZ)
.style(hint_text_style),
)
@ -1227,9 +1252,13 @@ pub fn color_picker_dropdown<'a>(
} else {
crate::ui::icons::themed_arrow_down(9.0)
},
text(if palette_open { "Less" } else { "More Colors…" })
.size(10)
.style(hint_text_style),
text(if palette_open {
t!("Less").into_owned()
} else {
t!("More Colors…").into_owned()
})
.size(10)
.style(hint_text_style),
]
.spacing(4)
.align_y(iced::Center),
@ -1365,7 +1394,11 @@ fn render_stepper_row<'a>(label: &'a str, display: &'a str) -> Element<'a, Messa
}
fn render_bool_row<'a>(label: &'a str, field: &'static str, value: bool) -> Element<'a, Message> {
let btn_label = if value { "Yes" } else { "No" };
let btn_label = if value {
t!("Yes").into_owned()
} else {
t!("No").into_owned()
};
let btn =
button(
text(btn_label)
@ -1617,7 +1650,7 @@ fn prop_row_widget<'a>(label: &'a str, widget: Element<'a, Message>) -> Element<
/// A plain text button used inside the color picker for ByLayer / ByBlock.
fn picker_text_btn(label: &str, msg: Message) -> Element<'_, Message> {
button(text(label).size(FONT_SZ))
button(text(t!(label)).size(FONT_SZ))
.on_press(msg)
.style(button::secondary)
.padding([2, 8])

View file

@ -26,6 +26,7 @@ mod collapse;
use collapse::{CollapsePanels, Panel};
pub use collapse::CollapseMode;
use crate::ui::wrap_bar::{PosReport, WrapBar, WrapFlow};
use crate::t;
// ── Ribbon state ───────────────────────────────────────────────────────────
@ -742,7 +743,7 @@ impl Ribbon {
button(
row![
crate::ui::icons::themed_check_cell(m == current),
text(m.label()).size(11),
text(t!(m.label())).size(11),
]
.spacing(4)
.align_y(iced::Center),
@ -824,7 +825,7 @@ impl Ribbon {
.width(Length::Fixed(20.0))
.into();
let label_el =
text(*label)
text(t!(*label))
.size(11)
.style(move |theme: &Theme| iced::widget::text::Style {
color: (!is_current).then_some(
@ -958,14 +959,14 @@ impl Ribbon {
let row_count = rows.len().max(1);
let list_h = (row_count as f32 * 26.0).min(420.0);
// Search box (#343): filters the list live as the user types.
let search = iced::widget::text_input("Search layers…", &self.layer_filter)
let search = iced::widget::text_input(t!("Search layers…").as_ref(), &self.layer_filter)
.on_input(Message::RibbonLayerFilterChanged)
.size(11)
.padding([4, 6]);
let state_manager = button(
row![
crate::ui::icons::themed_arrow_right(9.0),
text("Layer State Manager…").size(11),
text(t!("Layer State Manager…")).size(11),
]
.spacing(7)
.align_y(iced::Center),
@ -1081,7 +1082,7 @@ impl Ribbon {
if let Some(mgr) = manager_cmd {
rows.push(
button(text("Manage…").size(11))
button(text(t!("Manage…")).size(11))
.on_press(Message::Command(mgr.to_string()))
.style(popup_row_style)
.width(Fill)
@ -1323,7 +1324,7 @@ fn render_group<'a>(
column![
tools_el,
container(text(group.title).size(9).style(muted_text_style)).padding([1, 4]),
container(text(t!(group.title)).size(9).style(muted_text_style)).padding([1, 4]),
]
.align_x(iced::Center)
.spacing(0)
@ -1410,7 +1411,7 @@ fn collapse_button<'a>(
column![
icon,
row![
text(title.to_string()).size(9).style(muted_text_style),
text(t!(title)).size(9).style(muted_text_style),
crate::ui::icons::themed_secondary_arrow_down(8.0),
]
.spacing(3)

View file

@ -16,6 +16,7 @@ use crate::modules::{IconKind, ModuleEvent, RibbonItem, StyleKey, ToolDef};
use crate::ui::wrap_bar::PosReport;
use crate::ui::icons;
use crate::ui::properties::{acad_color_display, LwItem};
use crate::t;
use super::LayerInfo;
@ -311,7 +312,7 @@ pub(super) fn render_small<'a>(
let dim = start_dimmed(&state, &t.event);
let event = t.event.clone();
let tool_id = t.id.to_string();
let tip_text = format!("{}\nCommand: {}", t.label, t.id);
let tip_text = format!("{}\n{} {}", t!(t.label), t!("Command:"), t.id);
let btn = button(make_icon_dim(t.icon, SMALL_ICON, dim))
.on_press(Message::RibbonToolClick { tool_id, event })
.style(move |theme: &Theme, status| tool_btn_style(theme, active, status))
@ -373,7 +374,7 @@ pub(super) fn render_small<'a>(
})
.or_else(|| items.first().map(|(_, lbl, _)| *lbl))
.unwrap_or(*id);
let tip_text = format!("{}\nCommand: {}", cur_label, last);
let tip_text = format!("{}\n{} {}", t!(cur_label), t!("Command:"), last);
let icon_btn = button(make_icon_dim(cur_icon, SMALL_ICON, dim))
.on_press(Message::RibbonToolClick {
@ -385,7 +386,7 @@ pub(super) fn render_small<'a>(
.height(ROW_H)
.padding([4, 4]);
let arr_tip = format!("{} options", cur_label);
let arr_tip = format!("{} {}", t!(cur_label), t!("options"));
let arr_btn = button(
container(icons::themed_arrow_down(8.0))
.width(Fill)
@ -431,7 +432,7 @@ pub(super) fn render_small<'a>(
pub(super) fn render_large_dropdown<'a>(
id: &'static str,
icon: IconKind,
explicit_label: Option<&str>,
explicit_label: Option<&'a str>,
items: &[(&'static str, &'static str, IconKind)],
default: &'static str,
active_tool: &Option<String>,
@ -459,14 +460,14 @@ pub(super) fn render_large_dropdown<'a>(
.or_else(|| items.first().map(|(_, lbl, _)| *lbl))
.unwrap_or(id);
let label = explicit_label.unwrap_or(cur_label);
let tip_text = format!("{}\nCommand: {}", cur_label, last);
let arr_tip = format!("{} options", label);
let tip_text = format!("{}\n{} {}", t!(cur_label), t!("Command:"), last);
let arr_tip = format!("{} {}", t!(label), t!("options"));
// Icon on top with the label beneath it, then the ▾ strip at the very bottom.
let top_btn = button(
column![
make_icon_dim(cur_icon, LARGE_ICON, dim),
text(label.to_string())
text(t!(label))
.size(10)
.style(move |theme: &Theme| tool_label_style(theme, dim)),
]
@ -545,11 +546,11 @@ pub(super) fn render_large<'a>(
let dim = start_dimmed(&state, &t.event);
let event = t.event.clone();
let tool_id = t.id.to_string();
let tip_text = format!("{}\nCommand: {}", t.label, t.id);
let tip_text = format!("{}\n{} {}", t!(t.label), t!("Command:"), t.id);
let btn = button(
column![
make_icon_dim(t.icon, LARGE_ICON, dim),
text(t.label)
text(t!(t.label))
.size(10)
.style(move |theme: &Theme| tool_label_style(theme, dim)),
]
@ -685,7 +686,7 @@ pub(super) fn render_large<'a>(
.map(|t| {
let is_active = active_tool.as_deref() == Some(t.id);
let dim = start_dimmed(&state, &t.event);
let tip = t.label;
let tip = t!(t.label);
let event = t.event.clone();
let icon_el: Element<Message> = if dim {
make_icon_dim(t.icon, 16.0, true)
@ -752,11 +753,16 @@ pub(super) fn render_large<'a>(
let mp_dim = start_dimmed(&state, &match_prop.event);
let mp_event = match_prop.event.clone();
let mp_id = match_prop.id.to_string();
let mp_tip = format!("{}\nCommand: {}", match_prop.label, match_prop.id);
let mp_tip = format!(
"{}\n{} {}",
t!(match_prop.label),
t!("Command:"),
match_prop.id
);
let mp_btn = button(
column![
make_icon_dim(match_prop.icon, LARGE_ICON, mp_dim),
text(match_prop.label)
text(t!(match_prop.label))
.size(10)
.style(move |theme: &Theme| tool_label_style(theme, mp_dim)),
]
@ -906,7 +912,7 @@ pub(super) fn render_large<'a>(
.map(|t| {
let is_active = active_tool.as_deref() == Some(t.id);
let dim = start_dimmed(&state, &t.event);
let tip = t.label;
let tip = t!(t.label);
let event = t.event.clone();
let icon_el: Element<Message> = if dim {
make_icon_dim(t.icon, 16.0, true)
@ -1006,7 +1012,7 @@ pub(super) fn quick_access_btn<'a>(
.width(Length::Fixed(TOP_HIST_W))
.height(24)
.padding([2, 0]);
tooltip(btn, make_tip(label.to_string()), TipPos::Bottom)
tooltip(btn, make_tip(t!(label).into_owned()), TipPos::Bottom)
.gap(6.0)
.delay(Duration::from_millis(400))
.style(tip_style)
@ -1052,7 +1058,11 @@ pub(super) fn render_history_control<'a>(
};
tooltip(
btn,
make_tip(format!("{label}\n{count} steps available")),
make_tip(format!(
"{}\n{}",
t!(label),
t!("%{count} steps available", count = count)
)),
TipPos::Right,
)
.gap(6.0)
@ -1085,7 +1095,10 @@ pub(super) fn render_history_control<'a>(
};
tooltip(
btn,
make_tip(format!("Choose {label} history")),
make_tip(format!(
"{}",
t!("%{label} history", label = t!(label))
)),
TipPos::Right,
)
.gap(6.0)

View file

@ -26,6 +26,7 @@ use crate::snap::Snapper;
use crate::ui::statusbar::statusbar_config::{StatusBarConfig, StatusPill};
use crate::ui::statusbar::status_menu::Entry as StatusMenuEntry;
use crate::ui::wrap_bar::WrapBar;
use crate::t;
const ST_ANNO_VISIBILITY: &[u8] = include_bytes!("../../../assets/icons/scale_list.svg");
const ST_ANNO_AUTO_ADD: &[u8] = include_bytes!("../../../assets/icons/add_scale.svg");
@ -142,7 +143,7 @@ impl StatusBar {
let menu_btn = if is_start {
tip(
menu_button.into(),
"Open or create a drawing to manage layouts.",
t!("Open or create a drawing to manage layouts."),
)
} else {
status_menu::menu_bar(
@ -150,7 +151,7 @@ impl StatusBar {
menu_button
.on_press(Message::StatusMenuTooltipHidden(true))
.into(),
"Model and layout list",
t!("Model and layout list"),
tooltip_hidden,
),
statusbar_menu::layout_entries(&layout_names, &current_layout),
@ -164,7 +165,7 @@ impl StatusBar {
let add_btn = if is_start {
tip(
add_button.into(),
"Open or create a drawing to add a layout.",
t!("Open or create a drawing to add a layout."),
)
} else {
add_button.on_press(Message::LayoutCreate).into()
@ -174,7 +175,7 @@ impl StatusBar {
let osnap_active = snapper.is_active();
let vp_label = if viewport_count > 0 {
format!("{} VP", viewport_count)
t!("%{n} VP", n = viewport_count).into_owned()
} else {
String::new()
};
@ -205,7 +206,7 @@ impl StatusBar {
status_menu::menu_bar(
menu_tip(
popup_pill(&scale_label),
"Annotation / Viewport Scale\nClick to change",
t!("Annotation / Viewport Scale\nClick to change"),
tooltip_hidden,
),
crate::ui::popup::scale_popup::menu_entries(
@ -229,7 +230,7 @@ impl StatusBar {
pills.push(
tip(
action_pill(&coords_label, Message::CycleCoordsMode),
"Cursor coordinates ($COORDS)\nClick to cycle: static / live / polar",
t!("Cursor coordinates ($COORDS)\nClick to cycle: static / live / polar"),
)
.into(),
);
@ -238,7 +239,7 @@ impl StatusBar {
pills.push(
tip(
toggle_pill(crate::ui::icons::ST_ORTHO, ortho_mode, Message::ToggleOrtho),
"Orthogonal Mode\nF8",
t!("Orthogonal Mode\nF8"),
)
.into(),
);
@ -247,7 +248,7 @@ impl StatusBar {
pills.push(
tip(
toggle_pill(crate::ui::icons::ST_LWT, lineweight_display, Message::ToggleLineweightDisplay),
"Show Lineweight\nLWDISPLAY",
t!("Show Lineweight\nLWDISPLAY"),
)
.into(),
);
@ -270,7 +271,7 @@ impl StatusBar {
pills.push(
tip(
toggle_pill(crate::ui::icons::ST_DYN, dyn_input, Message::ToggleDynInput),
"Dynamic Input\nF12",
t!("Dynamic Input\nF12"),
)
.into(),
);
@ -279,7 +280,7 @@ impl StatusBar {
pills.push(
tip(
toggle_pill(crate::ui::icons::ST_OTRACK, otrack, Message::ToggleOTrack),
"Object Snap Tracking\nF11",
t!("Object Snap Tracking\nF11"),
)
.into(),
);
@ -299,7 +300,7 @@ impl StatusBar {
pills.push(
tip(
space_mode_btn(&current_layout, in_mspace),
"PAPER: double-click viewport to enter MSPACE\nMODEL: click to switch to Model Space",
t!("PAPER: double-click viewport to enter MSPACE\nMODEL: click to switch to Model Space"),
)
.into(),
);
@ -315,7 +316,7 @@ impl StatusBar {
annotation_all_visible,
Message::ToggleAnnotationVisibility,
),
"Show Annotation Objects",
t!("Show Annotation Objects"),
)
.into(),
);
@ -328,7 +329,7 @@ impl StatusBar {
annotation_auto_add,
Message::ToggleAnnotationAutoAdd,
),
"Automatically Add Scales",
t!("Automatically Add Scales"),
)
.into(),
);
@ -342,7 +343,7 @@ impl StatusBar {
synced,
Message::SyncViewportAnnotationScale,
),
"Viewport / Annotation Scale Sync",
t!("Viewport / Annotation Scale Sync"),
)
.into(),
);
@ -352,8 +353,8 @@ impl StatusBar {
pills.push(
status_menu::menu_bar(
menu_tip(
popup_pill(crate::ui::popup::units_popup::unit_short(insertion_units)),
"Drawing Units (INSUNITS)\nClick to change",
popup_pill(t!(crate::ui::popup::units_popup::unit_short(insertion_units))),
t!("Drawing Units (INSUNITS)\nClick to change"),
tooltip_hidden,
),
crate::ui::popup::units_popup::menu_entries(insertion_units),
@ -370,7 +371,7 @@ impl StatusBar {
transparency_display,
Message::ToggleTransparencyDisplay,
),
"Show Transparency\nForce opaque when off",
t!("Show Transparency\nForce opaque when off"),
)
.into(),
);
@ -384,7 +385,7 @@ impl StatusBar {
isolation_active,
Message::StatusMenuTooltipHidden(true),
),
"Isolate Objects\nClick for Isolate / Hide / End",
t!("Isolate Objects\nClick for Isolate / Hide / End"),
tooltip_hidden,
),
crate::ui::popup::isolate_popup::menu_entries(
@ -400,7 +401,7 @@ impl StatusBar {
pills.push(
tip(
toggle_pill(crate::ui::icons::ST_QUICKPROPS, quick_properties, Message::ToggleQuickProperties),
"Quick Properties\nFloating panel on selection",
t!("Quick Properties\nFloating panel on selection"),
)
.into(),
);
@ -414,7 +415,7 @@ impl StatusBar {
selection_filter_active,
Message::StatusMenuTooltipHidden(true),
),
"Selection Filtering\nLimit which object types can be picked",
t!("Selection Filtering\nLimit which object types can be picked"),
tooltip_hidden,
),
crate::ui::popup::selection_filter_popup::menu_entries(
@ -430,7 +431,7 @@ impl StatusBar {
pills.push(
tip(
toggle_pill(crate::ui::icons::ST_SELCYCLE, selection_cycling, Message::ToggleSelectionCycling),
"Selection Cycling\nRepeat-click to step through overlapping objects",
t!("Selection Cycling\nRepeat-click to step through overlapping objects"),
)
.into(),
);
@ -439,7 +440,7 @@ impl StatusBar {
pills.push(
tip(
status_pill(vp_label).into(),
"Viewport count in active layout",
t!("Viewport count in active layout"),
)
.into(),
);
@ -448,7 +449,7 @@ impl StatusBar {
pills.push(
tip(
toggle_pill(crate::ui::icons::ST_CLEANSCREEN, clean_screen, Message::ToggleCleanScreen),
"Clean Screen\nHide ribbon and panels",
t!("Clean Screen\nHide ribbon and panels"),
)
.into(),
);
@ -458,7 +459,7 @@ impl StatusBar {
status_menu::menu_bar(
menu_tip(
customize_btn(),
"Customization\nShow or hide status-bar items",
t!("Customization\nShow or hide status-bar items"),
tooltip_hidden,
),
statusbar_menu::customization_entries(config),
@ -589,7 +590,10 @@ fn customize_btn() -> Element<'static, Message> {
// ── Tooltip helper ────────────────────────────────────────────────────────
fn tip<'a>(content: Element<'a, Message>, label: &'static str) -> Element<'a, Message> {
fn tip<'a>(
content: Element<'a, Message>,
label: std::borrow::Cow<'static, str>,
) -> Element<'a, Message> {
tip_node(content, text(label).size(11).into())
}
@ -597,7 +601,7 @@ fn tip<'a>(content: Element<'a, Message>, label: &'static str) -> Element<'a, Me
/// opened menu resets suppression for the next hover without covering the menu.
fn menu_tip<'a>(
content: Element<'a, Message>,
label: &'static str,
label: std::borrow::Cow<'static, str>,
hidden: bool,
) -> Element<'a, Message> {
let content = if hidden {
@ -707,8 +711,9 @@ fn polar_pill<'a>(
entries: Vec<StatusMenuEntry<'a>>,
) -> Element<'a, Message> {
let angle = crate::ui::popup::polar_popup::angle_label(increment_deg);
let tooltip_text = format!(
"Polar Tracking ({angle})\nF10 — left-click on/off\nRight-click cycles · ▾ picks angle",
let tooltip_text = t!(
"Polar Tracking (%{angle})\nF10 — left-click on/off\nRight-click cycles · ▾ picks angle",
angle = angle,
);
// Right-click quick-cycles through the same increments the picker lists.
@ -762,7 +767,7 @@ fn polar_pill<'a>(
)
.on_press(Message::StatusMenuTooltipHidden(true))
.into(),
"Polar angle\nClick to choose",
t!("Polar angle\nClick to choose"),
tooltip_hidden,
),
entries,
@ -792,7 +797,7 @@ fn osnap_btn<'a>(
mouse_area(snap_icon)
.on_press(Message::ToggleSnapEnabled)
.into(),
"Object Snap: toggle on/off\nF3",
t!("Object Snap: toggle on/off\nF3"),
);
let caret = status_menu::menu_bar(
@ -807,7 +812,7 @@ fn osnap_btn<'a>(
)
.on_press(Message::StatusMenuTooltipHidden(true))
.into(),
"Object Snap list\nClick to choose snap types",
t!("Object Snap list\nClick to choose snap types"),
tooltip_hidden,
),
entries,
@ -820,7 +825,7 @@ fn osnap_btn<'a>(
// ── Helpers ───────────────────────────────────────────────────────────────
fn layout_tab_context_menu(name: String) -> Element<'static, Message> {
let item = |label: &'static str, msg: Message| {
let item = |label: std::borrow::Cow<'static, str>, msg: Message| {
button(text(label).size(12))
.on_press(msg)
.style(button::subtle)
@ -830,8 +835,8 @@ fn layout_tab_context_menu(name: String) -> Element<'static, Message> {
container(
column![
item("Rename", Message::LayoutRenameStart(name.clone())),
item("Delete", Message::LayoutDelete(name)),
item(t!("Rename"), Message::LayoutRenameStart(name.clone())),
item(t!("Delete"), Message::LayoutDelete(name)),
]
.spacing(0)
.width(160),
@ -888,7 +893,7 @@ fn space_tab<'a>(
format!("{report_key_prefix}:{label}"),
tip(
display.into(),
"Open or create a drawing to switch layouts.",
t!("Open or create a drawing to switch layouts."),
),
)
.into()
@ -960,11 +965,11 @@ fn space_mode_btn(current_layout: &str, in_mspace: bool) -> Element<'static, Mes
// PAPER = currently in paper-space editing
// MODEL = currently in model-space editing (either the Model tab or MSPACE)
let (label, active, on_press) = if is_model_tab {
("MODEL", false, None::<Message>)
(t!("MODEL"), false, None::<Message>)
} else if in_mspace {
("MODEL", true, Some(Message::ExitViewport))
(t!("MODEL"), true, Some(Message::ExitViewport))
} else {
("PAPER", false, Some(Message::MspaceCommand))
(t!("PAPER"), false, Some(Message::MspaceCommand))
};
let clickable = on_press.is_some();
@ -1002,12 +1007,12 @@ fn status_pill(label: impl Into<String>) -> Element<'static, Message> {
// ── Scale popup button ────────────────────────────────────────────────────
/// Shared visual root for labelled status-bar menus (units, scale, …).
fn popup_pill(label: &str) -> Element<'static, Message> {
fn popup_pill(label: impl Into<String>) -> Element<'static, Message> {
action_pill(label, Message::StatusMenuTooltipHidden(true))
}
fn action_pill(label: &str, msg: Message) -> Element<'static, Message> {
let label = label.to_string();
fn action_pill(label: impl Into<String>, msg: Message) -> Element<'static, Message> {
let label = label.into();
button(text(label).size(12))
.on_press(msg)
.style(button::subtle)

View file

@ -6,13 +6,14 @@ use iced::{Background, Element, Fill, Theme};
use crate::app::Message;
use crate::ui::statusbar::statusbar_config::{StatusBarConfig, StatusPill};
use crate::ui::statusbar::status_menu::Entry;
use crate::t;
pub fn customization_entries(config: &StatusBarConfig) -> Vec<Entry<'static>> {
StatusPill::ALL
.iter()
.map(|&pill| {
Entry::stay(menu_row(
pill.label(),
t!(pill.label()),
config.is_visible(pill),
Message::ToggleStatusPill(pill),
))
@ -48,7 +49,7 @@ fn layout_row<'a>(name: String, is_current: bool) -> Element<'a, Message> {
.into()
}
fn menu_row(label: &'static str, checked: bool, msg: Message) -> Element<'static, Message> {
fn menu_row(label: std::borrow::Cow<'static, str>, checked: bool, msg: Message) -> Element<'static, Message> {
let check = crate::ui::icons::themed_check_cell(checked);
let lbl = text(label).size(11);

View file

@ -10,6 +10,7 @@ use crate::app::Message;
use crate::ui::style::style_manager::{hdivider, muted_text_style, tb_button};
use iced::widget::{column, container, mouse_area, row, scrollable, text, Space};
use iced::{Background, Border, Element, Theme};
use crate::t;
/// `scales` is `(name, "paper:drawing" ratio, is_member)`. Every label is cloned
/// into the widget tree, so the returned element borrows nothing from the args.
@ -20,9 +21,9 @@ pub fn view_window(
) -> Element<'static, Message> {
let toolbar = container(
row![
text(format!("Object: {object_label}")).size(11),
text(t!("Object: %{object_label}", object_label = object_label)).size(11),
Space::new().width(sizing.width),
tb_button("Close", Message::CloseModal, true),
tb_button(t!("Close"), Message::CloseModal, true),
]
.spacing(4)
.align_y(iced::Center),
@ -75,7 +76,7 @@ pub fn view_window(
let body = container(
column![
text("Click a scale to add or remove the object's representation for it.")
text(t!("Click a scale to add or remove the object's representation for it."))
.size(10)
.style(muted_text_style),
list,

View file

@ -5,6 +5,21 @@ use iced::widget::{
button, checkbox, column, container, row, scrollable, text, text_input, Space,
};
use iced::{Background, Border, Element, Theme};
use crate::t;
use std::borrow::Cow;
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
struct DimEnumChoice {
code: String,
label: String,
}
impl fmt::Display for DimEnumChoice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.label)
}
}
/// All DimStyle field values needed by the view.
pub struct DimStyleValues<'a> {
@ -173,34 +188,34 @@ pub fn view_window<'a>(
) -> Element<'a, Message> {
// ── Tab bar ───────────────────────────────────────────────────────────
let tabs = row![
button(text("Lines").size(11))
button(text(t!("Lines")).size(11))
.on_press(Message::DimStyleDialogTab(0))
.style(tab_btn_style(tab == 0))
.padding([4, 10]),
button(text("Arrows").size(11))
button(text(t!("Arrows")).size(11))
.on_press(Message::DimStyleDialogTab(1))
.style(tab_btn_style(tab == 1))
.padding([4, 10]),
button(text("Text").size(11))
button(text(t!("Text")).size(11))
.on_press(Message::DimStyleDialogTab(2))
.style(tab_btn_style(tab == 2))
.padding([4, 10]),
button(text("Scale/Units").size(11))
button(text(t!("Scale/Units")).size(11))
.on_press(Message::DimStyleDialogTab(3))
.style(tab_btn_style(tab == 3))
.padding([4, 10]),
button(text("Tolerances").size(11))
button(text(t!("Tolerances")).size(11))
.on_press(Message::DimStyleDialogTab(4))
.style(tab_btn_style(tab == 4))
.padding([4, 10]),
button(text("Alternate").size(11))
button(text(t!("Alternate")).size(11))
.on_press(Message::DimStyleDialogTab(5))
.style(tab_btn_style(tab == 5))
.padding([4, 10]),
]
.spacing(2);
let lbl = |s: &'static str| text(s).size(11).style(muted_style).width(180);
let lbl = |s: Cow<'static, str>| text(s).size(11).style(muted_style).width(180);
let mk_field = |fld: DsField, val: &'a str| -> Element<'a, Message> {
text_input("", val)
@ -211,7 +226,7 @@ pub fn view_window<'a>(
.into()
};
let chk = |label: &'static str, val: bool, fld: DsField| -> Element<'a, Message> {
let chk = |label: Cow<'static, str>, val: bool, fld: DsField| -> Element<'a, Message> {
checkbox(val)
.label(label)
.on_toggle(move |_| Message::DsToggle(fld.clone()))
@ -222,27 +237,31 @@ pub fn view_window<'a>(
// Enum dropdown: maps the stored integer code to a named option and back,
// so the user picks "Above" rather than typing "1".
let enum_field = move |label: &'static str,
let enum_field = move |label: Cow<'static, str>,
fld: DsField,
val: &'a str,
opts: &'static [(&'static str, &'static str)]|
-> Element<'a, Message> {
let labels: Vec<String> = opts.iter().map(|(_, l)| (*l).to_string()).collect();
let cur = opts
let options: Vec<DimEnumChoice> = opts
.iter()
.find(|(c, _)| *c == val.trim())
.map(|(_, l)| (*l).to_string())
.unwrap_or_else(|| val.to_string());
.map(|(code, label)| DimEnumChoice {
code: (*code).to_string(),
label: crate::i18n::translate(label).into_owned(),
})
.collect();
let cur = options
.iter()
.find(|choice| choice.code == val.trim())
.cloned()
.unwrap_or_else(|| DimEnumChoice {
code: val.to_string(),
label: val.to_string(),
});
row![
lbl(label),
iced::widget::pick_list(Some(cur), labels, |value| value.to_string())
.on_select(move |chosen| {
let code = opts
.iter()
.find(|(_, l)| *l == chosen.as_str())
.map(|(c, _)| (*c).to_string())
.unwrap_or(chosen);
Message::DsEdit(fld.clone(), code)
iced::widget::pick_list(Some(cur), options, |value| value.to_string())
.on_select(move |chosen: DimEnumChoice| {
Message::DsEdit(fld.clone(), chosen.code)
})
.text_size(11)
.width(150),
@ -275,7 +294,7 @@ pub fn view_window<'a>(
// Shared colour selector (main dropdown + "more" palette), reusing the
// existing DsEdit path (the chosen colour is sent as an ACI string).
let color_open = vals.color_open.clone();
let color_row = move |label: &'static str, fld: DsField, _val: &'a str| -> Element<'a, Message> {
let color_row = move |label: Cow<'static, str>, fld: DsField, _val: &'a str| -> Element<'a, Message> {
let cur = crate::ui::color_select::aci_string_to_color(_val);
let open = color_open.as_ref() == Some(&fld);
let f_sel = fld.clone();
@ -301,7 +320,7 @@ pub fn view_window<'a>(
// Block / linetype Handle dropdown: pick a block-record (arrowheads) or a
// linetype by name from the available records.
let hrow = move |label: &'static str,
let hrow = move |label: Cow<'static, str>,
options: Vec<String>,
selected: String,
field: &'static str|
@ -320,81 +339,81 @@ pub fn view_window<'a>(
let tab_content: Element<'_, Message> = match tab {
0 => column![
text("Dimension Line").size(11).style(primary_style),
text(t!("Dimension Line")).size(11).style(primary_style),
row![
lbl("Extension (DIMDLE)"),
lbl(t!("Extension (DIMDLE)")),
mk_field(DsField::Dimdle, vals.dimdle)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Spacing (DIMDLI)"),
lbl(t!("Spacing (DIMDLI)")),
mk_field(DsField::Dimdli, vals.dimdli)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Text gap (DIMGAP)"),
lbl(t!("Text gap (DIMGAP)")),
mk_field(DsField::Dimgap, vals.dimgap)
]
.spacing(8)
.align_y(iced::Center),
chk("Suppress 1st line (DIMSD1)", vals.dimsd1, DsField::Dimsd1),
chk("Suppress 2nd line (DIMSD2)", vals.dimsd2, DsField::Dimsd2),
text("Extension Line").size(11).style(primary_style),
chk(t!("Suppress 1st line (DIMSD1)"), vals.dimsd1, DsField::Dimsd1),
chk(t!("Suppress 2nd line (DIMSD2)"), vals.dimsd2, DsField::Dimsd2),
text(t!("Extension Line")).size(11).style(primary_style),
row![
lbl("Extension (DIMEXE)"),
lbl(t!("Extension (DIMEXE)")),
mk_field(DsField::Dimexe, vals.dimexe)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Offset (DIMEXO)"),
lbl(t!("Offset (DIMEXO)")),
mk_field(DsField::Dimexo, vals.dimexo)
]
.spacing(8)
.align_y(iced::Center),
chk("Suppress 1st line (DIMSE1)", vals.dimse1, DsField::Dimse1),
chk("Suppress 2nd line (DIMSE2)", vals.dimse2, DsField::Dimse2),
color_row("Dim line color ACI (DIMCLRD)", DsField::Dimclrd, vals.dimclrd),
chk(t!("Suppress 1st line (DIMSE1)"), vals.dimse1, DsField::Dimse1),
chk(t!("Suppress 2nd line (DIMSE2)"), vals.dimse2, DsField::Dimse2),
color_row(t!("Dim line color ACI (DIMCLRD)"), DsField::Dimclrd, vals.dimclrd),
row![
lbl("Dim line weight (DIMLWD)"),
lbl(t!("Dim line weight (DIMLWD)")),
mk_field(DsField::Dimlwd, vals.dimlwd)
]
.spacing(8)
.align_y(iced::Center),
color_row("Ext line color ACI (DIMCLRE)", DsField::Dimclre, vals.dimclre),
color_row(t!("Ext line color ACI (DIMCLRE)"), DsField::Dimclre, vals.dimclre),
row![
lbl("Ext line weight (DIMLWE)"),
lbl(t!("Ext line weight (DIMLWE)")),
mk_field(DsField::Dimlwe, vals.dimlwe)
]
.spacing(8)
.align_y(iced::Center),
chk(
"Fixed-length ext lines (DIMFXLON)",
t!("Fixed-length ext lines (DIMFXLON)"),
vals.dimfxlon,
DsField::Dimfxlon
),
row![
lbl("Fixed length (DIMFXL)"),
lbl(t!("Fixed length (DIMFXL)")),
mk_field(DsField::Dimfxl, vals.dimfxl)
]
.spacing(8)
.align_y(iced::Center),
hrow(
"Dim line linetype (DIMLTYPE)",
t!("Dim line linetype (DIMLTYPE)"),
vals.lt_opts.clone(),
vals.dimltex_name.clone(),
"dimltex_handle"
),
hrow(
"Ext line 1 linetype (DIMLTEX1)",
t!("Ext line 1 linetype (DIMLTEX1)"),
vals.lt_opts.clone(),
vals.dimltex1_name.clone(),
"dimltex1_handle"
),
hrow(
"Ext line 2 linetype (DIMLTEX2)",
t!("Ext line 2 linetype (DIMLTEX2)"),
vals.lt_opts.clone(),
vals.dimltex2_name.clone(),
"dimltex2_handle"
@ -403,62 +422,62 @@ pub fn view_window<'a>(
.spacing(7)
.into(),
1 => column![
text("Arrows").size(11).style(primary_style),
text(t!("Arrows")).size(11).style(primary_style),
hrow(
"Arrowhead (DIMBLK)",
t!("Arrowhead (DIMBLK)"),
vals.block_opts.clone(),
vals.dimblk_name.clone(),
"dimblk"
),
hrow(
"1st arrowhead (DIMBLK1)",
t!("1st arrowhead (DIMBLK1)"),
vals.block_opts.clone(),
vals.dimblk1_name.clone(),
"dimblk1"
),
hrow(
"2nd arrowhead (DIMBLK2)",
t!("2nd arrowhead (DIMBLK2)"),
vals.block_opts.clone(),
vals.dimblk2_name.clone(),
"dimblk2"
),
hrow(
"Leader arrowhead (DIMLDRBLK)",
t!("Leader arrowhead (DIMLDRBLK)"),
vals.block_opts.clone(),
vals.dimldrblk_name.clone(),
"dimldrblk"
),
row![
lbl("Arrow size (DIMASZ)"),
lbl(t!("Arrow size (DIMASZ)")),
mk_field(DsField::Dimasz, vals.dimasz)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Center mark (DIMCEN)"),
lbl(t!("Center mark (DIMCEN)")),
mk_field(DsField::Dimcen, vals.dimcen)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Tick size (DIMTSZ)"),
lbl(t!("Tick size (DIMTSZ)")),
mk_field(DsField::Dimtsz, vals.dimtsz)
]
.spacing(8)
.align_y(iced::Center),
chk(
"Separate arrow blocks (DIMSAH)",
t!("Separate arrow blocks (DIMSAH)"),
vals.dimsah,
DsField::Dimsah
),
enum_field(
"Arc length symbol (DIMARCSYM)",
t!("Arc length symbol (DIMARCSYM)"),
DsField::Dimarcsym,
vals.dimarcsym,
&[("0", "Before text"), ("1", "Above text"), ("2", "None")],
),
row![
lbl("Jog angle ° (DIMJOGANG)"),
lbl(t!("Jog angle ° (DIMJOGANG)")),
mk_field(DsField::Dimjogang, vals.dimjogang)
]
.spacing(8)
@ -467,21 +486,21 @@ pub fn view_window<'a>(
.spacing(7)
.into(),
2 => column![
text("Text").size(11).style(primary_style),
text(t!("Text")).size(11).style(primary_style),
row![
lbl("Height (DIMTXT)"),
lbl(t!("Height (DIMTXT)")),
mk_field(DsField::Dimtxt, vals.dimtxt)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Style (DIMTXSTY)"),
lbl(t!("Style (DIMTXSTY)")),
mk_field(DsField::Dimtxsty, vals.dimtxsty)
]
.spacing(8)
.align_y(iced::Center),
enum_field(
"Vertical pos (DIMTAD)",
t!("Vertical pos (DIMTAD)"),
DsField::Dimtad,
vals.dimtad,
&[
@ -492,11 +511,11 @@ pub fn view_window<'a>(
("4", "Below"),
],
),
chk("Horizontal inside (DIMTIH)", vals.dimtih, DsField::Dimtih),
chk("Horizontal outside (DIMTOH)", vals.dimtoh, DsField::Dimtoh),
color_row("Text color ACI (DIMCLRT)", DsField::Dimclrt, vals.dimclrt),
chk(t!("Horizontal inside (DIMTIH)"), vals.dimtih, DsField::Dimtih),
chk(t!("Horizontal outside (DIMTOH)"), vals.dimtoh, DsField::Dimtoh),
color_row(t!("Text color ACI (DIMCLRT)"), DsField::Dimclrt, vals.dimclrt),
enum_field(
"Horizontal just (DIMJUST)",
t!("Horizontal just (DIMJUST)"),
DsField::Dimjust,
vals.dimjust,
&[
@ -508,13 +527,13 @@ pub fn view_window<'a>(
],
),
row![
lbl("Vertical pos (DIMTVP)"),
lbl(t!("Vertical pos (DIMTVP)")),
mk_field(DsField::Dimtvp, vals.dimtvp)
]
.spacing(8)
.align_y(iced::Center),
enum_field(
"Text fill mode (DIMTFILL)",
t!("Text fill mode (DIMTFILL)"),
DsField::Dimtfill,
vals.dimtfill,
&[
@ -523,9 +542,9 @@ pub fn view_window<'a>(
("2", "Color"),
],
),
color_row("Fill color ACI (DIMTFILLCLR)", DsField::Dimtfillclr, vals.dimtfillclr),
color_row(t!("Fill color ACI (DIMTFILLCLR)"), DsField::Dimtfillclr, vals.dimtfillclr),
chk(
"Left-to-right (DIMTXTDIRECTION)",
t!("Left-to-right (DIMTXTDIRECTION)"),
vals.dimtxtdirection,
DsField::Dimtxtdirection
),
@ -533,10 +552,10 @@ pub fn view_window<'a>(
.spacing(7)
.into(),
3 => column![
text("Scale").size(11).style(primary_style),
chk("Annotative", vals.annotative, DsField::Annotative),
text(t!("Scale")).size(11).style(primary_style),
chk(t!("Annotative"), vals.annotative, DsField::Annotative),
row![
lbl("Overall scale (DIMSCALE)"),
lbl(t!("Overall scale (DIMSCALE)")),
if vals.annotative {
text_input("", "0")
.style(field_style)
@ -550,40 +569,40 @@ pub fn view_window<'a>(
.spacing(8)
.align_y(iced::Center),
row![
lbl("Linear factor (DIMLFAC)"),
lbl(t!("Linear factor (DIMLFAC)")),
mk_field(DsField::Dimlfac, vals.dimlfac)
]
.spacing(8)
.align_y(iced::Center),
text("Units").size(11).style(primary_style),
enum_field("Format (DIMLUNIT)", DsField::Dimlunit, vals.dimlunit, OPT_LUNIT),
text(t!("Units")).size(11).style(primary_style),
enum_field(t!("Format (DIMLUNIT)"), DsField::Dimlunit, vals.dimlunit, OPT_LUNIT),
row![
lbl("Decimals (DIMDEC)"),
lbl(t!("Decimals (DIMDEC)")),
mk_field(DsField::Dimdec, vals.dimdec)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Suffix (DIMPOST)"),
lbl(t!("Suffix (DIMPOST)")),
mk_field(DsField::Dimpost, vals.dimpost)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Decimal sep ASCII (DIMDSEP)"),
lbl(t!("Decimal sep ASCII (DIMDSEP)")),
mk_field(DsField::Dimdsep, vals.dimdsep)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Round off (DIMRND)"),
lbl(t!("Round off (DIMRND)")),
mk_field(DsField::Dimrnd, vals.dimrnd)
]
.spacing(8)
.align_y(iced::Center),
enum_field("Zero suppress (DIMZIN)", DsField::Dimzin, vals.dimzin, OPT_ZIN),
enum_field(t!("Zero suppress (DIMZIN)"), DsField::Dimzin, vals.dimzin, OPT_ZIN),
enum_field(
"Fraction format (DIMFRAC)",
t!("Fraction format (DIMFRAC)"),
DsField::Dimfrac,
vals.dimfrac,
&[
@ -593,7 +612,7 @@ pub fn view_window<'a>(
],
),
enum_field(
"Angular unit (DIMAUNIT)",
t!("Angular unit (DIMAUNIT)"),
DsField::Dimaunit,
vals.dimaunit,
&[
@ -604,19 +623,19 @@ pub fn view_window<'a>(
],
),
row![
lbl("Angular decimals (DIMADEC)"),
lbl(t!("Angular decimals (DIMADEC)")),
mk_field(DsField::Dimadec, vals.dimadec)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Unit format (DIMUNIT)"),
lbl(t!("Unit format (DIMUNIT)")),
mk_field(DsField::Dimunit, vals.dimunit)
]
.spacing(8)
.align_y(iced::Center),
enum_field(
"Angular zero supp (DIMAZIN)",
t!("Angular zero supp (DIMAZIN)"),
DsField::Dimazin,
vals.dimazin,
&[
@ -626,9 +645,9 @@ pub fn view_window<'a>(
("3", "Leading & trailing"),
],
),
text("Fit").size(11).style(primary_style),
text(t!("Fit")).size(11).style(primary_style),
enum_field(
"Fit (DIMATFIT)",
t!("Fit (DIMATFIT)"),
DsField::Dimatfit,
vals.dimatfit,
&[
@ -639,7 +658,7 @@ pub fn view_window<'a>(
],
),
enum_field(
"Text movement (DIMTMOVE)",
t!("Text movement (DIMTMOVE)"),
DsField::Dimtmove,
vals.dimtmove,
&[
@ -649,20 +668,20 @@ pub fn view_window<'a>(
],
),
row![
lbl("Fit (legacy DIMFIT)"),
lbl(t!("Fit (legacy DIMFIT)")),
mk_field(DsField::Dimfit, vals.dimfit)
]
.spacing(8)
.align_y(iced::Center),
chk("Force text inside (DIMTIX)", vals.dimtix, DsField::Dimtix),
chk(t!("Force text inside (DIMTIX)"), vals.dimtix, DsField::Dimtix),
chk(
"Suppress outside arrows (DIMSOXD)",
t!("Suppress outside arrows (DIMSOXD)"),
vals.dimsoxd,
DsField::Dimsoxd
),
chk("Place text manually (DIMUPT)", vals.dimupt, DsField::Dimupt),
chk(t!("Place text manually (DIMUPT)"), vals.dimupt, DsField::Dimupt),
chk(
"Dim line between ext (DIMTOFL)",
t!("Dim line between ext (DIMTOFL)"),
vals.dimtofl,
DsField::Dimtofl
),
@ -670,46 +689,46 @@ pub fn view_window<'a>(
.spacing(7)
.into(),
5 => column![
text("Alternate Units").size(11).style(primary_style),
text(t!("Alternate Units")).size(11).style(primary_style),
chk(
"Enable alternate units (DIMALT)",
t!("Enable alternate units (DIMALT)"),
vals.dimalt,
DsField::Dimalt
),
row![
lbl("Multiplier (DIMALTF)"),
lbl(t!("Multiplier (DIMALTF)")),
mk_field(DsField::Dimaltf, vals.dimaltf)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Decimals (DIMALTD)"),
lbl(t!("Decimals (DIMALTD)")),
mk_field(DsField::Dimaltd, vals.dimaltd)
]
.spacing(8)
.align_y(iced::Center),
enum_field("Unit format (DIMALTU)", DsField::Dimaltu, vals.dimaltu, OPT_LUNIT),
enum_field(t!("Unit format (DIMALTU)"), DsField::Dimaltu, vals.dimaltu, OPT_LUNIT),
row![
lbl("Tol decimals (DIMALTTD)"),
lbl(t!("Tol decimals (DIMALTTD)")),
mk_field(DsField::Dimalttd, vals.dimalttd)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Round off (DIMALTRND)"),
lbl(t!("Round off (DIMALTRND)")),
mk_field(DsField::Dimaltrnd, vals.dimaltrnd)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Suffix (DIMAPOST)"),
lbl(t!("Suffix (DIMAPOST)")),
mk_field(DsField::Dimapost, vals.dimapost)
]
.spacing(8)
.align_y(iced::Center),
enum_field("Zero suppress (DIMALTZ)", DsField::Dimaltz, vals.dimaltz, OPT_ZIN),
enum_field(t!("Zero suppress (DIMALTZ)"), DsField::Dimaltz, vals.dimaltz, OPT_ZIN),
enum_field(
"Tol zero supp (DIMALTTZ)",
t!("Tol zero supp (DIMALTTZ)"),
DsField::Dimalttz,
vals.dimalttz,
OPT_ZIN,
@ -718,40 +737,40 @@ pub fn view_window<'a>(
.spacing(7)
.into(),
_ => column![
text("Tolerances").size(11).style(primary_style),
chk("Generate tolerances (DIMTOL)", vals.dimtol, DsField::Dimtol),
chk("Limits generation (DIMLIM)", vals.dimlim, DsField::Dimlim),
text(t!("Tolerances")).size(11).style(primary_style),
chk(t!("Generate tolerances (DIMTOL)"), vals.dimtol, DsField::Dimtol),
chk(t!("Limits generation (DIMLIM)"), vals.dimlim, DsField::Dimlim),
row![
lbl("Plus tolerance (DIMTP)"),
lbl(t!("Plus tolerance (DIMTP)")),
mk_field(DsField::Dimtp, vals.dimtp)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Minus tolerance (DIMTM)"),
lbl(t!("Minus tolerance (DIMTM)")),
mk_field(DsField::Dimtm, vals.dimtm)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Tol. decimals (DIMTDEC)"),
lbl(t!("Tol. decimals (DIMTDEC)")),
mk_field(DsField::Dimtdec, vals.dimtdec)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Tol. scale (DIMTFAC)"),
lbl(t!("Tol. scale (DIMTFAC)")),
mk_field(DsField::Dimtfac, vals.dimtfac)
]
.spacing(8)
.align_y(iced::Center),
enum_field(
"Tol. vert just (DIMTOLJ)",
t!("Tol. vert just (DIMTOLJ)"),
DsField::Dimtolj,
vals.dimtolj,
&[("0", "Bottom"), ("1", "Middle"), ("2", "Top")],
),
enum_field("Tol. zero supp (DIMTZIN)", DsField::Dimtzin, vals.dimtzin, OPT_ZIN),
enum_field(t!("Tol. zero supp (DIMTZIN)"), DsField::Dimtzin, vals.dimtzin, OPT_ZIN),
]
.spacing(7)
.into(),
@ -763,7 +782,7 @@ pub fn view_window<'a>(
// content container instead of the panel padding.
let right_panel = container(
column![
text(format!("Editing: {selected}")).size(11).style(muted_style),
text(t!("Editing: %{selected}", selected = selected)).size(11).style(muted_style),
tabs,
hdivider(sizing.width),
scrollable(container(tab_content).padding([12, 12]).width(sizing.width))

View file

@ -5,6 +5,18 @@ use iced::widget::{
button, checkbox, column, container, row, scrollable, text, text_input,
};
use iced::{Background, Border, Element, Theme};
use crate::t;
use std::borrow::Cow;
use std::fmt;
#[derive(Clone, Debug, Eq, PartialEq)]
struct EnumChoice(String);
impl fmt::Display for EnumChoice {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(crate::i18n::translate(&self.0).as_ref())
}
}
fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
move |theme: &Theme, st| {
@ -83,12 +95,12 @@ pub struct MLeaderStyleView<'a> {
pub color_open: Option<&'static str>,
}
fn section<'a>(label: &'static str) -> Element<'a, Message> {
fn section<'a>(label: Cow<'static, str>) -> Element<'a, Message> {
text(label).size(11).style(primary_style).into()
}
fn num_row<'a>(
label: &'static str,
label: Cow<'static, str>,
placeholder: &'static str,
value: &'a str,
field: &'static str,
@ -105,7 +117,10 @@ fn num_row<'a>(
.into()
}
fn readonly_num_row<'a>(label: &'static str, value: &'a str) -> Element<'a, Message> {
fn readonly_num_row<'a>(
label: Cow<'static, str>,
value: Cow<'static, str>,
) -> Element<'a, Message> {
row![
text(label).size(11).style(muted_style).width(150),
text(value).size(11).style(muted_style),
@ -118,7 +133,7 @@ fn readonly_num_row<'a>(label: &'static str, value: &'a str) -> Element<'a, Mess
/// Shared colour selector row. Reuses MLeaderStyleEdit by sending the chosen
/// colour as an ACI string; `open` shows the expanded palette.
fn color_row<'a>(
label: &'static str,
label: Cow<'static, str>,
value: &'a str,
field: &'static str,
open: bool,
@ -148,15 +163,15 @@ fn color_row<'a>(
}
fn enum_row<'a>(
label: &'static str,
options: Vec<String>,
label: Cow<'static, str>,
options: Vec<EnumChoice>,
selected: String,
field: &'static str,
) -> Element<'a, Message> {
row![
text(label).size(11).style(muted_style).width(150),
iced::widget::pick_list(Some(selected), options, |value| value.to_string())
.on_select(move |value| Message::MLeaderStyleSetEnum { field, value })
iced::widget::pick_list(Some(EnumChoice(selected)), options, |value| value.to_string())
.on_select(move |choice| Message::MLeaderStyleSetEnum { field, value: choice.0 })
.text_size(11)
.width(190),
]
@ -168,7 +183,7 @@ fn enum_row<'a>(
fn lineweight_row<'a>(selected: acadrust::types::LineWeight) -> Element<'a, Message> {
let selected = crate::ui::properties::LwItem(selected);
row![
text("Line weight:").size(11).style(muted_style).width(150),
text(t!("Line weight:")).size(11).style(muted_style).width(150),
iced::widget::pick_list(
Some(selected),
crate::ui::properties::lw_options(),
@ -198,14 +213,14 @@ const ATTACH_OPTS: [&str; 11] = [
"CenterOfTextOverline",
];
fn opts(list: &[&str]) -> Vec<String> {
list.iter().map(|s| s.to_string()).collect()
fn opts(list: &[&str]) -> Vec<EnumChoice> {
list.iter().map(|value| EnumChoice((*value).to_string())).collect()
}
/// Dropdown for an Option<Handle> field (linetype / arrowhead / text style /
/// block content). Options are record names; "None" clears the handle.
fn handle_row<'a>(
label: &'static str,
label: Cow<'static, str>,
options: Vec<String>,
selected: String,
field: &'static str,
@ -222,7 +237,7 @@ fn handle_row<'a>(
.into()
}
fn chk<'a>(label: &'static str, val: bool, field: &'static str) -> Element<'a, Message> {
fn chk<'a>(label: Cow<'static, str>, val: bool, field: &'static str) -> Element<'a, Message> {
checkbox(val)
.label(label)
.on_toggle(move |_| Message::MLeaderStyleToggle(field))
@ -240,174 +255,174 @@ pub fn view_window<'a>(
scrollable(
column![
row![
text("Name:").size(11).style(muted_style).width(150),
text(t!("Name:")).size(11).style(muted_style).width(150),
text(s.name.clone()).size(11),
]
.spacing(8),
num_row("Description:", "", v.description, "description"),
num_row(t!("Description:"), "", v.description, "description"),
// Leader Format
section("Leader Format"),
section(t!("Leader Format")),
enum_row(
"Path type:",
t!("Path type:"),
opts(&["Invisible", "StraightLineSegments", "Spline"]),
format!("{:?}", s.path_type),
"path_type"
),
color_row("Line color (ACI):", v.line_color, "line_color", v.color_open == Some("line_color")),
color_row(t!("Line color (ACI):"), v.line_color, "line_color", v.color_open == Some("line_color")),
lineweight_row(s.line_weight),
handle_row(
"Line type:",
t!("Line type:"),
v.lt_opts.clone(),
v.line_type_name.clone(),
"line_type_handle"
),
handle_row(
"Arrowhead block:",
t!("Arrowhead block:"),
v.block_opts.clone(),
v.arrowhead_name.clone(),
"arrowhead_handle"
),
num_row(
"Arrowhead size:",
t!("Arrowhead size:"),
"0.18",
v.arrowhead_size,
"arrowhead_size"
),
num_row("Break gap size:", "0.125", v.break_gap, "break_gap"),
num_row(t!("Break gap size:"), "0.125", v.break_gap, "break_gap"),
// Leader Structure
section("Leader Structure"),
chk("Enable landing", s.enable_landing, "enable_landing"),
chk("Enable dogleg", s.enable_dogleg, "enable_dogleg"),
section(t!("Leader Structure")),
chk(t!("Enable landing"), s.enable_landing, "enable_landing"),
chk(t!("Enable dogleg"), s.enable_dogleg, "enable_dogleg"),
num_row(
"Landing distance:",
t!("Landing distance:"),
"8.0",
v.landing_distance,
"landing_distance"
),
num_row("Landing gap:", "0.09", v.landing_gap, "landing_gap"),
num_row("Max leader points:", "2", v.max_points, "max_points"),
num_row(t!("Landing gap:"), "0.09", v.landing_gap, "landing_gap"),
num_row(t!("Max leader points:"), "2", v.max_points, "max_points"),
num_row(
"First seg. angle:",
t!("First seg. angle:"),
"0",
v.first_seg_angle,
"first_seg_angle"
),
num_row(
"Second seg. angle:",
t!("Second seg. angle:"),
"0",
v.second_seg_angle,
"second_seg_angle"
),
if s.is_annotative {
readonly_num_row("Scale factor:", "By annotation scale")
readonly_num_row(t!("Scale factor:"), t!("By annotation scale"))
} else {
num_row("Scale factor:", "1.0", v.scale_factor, "scale_factor")
num_row(t!("Scale factor:"), "1.0", v.scale_factor, "scale_factor")
},
num_row("Align space:", "4.0", v.align_space, "align_space"),
num_row(t!("Align space:"), "4.0", v.align_space, "align_space"),
enum_row(
"Leader draw order:",
t!("Leader draw order:"),
opts(&["LeaderHeadFirst", "LeaderTailFirst"]),
format!("{:?}", s.leader_draw_order),
"leader_draw_order"
),
enum_row(
"Multileader draw order:",
t!("Multileader draw order:"),
opts(&["ContentFirst", "LeaderFirst"]),
format!("{:?}", s.multileader_draw_order),
"multileader_draw_order"
),
chk("Annotative", s.is_annotative, "annotative"),
chk(t!("Annotative"), s.is_annotative, "annotative"),
// Content
section("Content"),
section(t!("Content")),
enum_row(
"Content type:",
t!("Content type:"),
opts(&["None", "Block", "MText", "Tolerance"]),
format!("{:?}", s.content_type),
"content_type"
),
num_row("Default text:", "", v.default_text, "default_text"),
num_row(t!("Default text:"), "", v.default_text, "default_text"),
handle_row(
"Text style:",
t!("Text style:"),
v.textstyle_opts.clone(),
v.text_style_name.clone(),
"text_style_handle"
),
num_row("Text height:", "0.18", v.text_height, "text_height"),
color_row("Text color (ACI):", v.text_color, "text_color", v.color_open == Some("text_color")),
num_row(t!("Text height:"), "0.18", v.text_height, "text_height"),
color_row(t!("Text color (ACI):"), v.text_color, "text_color", v.color_open == Some("text_color")),
enum_row(
"Text angle:",
t!("Text angle:"),
opts(&["ParallelToLastLeaderLine", "Horizontal", "Optimized"]),
format!("{:?}", s.text_angle_type),
"text_angle_type"
),
enum_row(
"Text alignment:",
t!("Text alignment:"),
opts(&["Left", "Center", "Right"]),
format!("{:?}", s.text_alignment),
"text_alignment"
),
enum_row(
"Left attachment:",
t!("Left attachment:"),
opts(&ATTACH_OPTS),
format!("{:?}", s.text_left_attachment),
"text_left_attachment"
),
enum_row(
"Right attachment:",
t!("Right attachment:"),
opts(&ATTACH_OPTS),
format!("{:?}", s.text_right_attachment),
"text_right_attachment"
),
enum_row(
"Top attachment:",
t!("Top attachment:"),
opts(&ATTACH_OPTS),
format!("{:?}", s.text_top_attachment),
"text_top_attachment"
),
enum_row(
"Bottom attachment:",
t!("Bottom attachment:"),
opts(&ATTACH_OPTS),
format!("{:?}", s.text_bottom_attachment),
"text_bottom_attachment"
),
enum_row(
"Attachment direction:",
t!("Attachment direction:"),
opts(&["Horizontal", "Vertical"]),
format!("{:?}", s.text_attachment_direction),
"text_attachment_direction"
),
chk("Text frame", s.text_frame, "text_frame"),
chk("Text always left", s.text_always_left, "text_always_left"),
chk(t!("Text frame"), s.text_frame, "text_frame"),
chk(t!("Text always left"), s.text_always_left, "text_always_left"),
// Block Content
section("Block Content"),
section(t!("Block Content")),
handle_row(
"Block:",
t!("Block:"),
v.block_opts.clone(),
v.block_content_name.clone(),
"block_content_handle"
),
color_row("Block color (ACI):", v.block_color, "block_color", v.color_open == Some("block_color")),
color_row(t!("Block color (ACI):"), v.block_color, "block_color", v.color_open == Some("block_color")),
enum_row(
"Block connection:",
t!("Block connection:"),
opts(&["BlockExtents", "BasePoint"]),
format!("{:?}", s.block_content_connection),
"block_content_connection"
),
num_row("Block rotation:", "0", v.block_rotation, "block_rotation"),
num_row("Block scale X:", "1.0", v.block_scale_x, "block_scale_x"),
num_row("Block scale Y:", "1.0", v.block_scale_y, "block_scale_y"),
num_row("Block scale Z:", "1.0", v.block_scale_z, "block_scale_z"),
num_row(t!("Block rotation:"), "0", v.block_rotation, "block_rotation"),
num_row(t!("Block scale X:"), "1.0", v.block_scale_x, "block_scale_x"),
num_row(t!("Block scale Y:"), "1.0", v.block_scale_y, "block_scale_y"),
num_row(t!("Block scale Z:"), "1.0", v.block_scale_z, "block_scale_z"),
chk(
"Enable block scale",
t!("Enable block scale"),
s.enable_block_scale,
"enable_block_scale"
),
chk(
"Enable block rotation",
t!("Enable block rotation"),
s.enable_block_rotation,
"enable_block_rotation"
),
button(text("Apply").size(11))
button(text(t!("Apply")).size(11))
.on_press(Message::MLeaderStyleApply)
.style(btn_s(true))
.padding([4, 14]),
@ -419,7 +434,7 @@ pub fn view_window<'a>(
.height(sizing.height)
.into()
} else {
container(text("Select a style to view details.").size(11).style(muted_style))
container(text(t!("Select a style to view details.")).size(11).style(muted_style))
.padding([12, 12])
.into()
};

View file

@ -3,6 +3,8 @@
use crate::app::Message;
use iced::widget::{column, container, row, scrollable, text};
use iced::{Element, Theme};
use crate::t;
use std::borrow::Cow;
fn muted_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
@ -20,7 +22,7 @@ pub fn view_window<'a>(
sizing: crate::ui::modal::ModalSizing,
) -> Element<'a, Message> {
// ── Right: Details panel ──────────────────────────────────────────────
let info_row = |label: &'static str, val: String| -> Element<'_, Message> {
let info_row = |label: Cow<'static, str>, val: String| -> Element<'_, Message> {
row![
text(label).size(11).style(muted_style).width(120),
text(val).size(11),
@ -61,9 +63,9 @@ pub fn view_window<'a>(
.collect();
let mut col_items: Vec<Element<'_, Message>> = vec![
info_row("Name:", s.name.clone()),
info_row("Elements:", s.elements.len().to_string()),
text(" Off Color Ltype")
info_row(t!("Name:"), s.name.clone()),
info_row(t!("Elements:"), s.elements.len().to_string()),
text(t!(" Off Color Ltype"))
.size(10)
.style(muted_style)
.into(),
@ -73,7 +75,7 @@ pub fn view_window<'a>(
.height(sizing.height)
.into()
} else {
container(text("Select a style to view details.").size(11).style(muted_style))
container(text(t!("Select a style to view details.")).size(11).style(muted_style))
.padding([12, 12])
.into()
};

View file

@ -3,6 +3,8 @@
use crate::app::Message;
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
use iced::{Background, Border, Element, Theme};
use crate::t;
use std::borrow::Cow;
fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
move |theme: &Theme, st| {
@ -89,21 +91,21 @@ pub fn view_window<'a>(
sizing: crate::ui::modal::ModalSizing,
) -> Element<'a, Message> {
let table_name = table
.map(|t| t.name.as_str())
.unwrap_or("(no table loaded)");
.map(|t| t.name.clone())
.unwrap_or_else(|| t!("(no table loaded)").into_owned());
// ── Toolbar ───────────────────────────────────────────────────────────
let toolbar = container(
row![
button(text("Load CTB/STB").size(11))
button(text(t!("Load CTB/STB")).size(11))
.on_press(Message::PlotStyleLoad)
.style(btn_s(false))
.padding([4, 10]),
button(text("Save As…").size(11))
button(text(t!("Save As…")).size(11))
.on_press(Message::PlotStylePanelSave)
.style(btn_s(false))
.padding([4, 10]),
button(text("Clear Table").size(11))
button(text(t!("Clear Table")).size(11))
.on_press(Message::PlotStyleClear)
.style(btn_s(false))
.padding([4, 10]),
@ -149,7 +151,7 @@ pub fn view_window<'a>(
let label = if has_override {
format!("{aci:>3} {color_str:<9} {lw_str}")
} else {
format!("{aci:>3} (default)")
format!("{aci:>3} {}", t!("(default)"))
};
button(text(label).size(10).font(iced::Font::MONOSPACE))
.on_press(Message::PlotStylePanelSelectAci(aci))
@ -178,7 +180,7 @@ pub fn view_window<'a>(
let aci_list = container(
column![
text("ACI Color Index").size(10).style(muted_style),
text(t!("ACI Color Index")).size(10).style(muted_style),
container(scrollable(column(aci_items).spacing(1)).height(sizing.height))
.style(|theme: &Theme| {
let palette = theme.palette();
@ -212,11 +214,11 @@ pub fn view_window<'a>(
let entry = table.and_then(|t| t.aci_entries.get(selected_aci as usize));
let cur_color = entry
.and_then(|e| e.color.map(|[r, g, b]| format!("#{r:02X}{g:02X}{b:02X}")))
.unwrap_or_else(|| "(none)".into());
.unwrap_or_else(|| t!("(none)").into());
let cur_lw = entry
.map(|e| {
if e.lineweight == 255 {
"object".into()
t!("object").into()
} else {
crate::io::plot_style::LW_TABLE
.get(e.lineweight as usize)
@ -229,41 +231,41 @@ pub fn view_window<'a>(
.map(|e| format!("{}%", e.screening))
.unwrap_or_else(|| "—".into());
let lbl = |s: &'static str| text(s).size(11).style(muted_style);
let lbl = |s: Cow<'static, str>| text(s).size(11).style(muted_style);
let edit_panel = container(
column![
row![
text("ACI:").size(11).style(muted_style).width(100),
text(t!("ACI:")).size(11).style(muted_style).width(100),
text(format!("{selected_aci}")).size(11),
]
.spacing(8)
.align_y(iced::Center),
lbl("Color override (#RRGGBB):"),
text_input("#RRGGBB or blank", color_buf)
lbl(t!("Color override (#RRGGBB):")),
text_input(t!("#RRGGBB or blank").as_ref(), color_buf)
.on_input(Message::PlotStylePanelColorBuf)
.style(field_style)
.size(11)
.padding([4, 8]),
lbl("Lineweight index (0-24, 255=obj):"),
lbl(t!("Lineweight index (0-24, 255=obj):")),
text_input("255", lw_buf)
.on_input(Message::PlotStylePanelLwBuf)
.style(field_style)
.size(11)
.padding([4, 8]),
lbl("Screening (0-100):"),
lbl(t!("Screening (0-100):")),
text_input("100", screen_buf)
.on_input(Message::PlotStylePanelScreenBuf)
.style(field_style)
.size(11)
.padding([4, 8]),
Space::new().height(8),
text("Current values:").size(10).style(muted_style),
text(format!(" Color: {cur_color}")).size(10),
text(format!(" Lineweight: {cur_lw}")).size(10),
text(format!(" Screening: {cur_scr}")).size(10),
text(t!("Current values:")).size(10).style(muted_style),
text(t!(" Color: %{cur_color}", cur_color = cur_color)).size(10),
text(t!(" Lineweight: %{cur_lw}", cur_lw = cur_lw)).size(10),
text(t!(" Screening: %{cur_scr}", cur_scr = cur_scr)).size(10),
Space::new().height(sizing.height),
button(text("Apply to ACI").size(11))
button(text(t!("Apply to ACI")).size(11))
.on_press(Message::PlotStylePanelApply)
.style(btn_s(true))
.padding([5, 10]),

View file

@ -8,6 +8,8 @@
use crate::app::Message;
use iced::widget::{button, canvas, column, container, radio, row, text, text_input, Space};
use iced::{mouse, Background, Border, Element, Length, Point, Rectangle, Size, Theme};
use crate::t;
use std::borrow::Cow;
const CELL_PX: f32 = 44.0;
@ -151,7 +153,7 @@ pub fn view_window<'a>(
}
let size_row = row![
text("Point Size:").size(13),
text(t!("Point Size:")).size(13),
Space::new().width(10),
text_input("0", size_buf)
.on_input(Message::PointStyleSizeInput)
@ -160,13 +162,13 @@ pub fn view_window<'a>(
.size(13)
.width(110),
Space::new().width(6),
text(if relative { "%" } else { "units" }).size(12).style(muted_style),
text(if relative { Cow::Borrowed("%") } else { t!("units") }).size(12).style(muted_style),
]
.align_y(iced::Center);
let radios = column![
radio(
"Set Size Relative to Screen",
t!("Set Size Relative to Screen"),
true,
Some(relative),
Message::PointStyleSizeRelative,
@ -174,7 +176,7 @@ pub fn view_window<'a>(
.size(15)
.text_size(13),
radio(
"Set Size in Absolute Units",
t!("Set Size in Absolute Units"),
false,
Some(relative),
Message::PointStyleSizeRelative,
@ -184,14 +186,14 @@ pub fn view_window<'a>(
]
.spacing(6);
let ok = button(text("OK").size(13))
let ok = button(text(t!("OK")).size(13))
.padding([5, 22])
.on_press(Message::PointStyleOk)
.style(button::primary);
container(
column![
text("Point Style").size(18),
text(t!("Point Style")).size(18),
Space::new().height(6),
grid,
Space::new().height(12),

View file

@ -13,6 +13,8 @@ use iced::widget::{
column, container, mouse_area, row, scrollable, text, text_input, Space,
};
use iced::{Background, Border, Element, Theme};
use crate::t;
use std::borrow::Cow;
/// Inline-rename text-input id, so the rename-start handler can focus it.
pub fn rename_input_id() -> iced::widget::Id {
@ -54,12 +56,12 @@ pub fn view_window<'a, 'b>(
// ── Toolbar: New / Delete | Set Current / Apply ───────────────────────
let toolbar = container(
row![
tb_button("New", Message::ScaleManagerNew, false),
tb_button("Copy", Message::ScaleManagerCopy, false),
tb_button("Delete", Message::ScaleManagerDelete, false),
tb_button(t!("New"), Message::ScaleManagerNew, false),
tb_button(t!("Copy"), Message::ScaleManagerCopy, false),
tb_button(t!("Delete"), Message::ScaleManagerDelete, false),
Space::new().width(sizing.width),
tb_button("Set Current", Message::ScaleManagerSetCurrent, false),
tb_button("Apply", Message::ScaleManagerApply, true),
tb_button(t!("Set Current"), Message::ScaleManagerSetCurrent, false),
tb_button(t!("Apply"), Message::ScaleManagerApply, true),
]
.spacing(4)
.align_y(iced::Center),
@ -119,7 +121,7 @@ pub fn view_window<'a, 'b>(
let list_panel = container(
column![
text("Scales").size(10).style(muted_text_style),
text(t!("Scales")).size(10).style(muted_text_style),
container(scrollable(column(rows).spacing(1)).height(sizing.height))
.style(|theme: &Theme| {
let palette = theme.palette();
@ -151,10 +153,10 @@ pub fn view_window<'a, 'b>(
// ── Right: editor (name + paper:drawing) ──────────────────────────────
let field =
|label: &'static str, ph: &'static str, value: &str, on: fn(String) -> Message| {
|label: Cow<'static, str>, ph: Cow<'static, str>, value: &str, on: fn(String) -> Message| {
row![
text(label).size(11).style(muted_text_style).width(96),
text_input(ph, value)
text_input(ph.as_ref(), value)
.on_input(on)
.style(field_style)
.size(12)
@ -167,11 +169,11 @@ pub fn view_window<'a, 'b>(
let editor = container(
column![
text("Scale").size(10).style(muted_text_style),
field("Paper units", "1", paper_buf, Message::ScaleManagerPaperBuf),
field("Drawing units", "50", drawing_buf, Message::ScaleManagerDrawingBuf),
text(Cow::Borrowed("Scale")).size(10).style(muted_text_style),
field(t!("Paper units"), Cow::Borrowed("1"), paper_buf, Message::ScaleManagerPaperBuf),
field(t!("Drawing units"), Cow::Borrowed("50"), drawing_buf, Message::ScaleManagerDrawingBuf),
Space::new().height(6),
text("Double-click a scale to rename it; edit its paper : drawing ratio here. New / Copy add a scale. Changes are kept only if you click Apply before closing.")
text(t!("Double-click a scale to rename it; edit its paper : drawing ratio here. New / Copy add a scale. Changes are kept only if you click Apply before closing."))
.size(10)
.style(muted_text_style),
]

View file

@ -11,6 +11,8 @@ use crate::app::{Message, StyleKind};
use iced::widget::button::{Status, Style};
use iced::widget::{button, column, container, row, scrollable, text, Space};
use iced::{Background, Border, Element, Theme};
use crate::t;
use std::borrow::Cow;
/// Everything the shared frame needs. The per-manager `editor` element is the
/// only bespoke part.
@ -51,12 +53,12 @@ pub fn view<'a, 'b>(s: Scaffold<'a, 'b>) -> Element<'a, Message> {
let height = s.sizing.height;
// ── Toolbar: New / Copy / Delete | … | Set Current / Apply ────────────
let bar = row![
tb_button("New", s.on_new, false),
tb_button("Copy", s.on_copy, false),
tb_button("Delete", s.on_delete, false),
tb_button(t!("New"), s.on_new, false),
tb_button(t!("Copy"), s.on_copy, false),
tb_button(t!("Delete"), s.on_delete, false),
Space::new().width(width),
tb_button("Set Current", s.on_set_current, false),
tb_button("Apply", s.on_apply, true),
tb_button(t!("Set Current"), s.on_set_current, false),
tb_button(t!("Apply"), s.on_apply, true),
]
.spacing(4)
.align_y(iced::Center);
@ -91,7 +93,7 @@ pub fn view<'a, 'b>(s: Scaffold<'a, 'b>) -> Element<'a, Message> {
let list_panel = container(
column![
text("Styles").size(10).style(muted_text_style),
text(t!("Styles")).size(10).style(muted_text_style),
container(scrollable(column(rows).spacing(1)).height(height))
.style(|theme: &Theme| {
let palette = theme.palette();
@ -137,9 +139,13 @@ pub fn view<'a, 'b>(s: Scaffold<'a, 'b>) -> Element<'a, Message> {
// ── Shared chrome ──────────────────────────────────────────────────────────
pub(crate) fn tb_button<'a>(label: &'a str, msg: Message, accent: bool) -> Element<'a, Message> {
pub(crate) fn tb_button(
label: impl Into<Cow<'static, str>>,
msg: Message,
accent: bool,
) -> Element<'static, Message> {
let pad = if accent { [4, 14] } else { [4, 10] };
button(text(label).size(11))
button(text(label.into()).size(11))
.on_press(msg)
.style(btn_s(accent))
.padding(pad)

View file

@ -5,6 +5,22 @@ use iced::widget::{
button, checkbox, column, container, row, scrollable, text, text_input, Column,
};
use iced::{Background, Border, Element, Theme};
use crate::t;
use std::borrow::Cow;
use std::fmt;
#[derive(Clone, Debug, Eq, PartialEq)]
struct EnumChoice(String);
impl fmt::Display for EnumChoice {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(crate::i18n::translate(&self.0).as_ref())
}
}
fn enum_choices(values: &[&str]) -> Vec<EnumChoice> {
values.iter().map(|value| EnumChoice((*value).to_string())).collect()
}
fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
move |theme: &Theme, st| {
@ -66,7 +82,7 @@ pub fn view_window<'a>(
sizing: crate::ui::modal::ModalSizing,
) -> Element<'a, Message> {
// ── Right: Details panel ──────────────────────────────────────────────
let info_row = |label: &'static str, val: String| -> Element<'_, Message> {
let info_row = |label: Cow<'static, str>, val: String| -> Element<'_, Message> {
row![
text(label).size(11).style(muted_style).width(160),
text(val).size(11),
@ -76,12 +92,12 @@ pub fn view_window<'a>(
.into()
};
let cell_editor = |row_label: &'static str,
let cell_editor = |row_label: Cow<'static, str>,
row: u8,
rs: &acadrust::objects::RowCellStyle|
-> Element<'a, Message> {
let r = row as usize;
let cell_in = |label: &'static str,
let cell_in = |label: Cow<'static, str>,
placeholder: &'static str,
value: &'a str,
field: &'static str|
@ -103,7 +119,7 @@ pub fn view_window<'a>(
};
// Shared colour selector — sends the chosen colour through the same
// cell edit as an ACI string.
let cell_color = |label: &'static str, value: &'a str, field: &'static str| -> Element<'a, Message> {
let cell_color = |label: Cow<'static, str>, value: &'a str, field: &'static str| -> Element<'a, Message> {
let cur = crate::ui::color_select::aci_string_to_color(value);
let open = color_open == Some((row, field));
let selector = crate::ui::color_select::color_selector(
@ -133,20 +149,20 @@ pub fn view_window<'a>(
.spacing(3)
.push(text(row_label).size(11).style(primary_style))
.push(cell_in(
" Text style:",
t!(" Text style:"),
"Standard",
&cell_textstyle[r],
"textstyle",
))
.push(cell_in(" Text height:", "0.18", &cell_height[r], "height"))
.push(cell_color(" Text color:", &cell_textcolor[r], "textcolor"))
.push(cell_color(" Fill color:", &cell_fillcolor[r], "fillcolor"))
.push(cell_in(t!(" Text height:"), "0.18", &cell_height[r], "height"))
.push(cell_color(t!(" Text color:"), &cell_textcolor[r], "textcolor"))
.push(cell_color(t!(" Fill color:"), &cell_fillcolor[r], "fillcolor"))
.push(
row![
text(" Alignment:").size(11).style(muted_style).width(150),
text(t!(" Alignment:")).size(11).style(muted_style).width(150),
iced::widget::pick_list(
Some(format!("{:?}", rs.alignment)),
[
Some(EnumChoice(format!("{:?}", rs.alignment))),
enum_choices(&[
"TopLeft",
"TopCenter",
"TopRight",
@ -156,13 +172,13 @@ pub fn view_window<'a>(
"BottomLeft",
"BottomCenter",
"BottomRight",
]
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>(),
]),
|value| value.to_string(),
)
.on_select(move |value| Message::TableStyleCellSetAlign { row, value })
.on_select(move |choice| Message::TableStyleCellSetAlign {
row,
value: choice.0,
})
.text_size(11)
.width(140),
]
@ -171,16 +187,16 @@ pub fn view_window<'a>(
)
.push(
checkbox(rs.fill_enabled)
.label(" Background fill enabled")
.label(t!(" Background fill enabled"))
.on_toggle(move |_| Message::TableStyleCellToggleFill(row))
.size(14)
.text_size(11),
)
.push(cell_in(" Data type:", "0", &cell_datatype[r], "datatype"))
.push(cell_in(" Unit type:", "0", &cell_unittype[r], "unittype"))
.push(cell_in(" Format string:", "", &cell_format[r], "format"))
.push(cell_in(t!(" Data type:"), "0", &cell_datatype[r], "datatype"))
.push(cell_in(t!(" Unit type:"), "0", &cell_unittype[r], "unittype"))
.push(cell_in(t!(" Format string:"), "", &cell_format[r], "format"))
.push(
text(" Borders (type / weight / color / spacing / hidden)")
text(t!(" Borders (type / weight / color / spacing / hidden)"))
.size(10)
.style(muted_style),
);
@ -199,21 +215,18 @@ pub fn view_window<'a>(
row![
text(format!(" {bname}")).size(11).style(muted_style).width(28),
iced::widget::pick_list(
Some(format!("{:?}", bd.border_type)),
["Single", "Double"]
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>(),
Some(EnumChoice(format!("{:?}", bd.border_type))),
enum_choices(&["Single", "Double"]),
|value| value.to_string(),
)
.on_select(move |value| Message::TableStyleBorderSetType {
.on_select(move |choice| Message::TableStyleBorderSetType {
cell: row,
border: bu,
value
value: choice.0,
})
.text_size(10)
.width(74),
text_input("wt", &border_lw[r][b])
text_input("", &border_lw[r][b])
.on_input(move |v| Message::TableStyleBorderEdit {
cell: row,
border: bu,
@ -222,7 +235,7 @@ pub fn view_window<'a>(
})
.size(10)
.width(46),
text_input("clr", &border_color[r][b])
text_input("", &border_color[r][b])
.on_input(move |v| Message::TableStyleBorderEdit {
cell: row,
border: bu,
@ -231,7 +244,7 @@ pub fn view_window<'a>(
})
.size(10)
.width(46),
text_input("gap", &border_spacing[r][b])
text_input("", &border_spacing[r][b])
.on_input(move |v| Message::TableStyleBorderEdit {
cell: row,
border: bu,
@ -253,7 +266,7 @@ pub fn view_window<'a>(
}
col.push(
button(text("Apply cell").size(11))
button(text(t!("Apply cell")).size(11))
.on_press(Message::TableStyleCellApply(row))
.style(btn_s(true))
.padding([4, 12]),
@ -264,9 +277,9 @@ pub fn view_window<'a>(
let details: Element<'_, Message> = if let Some(s) = selected_style {
scrollable(
column![
info_row("Name:", s.name.clone()),
info_row(t!("Name:"), s.name.clone()),
row![
text("Description:").size(11).style(muted_style).width(160),
text(t!("Description:")).size(11).style(muted_style).width(160),
text_input("", description_buf)
.on_input(|v| Message::TableStyleEdit {
field: "description",
@ -278,23 +291,20 @@ pub fn view_window<'a>(
.spacing(8)
.align_y(iced::Center),
row![
text("Flow direction:").size(11).style(muted_style).width(160),
text(t!("Flow direction:")).size(11).style(muted_style).width(160),
iced::widget::pick_list(
Some(format!("{:?}", s.flow_direction)),
["Down", "Up"]
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>(),
Some(EnumChoice(format!("{:?}", s.flow_direction))),
enum_choices(&["Down", "Up"]),
|value| value.to_string(),
)
.on_select(Message::TableStyleSetFlow)
.on_select(|choice| Message::TableStyleSetFlow(choice.0))
.text_size(11)
.width(100),
]
.spacing(8)
.align_y(iced::Center),
row![
text("H Margin:").size(11).style(muted_style).width(160),
text(t!("H Margin:")).size(11).style(muted_style).width(160),
text_input("1.5", hmargin_buf)
.on_input(|v| Message::TableStyleEdit {
field: "hmargin",
@ -306,7 +316,7 @@ pub fn view_window<'a>(
.spacing(8)
.align_y(iced::Center),
row![
text("V Margin:").size(11).style(muted_style).width(160),
text(t!("V Margin:")).size(11).style(muted_style).width(160),
text_input("1.5", vmargin_buf)
.on_input(|v| Message::TableStyleEdit {
field: "vmargin",
@ -318,22 +328,22 @@ pub fn view_window<'a>(
.spacing(8)
.align_y(iced::Center),
checkbox(s.title_suppressed)
.label("Title row suppressed")
.label(t!("Title row suppressed"))
.on_toggle(|_| Message::TableStyleToggle("title_sup"))
.size(14)
.text_size(11),
checkbox(s.header_suppressed)
.label("Header row suppressed")
.label(t!("Header row suppressed"))
.on_toggle(|_| Message::TableStyleToggle("header_sup"))
.size(14)
.text_size(11),
button(text("Apply margins").size(11))
button(text(t!("Apply margins")).size(11))
.on_press(Message::TableStyleApply)
.style(btn_s(true))
.padding([4, 12]),
cell_editor("Data Row:", 0, &s.data_row_style),
cell_editor("Header Row:", 1, &s.header_row_style),
cell_editor("Title Row:", 2, &s.title_row_style),
cell_editor(t!("Data Row:"), 0, &s.data_row_style),
cell_editor(t!("Header Row:"), 1, &s.header_row_style),
cell_editor(t!("Title Row:"), 2, &s.title_row_style),
]
.spacing(6)
.padding([12, 12]),
@ -342,7 +352,7 @@ pub fn view_window<'a>(
.height(sizing.height)
.into()
} else {
container(text("Select a style to view details.").size(11).style(muted_style))
container(text(t!("Select a style to view details.")).size(11).style(muted_style))
.padding([12, 12])
.into()
};

View file

@ -6,6 +6,8 @@ use iced::widget::{
button, canvas, checkbox, column, container, row, scrollable, text, text_input, Space,
};
use iced::{mouse, Background, Border, Element, Length, Point, Rectangle, Theme};
use crate::t;
use std::borrow::Cow;
/// View-model for the Text Style editor window.
pub struct TextStyleView<'a> {
@ -217,7 +219,7 @@ pub fn view_window<'a>(
let font_panel = container(
column![
text("Font File").size(10).style(muted_style),
text(t!("Font File")).size(10).style(muted_style),
container(scrollable(column(font_items).spacing(1)).height(sizing.height))
.style(|theme: &Theme| {
let palette = theme.palette();
@ -234,7 +236,7 @@ pub fn view_window<'a>(
.width(sizing.width)
.height(sizing.height)
.padding(2),
text_input("font file…", font_buf)
text_input(t!("font file…").as_ref(), font_buf)
.on_input(|v| Message::TextStyleEdit {
field: "font",
value: v
@ -252,14 +254,14 @@ pub fn view_window<'a>(
// Labeled numeric/text field row → TextStyleEdit { field, value }.
fn frow<'a>(
label: &'a str,
ph: &'a str,
label: impl Into<Cow<'static, str>>,
ph: impl Into<Cow<'static, str>>,
buf: &'a str,
field: &'static str,
) -> Element<'a, Message> {
row![
text(label).size(11).style(muted_style).width(120),
text_input(ph, buf)
text(label.into()).size(11).style(muted_style).width(120),
text_input(ph.into().as_ref(), buf)
.on_input(move |v| Message::TextStyleEdit { field, value: v })
.style(field_style)
.size(11)
@ -295,41 +297,41 @@ pub fn view_window<'a>(
// ── Right: Properties ─────────────────────────────────────────────────
let props_panel = container(
column![
text("Properties").size(11).style(primary_style),
frow("Big Font:", "big-font file…", bigfont_buf, "bigfont"),
frow("TrueType Font:", "e.g. Arial", ttf_buf, "ttf"),
text(t!("Properties")).size(11).style(primary_style),
frow(t!("Big Font:"), t!("big-font file…"), bigfont_buf, "bigfont"),
frow(t!("TrueType Font:"), t!("e.g. Arial"), ttf_buf, "ttf"),
frow(
if annotative {
"Paper Text Height:"
t!("Paper Text Height:")
} else {
"Fixed Height:"
t!("Fixed Height:")
},
"0 = variable",
t!("0 = variable"),
height_buf,
"height",
),
frow("Width Factor:", "1.0", width_buf, "width"),
frow("Oblique (°):", "0.0", oblique_buf, "oblique"),
frow(t!("Width Factor:"), "1.0", width_buf, "width"),
frow(t!("Oblique (°):"), "0.0", oblique_buf, "oblique"),
row![
checkbox(backward)
.label("Backward")
.label(t!("Backward"))
.on_toggle(|_| Message::TextStyleToggle("backward"))
.size(15)
.text_size(11),
checkbox(upside_down)
.label("Upside down")
.label(t!("Upside down"))
.on_toggle(|_| Message::TextStyleToggle("upside_down"))
.size(15)
.text_size(11),
]
.spacing(16),
checkbox(annotative)
.label("Annotative")
.label(t!("Annotative"))
.on_toggle(|_| Message::TextStyleToggle("annotative"))
.size(15)
.text_size(11),
Space::new().height(8),
text("Preview").size(10).style(muted_style),
text(t!("Preview")).size(10).style(muted_style),
container(preview)
.style(|theme: &Theme| {
let palette = theme.palette();
@ -377,7 +379,7 @@ pub fn view_window<'a>(
let ttf_panel = container(
column![
text("TrueType (system)").size(10).style(muted_style),
text(t!("TrueType (system)")).size(10).style(muted_style),
container(scrollable(column(ttf_items).spacing(1)).height(sizing.height))
.style(|theme: &Theme| {
let palette = theme.palette();
@ -394,7 +396,7 @@ pub fn view_window<'a>(
.width(sizing.width)
.height(sizing.height)
.padding(2),
text_input("TrueType font…", ttf_buf)
text_input(t!("TrueType font…").as_ref(), ttf_buf)
.on_input(|v| Message::TextStyleEdit {
field: "ttf",
value: v

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]);

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);

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)

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),

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]),

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)

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]),

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])

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)

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]),

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");

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]),

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()