1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
//! Easy-to-use i18n library for Rust, based on code generation.
//!
//! ## Usage
//! Please read the [documentation] to learn how to use this library.
//!
//! ```ignore
//! mod translations {
//!     rosetta_i18n::include_translations!();
//! }
//!
//! fn main() {
//!     assert_eq!(Lang::En.hello(), "Hello world!");
//! }
//! ```
//!
//! ## Serde support
//! This crate provide serialization and deserialization of languages types with Serde.
//! The `serde` feature must be enabled.
//!
//! [documentation]: https://baptiste0928.github.io/rosetta/
#![cfg_attr(docsrs, feature(doc_cfg))]

use std::borrow::Cow;

#[doc(hidden)]
pub mod provider;
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
pub mod serde_helpers;

/// Include the generated translations.
///
/// The generated code will be included in the file as if it were a direct element of it.
/// It is recommended to wrap the generated code in its own module:
///
/// ```ignore
/// mod translations {
///     rosetta_18n::include_translations!();
/// }
/// ```
///
/// This only works if the `rosetta-build` output file has been unmodified.
/// Otherwise, use the following pattern to include the file:
///
/// ```ignore
/// include!("/relative/path/to/rosetta_output.rs");
/// ```
#[macro_export]
macro_rules! include_translations {
    () => {
        include!(concat!(env!("OUT_DIR"), "/rosetta_output.rs"));
    };
}

/// Trait implemented by languages structs generated by `rosetta-build`.
pub trait Language: Sized {
    /// Initialize this type from a [`LanguageId`].
    ///
    /// The method returns [`None`] if the provided language id is not supported
    /// by the struct.
    fn from_language_id(language_id: &LanguageId) -> Option<Self>;
    /// Convert this struct to a [`LanguageId`].
    fn language_id(&self) -> LanguageId;
    /// Get the fallback language of this type.
    ///
    /// This fallback value can be used like a default value.
    fn fallback() -> Self;
}

/// Generic language type that implement the [`Language`] trait.
///
/// This type can be used as a default generic type when sharing models between multiple
/// crates that does not necessarily use translations.
///
/// ## Panics
/// The [`fallback`] method of the [`Language`] trait is not implemented and will panic if called.
///
/// [`fallback`]: Language::fallback
pub struct GenericLanguage(String);

impl Language for GenericLanguage {
    fn from_language_id(language_id: &LanguageId) -> Option<Self> {
        Some(Self(language_id.value().into()))
    }

    fn language_id(&self) -> LanguageId {
        LanguageId::new(&self.0)
    }

    fn fallback() -> Self {
        unimplemented!("GenericLanguage has no fallback language")
    }
}

/// ISO 639-1 language identifier.
///
/// This type holds a string representing a language in the [ISO 693-1] format (two-letter code).
/// The inner value is stored in a [`Cow`] to avoid allocation when possible.
///
/// ## Validation
/// The type inner value is not validated unless the [`validate`] method is used to initialize the instance.
/// Generally, you should use this method to initialize this type.
///
/// The performed validation only checks that the provided *looks like* an [ISO 693-1]language identifier
/// (2 character alphanumeric ascii string).
///
/// ## Serde support
/// This type implements the `Serialize` and `Deserialize` traits if the `serde` feature is enabled.
/// Deserialization will fail if the value is not an ISO 639-1 language identifier.
///
/// ## Example
/// ```
/// use rosetta_i18n::LanguageId;
///
/// let language_id = LanguageId::new("fr");
/// assert_eq!(language_id.value(), "fr");
///
/// let language_id = LanguageId::validate("fr");
/// assert!(language_id.is_some());
/// ```
///
/// [ISO 693-1]: https://en.wikipedia.org/wiki/ISO_639-1
/// [`validate`]: LanguageId::validate
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct LanguageId<'a>(Cow<'a, str>);

impl<'a> LanguageId<'a> {
    /// Initialize a new valid [`LanguageId`].
    ///
    /// Unlike [`new`], this method ensures that the provided
    /// value is a valid [ISO 693-1] encoded language id.
    ///
    /// ```
    /// # use rosetta_i18n::LanguageId;
    /// assert!(LanguageId::validate("fr").is_some());
    /// assert!(LanguageId::validate("invalid").is_none());
    /// ```
    ///
    /// [`new`]: LanguageId::new
    /// [ISO 693-1]: https://en.wikipedia.org/wiki/ISO_639-1
    pub fn validate(value: &str) -> Option<Self> {
        let valid_length = value.len() == 2;
        let ascii_alphabetic = value.chars().all(|c| c.is_ascii_alphabetic());

        if valid_length && ascii_alphabetic {
            Some(Self(Cow::Owned(value.to_ascii_lowercase())))
        } else {
            None
        }
    }

    /// Initialize a new [`LanguageId`] from a string.
    ///
    /// The provided value must be an [ISO 693-1] encoded language id.
    /// If you want to validate the value, use [`validate`] instead.
    ///
    /// ```
    /// # use rosetta_i18n::LanguageId;
    /// let language_id = LanguageId::new("en");
    /// assert_eq!(language_id.value(), "en");
    /// ```
    ///
    /// [ISO 693-1]: https://en.wikipedia.org/wiki/ISO_639-1
    /// [`validate`]: LanguageId::validate
    pub fn new(value: impl Into<Cow<'a, str>>) -> Self {
        Self(value.into())
    }

    /// Return a reference of the inner value.
    pub fn value(&self) -> &str {
        &self.0
    }

    /// Convert the type into a [`String`].
    pub fn into_inner(self) -> String {
        self.0.into_owned()
    }
}