Skip to main content

wlr_capture/
i18n.rs

1//! Internationalisation.
2//!
3//! With the `i18n` feature (on by default) UI strings are localised via Fluent
4//! (`i18n-embed`): catalogs live in `i18n/<lang>/wlr_capture.ftl`, are embedded into
5//! the binary, and the UI language is negotiated from the desktop locale, falling
6//! back to English. Without the feature, [`tr!`](crate::tr) returns the English text
7//! generated from the `en` catalog at build time (see `build.rs`) — no Fluent
8//! dependency is pulled at all, which keeps minimal/headless builds lean.
9//!
10//! Use the [`tr!`](crate::tr) macro for message lookups; it works unchanged from any
11//! crate in the workspace and across both build configurations.
12
13#[cfg(feature = "i18n")]
14mod fluent_impl {
15    use i18n_embed::fluent::{FluentLanguageLoader, fluent_language_loader};
16    use i18n_embed::{DesktopLanguageRequester, LanguageLoader};
17    use rust_embed::RustEmbed;
18    use std::sync::LazyLock;
19
20    #[derive(RustEmbed)]
21    #[folder = "i18n/"]
22    struct Localizations;
23
24    /// The process-wide Fluent loader, preloaded with the fallback language.
25    pub static LOADER: LazyLock<FluentLanguageLoader> = LazyLock::new(|| {
26        let loader = fluent_language_loader!();
27        loader
28            .load_fallback_language(&Localizations)
29            .expect("fallback language must be present");
30        // No bidirectional isolation marks around placeables (we render plain LTR text).
31        loader.set_use_isolating(false);
32        loader
33    });
34
35    /// Initialise localisation (call once at startup).
36    ///
37    /// The UI follows the desktop locale (`LANGUAGE`/`LC_ALL`/`LC_MESSAGES`/`LANG`),
38    /// falling back to English; set `LANGUAGE` (e.g. `LANGUAGE=ja`) to override.
39    pub fn init() {
40        let requested = DesktopLanguageRequester::requested_languages();
41        let _ = i18n_embed::select(&*LOADER, &Localizations, &requested);
42    }
43
44    #[cfg(test)]
45    mod tests {
46        use super::*;
47
48        /// Every embedded catalog must parse and load (catches malformed `.ftl`).
49        #[test]
50        fn every_catalog_loads() {
51            let loader = fluent_language_loader!();
52            let langs = loader
53                .available_languages(&Localizations)
54                .expect("list languages");
55            assert!(
56                langs.len() >= 13,
57                "expected ≥13 languages, got {}",
58                langs.len()
59            );
60            for lang in langs {
61                loader
62                    .load_languages(&Localizations, std::slice::from_ref(&lang))
63                    .unwrap_or_else(|e| panic!("catalog {lang} failed to load: {e}"));
64            }
65        }
66    }
67}
68
69#[cfg(feature = "i18n")]
70pub use fluent_impl::{LOADER, init};
71
72#[cfg(not(feature = "i18n"))]
73mod fallback_impl {
74    // `fallback(id, args) -> String`, generated from the `en` catalog by `build.rs`.
75    include!(concat!(env!("OUT_DIR"), "/i18n_fallback.rs"));
76
77    /// No-op: there is no locale to negotiate without Fluent.
78    pub fn init() {}
79}
80
81#[cfg(not(feature = "i18n"))]
82pub use fallback_impl::{fallback, init};
83
84/// Look up a UI message, optionally with `name = value` arguments.
85///
86/// With the `i18n` feature this is a runtime Fluent lookup against [`LOADER`]; without
87/// it, the English text from the `en` catalog (generated at build time). Either way it
88/// returns a `String` and is fully qualified through `$crate`, so it works from any
89/// crate in the workspace without extra dependencies. Argument values only need to be
90/// `Into<FluentValue>` (with `i18n`) / `Display` (without) — `String`, `&str`, integers.
91#[cfg(feature = "i18n")]
92#[macro_export]
93macro_rules! tr {
94    ($id:literal) => {
95        $crate::i18n::LOADER.get($id)
96    };
97    ($id:literal, $($name:ident = $value:expr),+ $(,)?) => {{
98        // Values keep their own type; `get_args` accepts any `V: Into<FluentValue>`
99        // (e.g. `String`, `&str`, integers). One map, so all args share a type.
100        let mut args = ::std::collections::HashMap::new();
101        $( args.insert(::std::stringify!($name), $value); )+
102        $crate::i18n::LOADER.get_args($id, args)
103    }};
104}
105
106/// English-only fallback variant (no `i18n` feature): expands to the generated
107/// `fallback`, formatting each argument with `Display`.
108#[cfg(not(feature = "i18n"))]
109#[macro_export]
110macro_rules! tr {
111    ($id:literal) => {
112        $crate::i18n::fallback($id, &[])
113    };
114    ($id:literal, $($name:ident = $value:expr),+ $(,)?) => {
115        $crate::i18n::fallback(
116            $id,
117            &[ $( (::std::stringify!($name), ::std::format!("{}", $value)) ),+ ],
118        )
119    };
120}