Skip to main content

i18n_string/
lib.rs

1#![doc = include_str!("../README.md")]
2#![cfg_attr(not(feature = "std"), no_std)]
3
4extern crate alloc;
5extern crate core;
6
7pub mod escape;
8mod format;
9mod parse;
10#[cfg(test)]
11mod tests;
12mod translate;
13
14use alloc::{
15    borrow::Cow,
16    boxed::Box,
17    format,
18    rc::Rc,
19    string::{String, ToString},
20    sync::Arc,
21};
22use core::{
23    fmt::{Debug, Display, Formatter},
24    str::FromStr,
25};
26
27use compact_str::CompactString;
28
29/// Error type for invalid I18nString format.
30#[derive(Debug)]
31pub struct InvalidFormat;
32
33impl Display for InvalidFormat {
34    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
35        write!(f, "invalid format")
36    }
37}
38
39#[cfg(feature = "std")]
40impl std::error::Error for InvalidFormat {}
41
42/// Trait for resolving translated I18nString templates.
43pub trait Resolver {
44    /// Resolve a template string.
45    fn resolve<'s>(&'s self, template: &'s str) -> Cow<'s, str>;
46}
47
48macro_rules! impl_resolver_delegate {
49    ($typ:ty) => {
50        impl<T: Resolver> Resolver for $typ {
51            fn resolve<'s>(&'s self, template: &'s str) -> Cow<'s, str> {
52                Resolver::resolve(&**self, template)
53            }
54        }
55    };
56}
57
58impl_resolver_delegate!(&T);
59impl_resolver_delegate!(&mut T);
60impl_resolver_delegate!(Box<T>);
61impl_resolver_delegate!(Arc<T>);
62impl_resolver_delegate!(Rc<T>);
63
64/// A resolver that does not resolve any templates.
65#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
66pub struct NoResolver;
67
68impl Resolver for NoResolver {
69    fn resolve<'s>(&'s self, template: &'s str) -> Cow<'s, str> {
70        template.into()
71    }
72}
73
74/// A string that can be translated into multiple languages.
75///
76/// # Examples
77///
78/// Basic example.
79/// ```
80/// use std::borrow::Cow;
81///
82/// use i18n_string::{I18nString, Resolver};
83///
84/// struct SimpleResolver;
85///
86/// impl Resolver for SimpleResolver {
87///     fn resolve<'s>(&'s self, template: &'s str) -> Cow<'s, str> {
88///         match template {
89///             "world" => Cow::Borrowed("<translated world>"),
90///             _ => template.into(),
91///         }
92///     }
93/// }
94///
95/// let s = I18nString::template("hello {0}, you are {1}", [I18nString::template("world", []), I18nString::literal("123")]);
96/// assert_eq!(s.translate(&SimpleResolver), "hello <translated world>, you are 123");
97/// ```
98#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
99#[non_exhaustive]
100pub enum I18nString {
101    /// A literal string.
102    Literal(CompactString),
103    /// A template string.
104    Template(CompactString, Box<[I18nString]>),
105}
106
107impl I18nString {
108    /// Create a new `I18nString::Literal` from a string.
109    ///
110    ///
111    /// # Examples
112    ///
113    /// Basic example.
114    /// ```
115    /// use i18n_string::I18nString;
116    ///
117    /// let s = I18nString::literal("hello");
118    /// assert_eq!(s, I18nString::Literal("hello".into()));
119    /// ```
120    pub fn literal<S: Into<CompactString>>(s: S) -> Self {
121        Self::Literal(s.into())
122    }
123
124    /// Create a new `I18nString::Template` from a string and arguments.
125    ///
126    /// # Examples
127    ///
128    /// Basic example.
129    /// ```
130    /// use i18n_string::I18nString;
131    ///
132    /// let s = I18nString::template("hello {}", [I18nString::literal("world")]);
133    /// assert_eq!(s, I18nString::Template("hello {}".into(), [I18nString::Literal("world".into())].into()));
134    /// ```
135    pub fn template<S: Into<CompactString>, ARGS: IntoIterator<Item = I18nString>>(s: S, args: ARGS) -> Self {
136        Self::Template(s.into(), args.into_iter().collect())
137    }
138}
139
140impl FromStr for I18nString {
141    type Err = InvalidFormat;
142
143    fn from_str(s: &str) -> Result<Self, Self::Err> {
144        parse::parse(s)
145    }
146}
147
148impl Display for I18nString {
149    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
150        format::format_to(f, self)
151    }
152}
153
154#[cfg(feature = "serde")]
155impl serde::Serialize for I18nString {
156    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
157    where
158        S: serde::Serializer,
159    {
160        serializer.serialize_str(&self.to_string())
161    }
162}
163
164#[cfg(feature = "serde")]
165impl<'de> serde::Deserialize<'de> for I18nString {
166    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
167    where
168        D: serde::Deserializer<'de>,
169    {
170        struct Visitor;
171
172        impl<'de> serde::de::Visitor<'de> for Visitor {
173            type Value = I18nString;
174
175            fn expecting(&self, formatter: &mut Formatter<'_>) -> core::fmt::Result {
176                formatter.write_str("a string")
177            }
178
179            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
180            where
181                E: serde::de::Error,
182            {
183                value.parse().map_err(serde::de::Error::custom)
184            }
185        }
186
187        deserializer.deserialize_str(Visitor)
188    }
189}
190
191/// Extension trait for `I18nString` to translate it into a no-translate string.
192pub trait I18nStringTranslateExt {
193    /// Translate the `I18nString` into a no-translate string.
194    ///
195    /// # Examples
196    ///
197    /// Basic example.
198    /// ```
199    /// use i18n_string::{I18nString, I18nStringTranslateExt};
200    ///
201    /// let s = I18nString::template("hello {0}", [I18nString::literal("world")]);
202    /// assert_eq!(s.to_no_translate_string(), "hello world");
203    /// ```
204    fn to_no_translate_string(&self) -> String;
205}
206
207impl I18nStringTranslateExt for I18nString {
208    fn to_no_translate_string(&self) -> String {
209        self.translate(NoResolver)
210    }
211}
212
213/// Extension trait for `I18nString` to build it from other types.
214pub trait I18nStringBuilderExt {
215    /// Create a new `I18nString::Literal` from a `Display` type.
216    fn display<D: Display + ?Sized>(display: &D) -> Self;
217
218    /// Create a new `I18nString::Literal` from a `Debug` type.
219    fn debug<D: Debug + ?Sized>(debug: &D) -> Self;
220
221    /// Create a new `I18nString::Template` from a `Display` type.
222    fn template_display<D: Display + ?Sized>(display: &D) -> Self;
223
224    /// Create a new `I18nString::Template` from a `Debug` type.
225    fn template_debug<D: Debug + ?Sized>(debug: &D) -> Self;
226}
227
228impl I18nStringBuilderExt for I18nString {
229    fn display<D: Display + ?Sized>(display: &D) -> Self {
230        Self::literal(display.to_string())
231    }
232
233    fn debug<D: Debug + ?Sized>(debug: &D) -> Self {
234        Self::literal(format!("{:?}", debug))
235    }
236
237    fn template_display<D: Display + ?Sized>(display: &D) -> Self {
238        Self::template(display.to_string(), [])
239    }
240
241    fn template_debug<D: Debug + ?Sized>(debug: &D) -> Self {
242        Self::template(format!("{:?}", debug), [])
243    }
244}