rosetta_i18n/
lib.rs

1//! Easy-to-use i18n library for Rust, based on code generation.
2//!
3//! ## Usage
4//! Please read the [documentation] to learn how to use this library.
5//!
6//! ```ignore
7//! mod translations {
8//!     rosetta_i18n::include_translations!();
9//! }
10//!
11//! fn main() {
12//!     assert_eq!(Lang::En.hello(), "Hello world!");
13//! }
14//! ```
15//!
16//! ## Serde support
17//! This crate provide serialization and deserialization of languages types with Serde.
18//! The `serde` feature must be enabled.
19//!
20//! [documentation]: https://baptiste0928.github.io/rosetta/
21#![cfg_attr(docsrs, feature(doc_cfg))]
22
23use std::borrow::Cow;
24
25#[doc(hidden)]
26pub mod provider;
27#[cfg(feature = "serde")]
28#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
29pub mod serde_helpers;
30
31/// Include the generated translations.
32///
33/// The generated code will be included in the file as if it were a direct element of it.
34/// It is recommended to wrap the generated code in its own module:
35///
36/// ```ignore
37/// mod translations {
38///     rosetta_18n::include_translations!();
39/// }
40/// ```
41///
42/// This only works if the `rosetta-build` output file has been unmodified.
43/// Otherwise, use the following pattern to include the file:
44///
45/// ```ignore
46/// include!("/relative/path/to/rosetta_output.rs");
47/// ```
48#[macro_export]
49macro_rules! include_translations {
50    () => {
51        include!(concat!(env!("OUT_DIR"), "/rosetta_output.rs"));
52    };
53}
54
55/// Trait implemented by languages structs generated by `rosetta-build`.
56pub trait Language: Sized {
57    /// Initialize this type from a [`LanguageId`].
58    ///
59    /// The method returns [`None`] if the provided language id is not supported
60    /// by the struct.
61    fn from_language_id(language_id: &LanguageId) -> Option<Self>;
62    /// Convert this struct to a [`LanguageId`].
63    fn language_id(&self) -> LanguageId;
64    /// Get the fallback language of this type.
65    ///
66    /// This fallback value can be used like a default value.
67    fn fallback() -> Self;
68}
69
70/// Generic language type that implement the [`Language`] trait.
71///
72/// This type can be used as a default generic type when sharing models between multiple
73/// crates that does not necessarily use translations.
74///
75/// ## Panics
76/// The [`fallback`] method of the [`Language`] trait is not implemented and will panic if called.
77///
78/// [`fallback`]: Language::fallback
79pub struct GenericLanguage(String);
80
81impl Language for GenericLanguage {
82    fn from_language_id(language_id: &LanguageId) -> Option<Self> {
83        Some(Self(language_id.value().into()))
84    }
85
86    fn language_id(&self) -> LanguageId {
87        LanguageId::new(&self.0)
88    }
89
90    fn fallback() -> Self {
91        unimplemented!("GenericLanguage has no fallback language")
92    }
93}
94
95/// ISO 639-1 language identifier.
96///
97/// This type holds a string representing a language in the [ISO 693-1] format (two-letter code).
98/// The inner value is stored in a [`Cow`] to avoid allocation when possible.
99///
100/// ## Validation
101/// The type inner value is not validated unless the [`validate`] method is used to initialize the instance.
102/// Generally, you should use this method to initialize this type.
103///
104/// The performed validation only checks that the provided *looks like* an [ISO 693-1]language identifier
105/// (2 character alphanumeric ascii string).
106///
107/// ## Serde support
108/// This type implements the `Serialize` and `Deserialize` traits if the `serde` feature is enabled.
109/// Deserialization will fail if the value is not an ISO 639-1 language identifier.
110///
111/// ## Example
112/// ```
113/// use rosetta_i18n::LanguageId;
114///
115/// let language_id = LanguageId::new("fr");
116/// assert_eq!(language_id.value(), "fr");
117///
118/// let language_id = LanguageId::validate("fr");
119/// assert!(language_id.is_some());
120/// ```
121///
122/// [ISO 693-1]: https://en.wikipedia.org/wiki/ISO_639-1
123/// [`validate`]: LanguageId::validate
124#[derive(Debug, Clone, PartialEq, Eq, Hash)]
125pub struct LanguageId<'a>(Cow<'a, str>);
126
127impl<'a> LanguageId<'a> {
128    /// Initialize a new valid [`LanguageId`].
129    ///
130    /// Unlike [`new`], this method ensures that the provided
131    /// value is a valid [ISO 693-1] encoded language id.
132    ///
133    /// ```
134    /// # use rosetta_i18n::LanguageId;
135    /// assert!(LanguageId::validate("fr").is_some());
136    /// assert!(LanguageId::validate("invalid").is_none());
137    /// ```
138    ///
139    /// [`new`]: LanguageId::new
140    /// [ISO 693-1]: https://en.wikipedia.org/wiki/ISO_639-1
141    pub fn validate(value: &str) -> Option<Self> {
142        let valid_length = value.len() == 2;
143        let ascii_alphabetic = value.chars().all(|c| c.is_ascii_alphabetic());
144
145        if valid_length && ascii_alphabetic {
146            Some(Self(Cow::Owned(value.to_ascii_lowercase())))
147        } else {
148            None
149        }
150    }
151
152    /// Initialize a new [`LanguageId`] from a string.
153    ///
154    /// The provided value must be an [ISO 693-1] encoded language id.
155    /// If you want to validate the value, use [`validate`] instead.
156    ///
157    /// ```
158    /// # use rosetta_i18n::LanguageId;
159    /// let language_id = LanguageId::new("en");
160    /// assert_eq!(language_id.value(), "en");
161    /// ```
162    ///
163    /// [ISO 693-1]: https://en.wikipedia.org/wiki/ISO_639-1
164    /// [`validate`]: LanguageId::validate
165    pub fn new(value: impl Into<Cow<'a, str>>) -> Self {
166        Self(value.into())
167    }
168
169    /// Return a reference of the inner value.
170    pub fn value(&self) -> &str {
171        &self.0
172    }
173
174    /// Convert the type into a [`String`].
175    pub fn into_inner(self) -> String {
176        self.0.into_owned()
177    }
178}