Skip to main content

patois/
lib.rs

1//! Gettext translations for Rust desktop apps.
2//!
3//! An app embeds its compiled catalogs in the binary and calls [`init_auto`] at startup. Code in
4//! the app, and in any library the app depends on, then calls [`t`] and [`nt`] to get translated
5//! strings.
6//!
7//! ```rust,no_run
8//! use patois::{nt, t};
9//!
10//! patois::embed_domain!();
11//!
12//! fn main() {
13//!     patois::init_auto(env!("CARGO_PKG_NAME"));
14//!     println!("{}", t("Checking for updates..."));
15//!     println!("{}", nt("%d file", "%d files", 3).replacen("%d", "3", 1));
16//! }
17//! ```
18//!
19//! A library calls [`t`] and [`nt`] and needs no setup of its own: the strings resolve against the
20//! catalog of the app that uses the library.
21
22mod catalog;
23mod lang;
24mod plural;
25mod registry;
26#[cfg(feature = "wx")]
27pub mod wx;
28
29use std::path::Path;
30
31#[doc(hidden)]
32pub use inventory;
33pub use lang::{LanguageInfo, LanguageManager, best_available_language, language_name};
34pub use patois_macros::embed_domain;
35#[cfg(feature = "wx")]
36pub use patois_macros::embed_wx_translations;
37
38/// The catalogs that [`embed_domain!`] registers for one crate, generated as Rust statics so that
39/// nothing is parsed at runtime and each msgid is stored once for every locale.
40#[doc(hidden)]
41pub struct EmbeddedDomain {
42	pub name: &'static str,
43	/// The msgid of every entry, sorted by bytes. A plural entry is listed by its singular.
44	pub msgids: &'static [&'static str],
45	pub locales: &'static [EmbeddedLocale],
46}
47
48/// One locale of an [`EmbeddedDomain`].
49#[doc(hidden)]
50pub struct EmbeddedLocale {
51	/// The name of the catalog's directory under `locale/`, such as `pt_br`.
52	pub code: &'static str,
53	/// The catalog's header, which gives its plural rule.
54	pub header: &'static str,
55	/// Every translation, one after another, with the forms of a plural one separated by `\0`.
56	pub text: &'static str,
57	/// Where the translation of each msgid starts in `text`; it ends where the next one starts, so
58	/// there is one more entry than there are msgids. An empty one has no translation.
59	pub starts: &'static [u32],
60}
61
62inventory::collect!(EmbeddedDomain);
63
64/// Sets up translations for an app that loads its catalogs from disk.
65///
66/// Registers the locale directory of `domain`, makes `domain` the active domain, and activates
67/// `locale_id`. The catalogs must be at `<locale_dir>/<locale>/LC_MESSAGES/<domain>.mo`.
68///
69/// [`LanguageManager`] and [`available_locales`] list embedded catalogs only, so they don't see
70/// catalogs loaded this way.
71///
72/// # Panics
73///
74/// Panics if a thread panicked while it held the lock on the global translation state.
75pub fn init(domain: &str, locale_dir: impl AsRef<Path>, locale_id: &str) {
76	let mut reg = registry::global().lock().unwrap();
77	reg.register_path(domain, locale_dir.as_ref().to_path_buf());
78	reg.set_default_domain(domain);
79	reg.set_locale(locale_id);
80}
81
82/// Sets up translations for an app that embeds its catalogs with [`embed_domain!`].
83///
84/// Makes `domain` the active domain and activates the catalog that best fits the system language,
85/// as [`best_available_language`] chooses it. If no catalog fits, strings stay in English.
86///
87/// # Panics
88///
89/// Panics if a thread panicked while it held the lock on the global translation state.
90pub fn init_auto(domain: &str) {
91	let languages = LanguageManager::new(domain).available();
92	let locale = best_available_language(&system_locale(), &languages).unwrap_or_else(|| "en".to_string());
93	let mut reg = registry::global().lock().unwrap();
94	reg.set_default_domain(domain);
95	reg.set_locale(&locale);
96}
97
98/// Returns the translation of `s` in the active locale.
99///
100/// Returns `s` unchanged if no domain is active or the catalog has no translation.
101///
102/// # Panics
103///
104/// Panics if a thread panicked while it held the lock on the global translation state.
105#[must_use]
106pub fn t(s: &str) -> String {
107	registry::global().lock().unwrap().translate(s)
108}
109
110/// Returns the form of a plural string that fits `n` in the active locale.
111///
112/// The rule comes from the `Plural-Forms` header of the catalog. If no domain is active or the
113/// catalog has no translation, returns `singular` when `n` is 1 and `plural` otherwise.
114///
115/// The result still contains its placeholder, so replace that with the number yourself.
116///
117/// # Panics
118///
119/// Panics if a thread panicked while it held the lock on the global translation state.
120#[must_use]
121pub fn nt(singular: &str, plural: &str, n: u64) -> String {
122	registry::global().lock().unwrap().translate_plural(singular, plural, n)
123}
124
125/// Changes the active locale.
126///
127/// Later calls to [`t`] and [`nt`] use the new locale. Case and the separator don't matter, so
128/// `pt-BR`, `pt_br`, and `pt_BR` all select a catalog named `pt_br`. If there is no catalog for the
129/// region, the catalog for the bare language is used.
130///
131/// # Panics
132///
133/// Panics if a thread panicked while it held the lock on the global translation state.
134pub fn set_locale(locale_id: &str) {
135	registry::global().lock().unwrap().set_locale(locale_id);
136}
137
138/// Returns the active locale in normalized form, such as `pt_BR`.
139///
140/// Returns an empty string before a locale is set.
141///
142/// # Panics
143///
144/// Panics if a thread panicked while it held the lock on the global translation state.
145#[must_use]
146pub fn locale() -> String {
147	registry::global().lock().unwrap().locale().to_string()
148}
149
150/// Makes `domain` the active domain without registering a locale directory.
151///
152/// Use this function when [`embed_domain!`] embeds the catalogs of `domain` and you choose the
153/// locale yourself.
154///
155/// # Panics
156///
157/// Panics if a thread panicked while it held the lock on the global translation state.
158pub fn set_default_domain(domain: &str) {
159	registry::global().lock().unwrap().set_default_domain(domain);
160}
161
162/// Returns the locale codes of the catalogs embedded for `domain`.
163///
164/// The codes are the names of the directories under `locale/` at compile time, such as `de` and
165/// `pt_br`.
166#[must_use]
167pub fn available_locales(domain: &str) -> Vec<&'static str> {
168	inventory::iter::<EmbeddedDomain>
169		.into_iter()
170		.filter(|ed| ed.name == domain)
171		.flat_map(|ed| ed.locales.iter().map(|locale| locale.code))
172		.collect()
173}
174
175/// Returns the preferred locale of the operating system in normalized form, such as `en_US`.
176///
177/// Returns `en` if the system locale can't be determined.
178#[must_use]
179pub fn system_locale() -> String {
180	sys_locale::get_locale().map_or_else(|| "en".to_string(), |l| registry::normalize_locale(&l))
181}