Skip to main content

day_localization_ui/
lib.rs

1// Copyright © The Daybrite Project
2// SPDX-License-Identifier: MPL-2.0
3
4//! day-localization-ui: the piece-facing localization layer over the [`day_localization`] engine (DESIGN.md §12).
5//!
6//! The engine (bundles, the current-locale signal, `format_in`, and the built-in core catalog of
7//! standard UI strings) now lives in `day-localization`, low enough that the central crates localize their
8//! own strings. This crate re-exports that engine and adds the reactive `tr()` **text source**:
9//! `label(tr("greeting").arg("name", name))`, whose value re-reads the locale signal plus any signal
10//! args, so a locale switch updates every visible string fine-grained, followed by one relayout.
11
12use day_pieces::{IntoText, TextSource};
13
14// Re-export the engine so the app-facing API (`install_locales`, `set_locale`, …) is unchanged.
15pub use day_localization::{
16    Catalog, DateFields, FArg, IntoFArg, IntoNumberFArg, SigM, ValM, add_launch_locales, compare,
17    compare_in, format_catalog, format_date, format_date_in, format_decimal, format_decimal_in,
18    format_in, format_percent, format_percent_in, key, locale, matches_search, matches_search_in,
19    register_catalog, set_launch_locale, set_launch_locales, set_locale, sort_localized,
20    strip_isolates, t,
21};
22
23/// Register the app's locales (see [`day_localization::install`]) and fix the layout direction from the
24/// locale that actually resolved (docs/localization): an RTL locale (Arabic, Hebrew, …) mirrors
25/// every horizontal placement and flips the native toolkit's direction. Direction is resolved
26/// once, before the first layout; runtime `set_locale` switches strings but not direction.
27pub fn install(default: &str, locales: &[(&str, &str)]) {
28    day_localization::install(default, locales);
29    day_core::set_layout_direction(day_core::direction_of_locale(
30        &day_localization::locale().get_untracked(),
31    ));
32}
33
34/// A localized text source: `label(tr("greeting").arg("name", name))` (§12.2).
35#[derive(Clone)]
36pub struct LocalizedText {
37    key: String,
38    catalog: Option<&'static Catalog>,
39    args: Vec<(String, FArg)>,
40}
41
42pub fn tr(key: &str) -> LocalizedText {
43    LocalizedText {
44        key: key.to_owned(),
45        catalog: None,
46        args: Vec::new(),
47    }
48}
49
50/// Bind a key to a private crate/file catalog. Prefer generated typed accessors.
51pub fn tr_in(catalog: &'static Catalog, key: &str) -> LocalizedText {
52    register_catalog(catalog);
53    LocalizedText {
54        key: key.to_owned(),
55        catalog: Some(catalog),
56        args: Vec::new(),
57    }
58}
59
60/// Day's own strings, named the way an app's are: `res::str::day_cancel()`,
61/// `res::str::day_about_app(name)`, one typed function per message in the core catalog
62/// (`day-localization/catalog/en.ftl`), generated from it at build time (docs/localization.md).
63///
64/// An app reaches these through its own `res::str`, which re-exports them beside its messages, so
65/// `res::str::day_about_website()` works there too, and a message the app defines under the same
66/// name overrides the core one. Pieces with no `res` of their own call `day_localization_ui::res::str`.
67pub mod res {
68    /// The core catalog's messages as typed [`LocalizedText`](crate::LocalizedText) constructors.
69    pub mod str {
70        use crate::{LocalizedText, tr};
71        day_localization::__core_strings!();
72    }
73}
74
75/// Include the private localization modules written by `day_build::generate_locales()`.
76/// Usable by reusable pieces without depending on the `day` umbrella crate.
77#[macro_export]
78macro_rules! locales {
79    () => {
80        pub mod res {
81            include!(concat!(env!("OUT_DIR"), "/day_locales.rs"));
82        }
83    };
84}
85
86impl LocalizedText {
87    pub fn arg<M>(mut self, name: &str, value: impl IntoFArg<M>) -> Self {
88        self.args.push((name.to_owned(), value.into_farg()));
89        self
90    }
91
92    /// Tracked format: reads the locale signal + any signal args.
93    pub fn format(&self) -> String {
94        match self.catalog {
95            Some(catalog) => format_catalog(catalog, &locale().get(), &self.key, &self.args),
96            None => format_in(&locale().get(), &self.key, &self.args),
97        }
98    }
99}
100
101/// A formatted snapshot in the current locale, for the places that hold a `String`. Lets a
102/// closure returning a `LocalizedText` be an `IntoText` too (`label(move || res::str::n(count.get()))`),
103/// since the closure form converts through `Into<String>`.
104impl From<LocalizedText> for String {
105    fn from(t: LocalizedText) -> String {
106        t.format()
107    }
108}
109
110pub struct LocalizedMark;
111
112impl IntoText<LocalizedMark> for LocalizedText {
113    fn into_text(self) -> TextSource {
114        TextSource::Dyn(std::rc::Rc::new(move || self.format()))
115    }
116}