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}