Skip to main content

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}