dioxus_dnd/core/strings.rs
1//! Localizable strings - every phrase the crate voices, in one place.
2//!
3//! Screen-reader announcements ("Picked up Piranesi. …"), reorder-button
4//! labels and the selection badge all read a [`DndStrings`] from context,
5//! falling back to built-in English when none is provided. Provide one
6//! anywhere above your drag UI to localize everything below it:
7//!
8//! ```text
9//! use_context_provider(|| DndStrings {
10//! dropped_in: Rc::new(|name| t!("dropped-in", name: name)),
11//! ..Default::default()
12//! });
13//! ```
14//!
15//! The fields are plain `Rc<dyn Fn(..) -> String>`, so the crate stays
16//! dependency-free while any i18n system plugs in - the closures above call
17//! **dioxus-i18n**'s `t!` (the Fluent-based crate the Dioxus docs
18//! recommend), but a `match` on your own locale signal works just as well.
19//! Have the closures *read* your locale state rather than re-providing the
20//! struct on switch: components capture `DndStrings` once at mount, and a
21//! closure that reads a signal re-renders its readers when the locale
22//! changes.
23
24use std::rc::Rc;
25
26use dioxus::prelude::*;
27
28/// A phrase taking two names (the zone, then its parent).
29pub type TwoNamePhrase = Rc<dyn Fn(&str, &str) -> String>;
30
31/// The crate's voice, one field per phrase. Every field is a function so
32/// translations can reorder, inflect or pluralize freely; build it with
33/// struct-update syntax over [`Default::default`] to override only what
34/// you translate.
35#[derive(Clone)]
36pub struct DndStrings {
37 /// Voiced when a keyboard drag picks an item up. Receives the
38 /// draggable's `label`. This is also the user's manual - keep the
39 /// key instructions (arrows, Enter, Escape) in the translation.
40 pub picked_up: Rc<dyn Fn(&str) -> String>,
41 /// Voiced when keyboard navigation reaches a zone. Receives the zone's
42 /// name.
43 pub over: Rc<dyn Fn(&str) -> String>,
44 /// Voiced when keyboard navigation reaches a zone nested in a labeled
45 /// parent. Receives the zone's name, then the parent's.
46 pub over_inside: TwoNamePhrase,
47 /// Voiced when an arrow key finds nowhere to go.
48 pub no_targets: Rc<dyn Fn() -> String>,
49 /// Voiced when Enter is pressed with no zone selected.
50 pub no_target_selected: Rc<dyn Fn() -> String>,
51 /// Voiced when a keyboard drop lands. Receives the zone's name.
52 pub dropped_in: Rc<dyn Fn(&str) -> String>,
53 /// Voiced when Escape cancels the drag.
54 pub cancelled: Rc<dyn Fn() -> String>,
55 /// Fallback name for a draggable with no `label`.
56 pub item: Rc<dyn Fn() -> String>,
57 /// Fallback name for a zone with no `label`. Receives the zone id's
58 /// number.
59 pub zone: Rc<dyn Fn(u64) -> String>,
60 /// `ReorderButtons`: the up button's `aria-label`. Receives the row's
61 /// name.
62 pub move_up: Rc<dyn Fn(&str) -> String>,
63 /// `ReorderButtons`: the down button's `aria-label`. Receives the
64 /// row's name.
65 pub move_down: Rc<dyn Fn(&str) -> String>,
66 /// `ReorderButtons`: fallback name for a row with no `label`. Receives
67 /// the 1-based row number.
68 pub row: Rc<dyn Fn(usize) -> String>,
69 /// `SelectionCount`: the badge text. Receives how many items are in
70 /// flight - your chance at real plural rules.
71 pub selection_count: Rc<dyn Fn(usize) -> String>,
72}
73
74impl Default for DndStrings {
75 /// The built-in English.
76 fn default() -> Self {
77 Self {
78 picked_up: Rc::new(|name| {
79 format!(
80 "Picked up {name}. Use arrow keys to choose a drop target, \
81 Enter to drop, Escape to cancel."
82 )
83 }),
84 over: Rc::new(|name| format!("Over {name}.")),
85 over_inside: Rc::new(|name, parent| format!("Over {name}, inside {parent}.")),
86 no_targets: Rc::new(|| "No drop targets available.".to_string()),
87 no_target_selected: Rc::new(|| "No drop target selected.".to_string()),
88 dropped_in: Rc::new(|name| format!("Dropped in {name}.")),
89 cancelled: Rc::new(|| "Drag cancelled.".to_string()),
90 item: Rc::new(|| "item".to_string()),
91 zone: Rc::new(|n| format!("zone {n}")),
92 move_up: Rc::new(|name| format!("Move {name} up")),
93 move_down: Rc::new(|name| format!("Move {name} down")),
94 row: Rc::new(|n| format!("item {n}")),
95 selection_count: Rc::new(|n| format!("{n} item(s)")),
96 }
97 }
98}
99
100impl std::fmt::Debug for DndStrings {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 f.write_str("DndStrings")
103 }
104}
105
106/// The subtree's [`DndStrings`], or the English defaults when no ancestor
107/// provided one. Captured once per component instance - localize by having
108/// the provided closures read your locale state, not by re-providing the
109/// struct. Public so custom components voice themselves consistently.
110pub fn use_dnd_strings() -> DndStrings {
111 use_hook(|| try_consume_context::<DndStrings>().unwrap_or_default())
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 /// The built-in English, pinned - these are user-facing contracts apps
119 /// may key translations off.
120 #[test]
121 fn english_defaults() {
122 let s = DndStrings::default();
123 assert_eq!(
124 (s.picked_up)("Piranesi"),
125 "Picked up Piranesi. Use arrow keys to choose a drop target, \
126 Enter to drop, Escape to cancel."
127 );
128 assert_eq!((s.over)("Done"), "Over Done.");
129 assert_eq!((s.over_inside)("Done", "Board"), "Over Done, inside Board.");
130 assert_eq!((s.no_targets)(), "No drop targets available.");
131 assert_eq!((s.no_target_selected)(), "No drop target selected.");
132 assert_eq!((s.dropped_in)("Done"), "Dropped in Done.");
133 assert_eq!((s.cancelled)(), "Drag cancelled.");
134 assert_eq!((s.item)(), "item");
135 assert_eq!((s.zone)(7), "zone 7");
136 assert_eq!((s.move_up)("Draft"), "Move Draft up");
137 assert_eq!((s.move_down)("Draft"), "Move Draft down");
138 assert_eq!((s.row)(3), "item 3");
139 assert_eq!((s.selection_count)(3), "3 item(s)");
140 }
141}