typed_i18n/
lib.rs

1#![no_std]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![forbid(unsafe_code)]
4#![forbid(missing_docs)]
5#![warn(clippy::pedantic)]
6
7//! Convert a language file and an enum into a type safe i18n system.
8//!
9//! # Basic Usage
10//!
11//! Yaml language file: (json and lrc is also supported, more below)
12//! ```yaml
13//! hello_world:
14//!   en: Hello World
15//!   de: Hallo Welt
16//! hello_you:
17//!   en: Hello %{name}
18//! ```
19//!
20//! Code:
21//! ```rust
22//! # use typed_i18n::TypedI18N;
23//! #[derive(Copy, Clone, TypedI18N)]
24//! #[typed_i18n(filename = "example.yaml")]
25//! #[typed_i18n(builder = "mixed_str", prefix = "str_")]
26//! enum Language { En, De }
27//! ```
28//!
29//! Generated code:
30//! ```rust
31//! # enum Language { En, De }
32//! impl Language {
33//! # } trait LanguageTest {
34//!     fn str_hello_world(self) -> &'static str;
35//!     fn str_hello_you(self, name: &str) -> String;
36//! }
37//! ```
38//!
39//! Usage:
40//! ```rust
41//! # enum Language { En, De }
42//! # impl Language {
43//! #    fn str_hello_world(self) -> &'static str {""}
44//! #    fn str_hello_you(self, name: &str) -> String {String::new()}
45//! # }
46//! fn print_hello(language: Language, name: Option<&str>){
47//!   if let Some(name) = name {
48//!     println!("{}", language.str_hello_you(name));
49//!   } else {
50//!     println!("{}", language.str_hello_world());
51//!   }
52//! }
53//! ```
54//!
55//! A global stored language is also supported, see [global](#global) below.
56//!
57//! ## More builders
58//!
59//! Different generators add different code:
60//! ```rust
61//! # use typed_i18n::TypedI18N;
62//! # #[derive(Copy, Clone, TypedI18N)]
63//! # #[typed_i18n(filename = "example.yaml")]
64//! #[typed_i18n(builder = "static_str", prefix = "sta_")]
65//! #[typed_i18n(builder = "String")]
66//! # enum Language { En, De }
67//! ```
68//!
69//! Generated code:
70//! ```rust
71//! # struct Language;
72//! impl Language {
73//! # } trait LanguageTest {
74//!     fn sta_hello_world(self) -> &'static str;
75//!     // The static_str builder skips all parameterized messages
76//!     fn hello_world(self) -> String;
77//!     fn hello_you(self, name: &str) -> String;
78//! }
79//! ```
80//!
81//! ## Other output types
82//!
83//! Also types other than strings are possible:
84//! ```yaml
85//! #in addition to the messages above
86//! hello_with_icon:
87//!   en: Hello %{name}*{icon}
88//! ```
89//!
90//! ```rust
91//! # use typed_i18n::{Builder, TypedI18N};
92//! # struct HtmlBuilder;
93//! # struct Html;
94//! # impl Builder for HtmlBuilder {
95//! #   type Output = Html;
96//! #   fn new() -> Self { todo!() }
97//! #   fn push_str(self, i: &str) -> Self { todo!() }
98//! #   fn finish(self) -> Self::Output { todo!() }
99//! # }
100//! # impl typed_i18n::BuilderFromValue<Html> for HtmlBuilder {fn push(self, i: Html) -> Self { todo!() } }
101//! # #[derive(Copy, Clone, TypedI18N)]
102//! # #[typed_i18n(filename = "example.yaml")]
103//! #[typed_i18n(builder = "HtmlBuilder", input = "Html", prefix = "html_")]
104//! # enum Language { En, De }
105//! ```
106//!
107//! Generated code:
108//! ```rust
109//! # use typed_i18n::Builder;
110//! # struct HtmlBuilder;
111//! # struct Html;
112//! # impl Builder for HtmlBuilder {
113//! #   type Output = Html;
114//! #   fn new() -> Self { todo!() }
115//! #   fn push_str(self, i: &str) -> Self { todo!() }
116//! #   fn finish(self) -> Self::Output { todo!() }
117//! # }
118//! # struct Language;
119//! impl Language {
120//! # } trait LanguageTest {
121//!     fn html_hello_world(self) -> <HtmlBuilder as Builder>::Output;
122//!     fn html_hello_you(self, name: &str) -> <HtmlBuilder as Builder>::Output;
123//!     fn html_hello_with_icon<T1: Into<Html>>(
124//!         self,
125//!         name: &str,
126//!         icon: T1,
127//!     ) -> <HtmlBuilder as Builder>::Output;
128//! }
129//! ```
130//! See [examples](https://github.com/alexkazik/typed-i18n/blob/main/examples/src/html.rs) for a `HtmlBuilder` for `yew::Html` implementation.
131//!
132//! # Input
133//!
134//! Fields:
135//!
136//! - `filename`: the path to the translations, relative to the crate root (required).
137//! - `separator`: used for combining paths of a tree, default: `_`.
138//! - `global`: used for a global stored language, see [global](#global) below, default: not used.
139//!
140//! Example:
141//!
142//! `#[typed_i18n(filename = "example.yaml", separator = "_")]`
143//!
144//! The messages can be supplied in
145//! - yaml (as seen in the examples above)
146//! - json
147//! - lrc
148//!
149//! json:
150//! ```json
151//! {"hello_world": {"en": "Hello World"}}
152//! ```
153//!
154//! The yaml and json files may have a `_version: 2` entry, to be compatible
155//! with other i18n tools. (Other numbers are an error, omitting it is ok.)
156//!
157//! The yaml and json files may have a deeper tree. In that case the separator is used to join
158//! the segments into a function name.
159//!
160//! Example:
161//! ```yaml
162//! hello:
163//!   world:
164//!     en: Hello World
165//!   you:
166//!     en: Hello %{name}
167//! ```
168//! With the separator `_` it will result in the same names as the examples above.
169//! This may help you structure the messages.
170//!
171//! Since the name is a function name a `.` as a separator is not allowed, but you can use `ยท` (U+00B7).
172//!
173//! lrc:
174//! ```text
175//! ; lines starting with `;` or `/` will be skipped
176//! #hello_world
177//!   en Hello World
178//! ```
179//!
180//! # Output
181//!
182//! Fields:
183//!
184//! - `builder`: The name of an item which implements [`Builder`], or a special value.
185//! - `prefix`: Prefix for all functions generated by this builder, default: the empty string.
186//! - `str_conversion`: How to convert str parameters, default `ref`.
187//! - `input`: Type of the input (for typed inputs), default: no input.
188//! - `input_conversion`: How to convert the parameter into the input type, default: `into`.
189//!
190//! ## `builder`
191//!
192//! Must be either a special value or a type for which [`Builder`] is implemented.
193//!
194//! All builders without a input type will skip all messages for it.
195//!
196//! All the builders below are built-in. The `input` must not be set for these. Always available:
197//!
198//! * `static_str`: all messages without parameters have the return type `&'static str`. All others are skipped.
199//! * `mixed_str`: all messages without parameters have the return type `&'static str` all others will have the return type `String`.
200//! * `String`
201//! * `Cow<'static, str>`
202//! * `_`: The functions will be generic over the builder type. This is sometimes not helpful,
203//!   e.g. when `.into()` or `.as_ref()` will be called on the result of the function.
204//!
205//! All except `static_str` and `_` require the feature `alloc` (enabled by default) and that `String` or `Cow` is in scope.
206//!
207//! To learn about custom type builder see the example above and in the [examples directory](https://github.com/alexkazik/typed-i18n/blob/main/examples).
208//!
209//! ## `prefix`
210//!
211//! All functions of this `builder` will be composed of the prefix and the message name.
212//!
213//! Using identical prefixes or overlapping names will result in functions with identical names and
214//! thus result in a compile error. This will not be checked by the derive macro.
215//!
216//! ## `str_conversion`
217//!
218//! The conversion for all string (normal) `%{param}` parameters:
219//!
220//! * `ref` (default): `fn str_hello_you(self, name: &str) -> String`.
221//! * `as_ref`: `fn str_hello_you<S1: AsRef<str>>(self, name: S1) -> String`.
222//!
223//! ## `input`
224//!
225//! Type of the input for typed `*{param}` parameters.
226//!
227//! All builders without `input` will silently skip all messages with typed parameters.
228//!
229//! With `_` a generic function over the input type is created. May lead to the same problems as
230//! a generic builder type.
231//!
232//! ## `input_conversion`
233//!
234//! How to convert the types inputs:
235//!
236//! * `value`: The input type as parameter:
237//!   `fn html_hello_with_icon(self, name: &str, icon: Input) -> <HtmlBuilder as BuilderFromValue>::Output`
238//! * `into` (default): Something that can be converted into the input type:
239//!   `fn html_hello_with_icon<T1: Into<Input>>(self, name: &str, icon: T1) -> <HtmlBuilder as BuilderFromValue>::Output`
240//! * `ref`: A reference to the input type:
241//!   `fn html_hello_with_icon(self, name: &str, icon: &Input) -> <HtmlBuilder as BuilderFromRef>::Output`
242//! * `as_ref`: Something that can be converted into a reference to the input type:
243//!   `fn html_hello_with_icon<T1: AsRef<Input>>(self, name: &str, icon: T1) -> <HtmlBuilder as BuilderFromRef>::Output`
244//!
245//! For `value` and `into` to work the builder must also implement [`BuilderFromValue`].
246//!
247//! For `ref` and `as_ref` to work the builder must also implement [`BuilderFromRef`].
248//!
249//! # Language
250//!
251//! The enum values can be annotated with:
252//!
253//! * `name`: The name of the language in the messages file, defaults to the name of the value in `snake_case`.
254//! * `fallback`: A space and/or comma separated list of language names which defines which language
255//!   should be used when a message is missing. Default: all languages in listing order (not necessary in numerical order).
256//! * `default`: Is used for a [global](#global) storage. Only one language may be the default.
257//!
258//! Example:
259//!
260//! ```rust
261//! # use typed_i18n::TypedI18N;
262//! #[derive(Copy, Clone, TypedI18N)]
263//! #[typed_i18n(filename = "example.yaml")]
264//! #[typed_i18n(builder = "mixed_str", prefix = "str_")]
265//! enum Language {
266//!   De,
267//!   #[typed_i18n(name = "en")]
268//!   English,
269//!   #[typed_i18n(fallback = "en, de")]
270//!   EnAu,
271//! }
272//! ```
273//!
274//! # Global
275//!
276//! It is possible to generate a global language storage.
277//!
278//! Code:
279//! ```rust
280//! # use typed_i18n::TypedI18N;
281//! #[derive(Copy, Clone, TypedI18N)]
282//! #[typed_i18n(filename = "example.yaml", global = "atomic")]
283//! #[typed_i18n(builder = "static_str")]
284//! enum Language { En, #[typed_i18n(default = "true")] De }
285//! ```
286//!
287//! Generated code:
288//! ```rust
289//! # enum Language { En, De }
290//! impl Language {
291//! # } trait LanguageTest {
292//!     fn global() -> Self;
293//!     fn set_global(self);
294//! }
295//! ```
296//!
297//! Example usage:
298//! ```rust
299//! # use typed_i18n::TypedI18N;
300//! # #[derive(Copy, Clone, TypedI18N)]
301//! # #[typed_i18n(filename = "example.yaml", global = "atomic")]
302//! # #[typed_i18n(builder = "static_str")]
303//! # enum Language { En, #[typed_i18n(default = "true")] De }
304//! // de is the default
305//! assert_eq!(Language::global().hello_world(), "Hallo Welt");
306//! Language::En.set_global();
307//! assert_eq!(Language::global().hello_world(), "Hello world");
308//! ```
309//!
310//! The default language (either marked as such, see example above, or the first one) is initially
311//! stored as the global language.
312//!
313//! Currently `atomic` is the only implementation, which uses an [`AtomicU8`](::core::sync::atomic::AtomicU8)
314//! to store the language. The conversion does not depend on the representation of the enum.
315//! In case of more than 256 languages an [`AtomicUsize`](::core::sync::atomic::AtomicUsize) is used.
316//!
317//! # Features
318//!
319//! - `alloc`, enabled by default: Provide Builder implementations for `String` and `Cow<'static, str>`, also support `mixed_str`.
320//!
321//! The library is always `no_std`.
322
323#[cfg(feature = "alloc")]
324mod alloc;
325
326pub use typed_i18n_derive::TypedI18N;
327
328/// Trait to create localized strings (from constant messages and string parameters).
329pub trait Builder: Sized {
330    /// The output type.
331    type Output;
332
333    /// Create a new empty result.
334    #[inline]
335    #[must_use]
336    fn empty() -> Self::Output {
337        Self::const_str("")
338    }
339
340    /// Create a new result from a const string.
341    #[inline]
342    #[must_use]
343    fn const_str(i: &'static str) -> Self::Output {
344        Self::new().push_const_str(i).finish()
345    }
346
347    /// Create a new builder.
348    #[must_use]
349    fn new() -> Self;
350
351    /// Add a const string to the builder.
352    #[inline]
353    #[must_use]
354    fn push_const_str(self, i: &'static str) -> Self {
355        self.push_str(i)
356    }
357
358    /// Add a string to the builder.
359    #[must_use]
360    fn push_str(self, i: &str) -> Self;
361
362    /// Convert the builder into the output.
363    #[must_use]
364    fn finish(self) -> Self::Output;
365}
366
367/// Trait to create localized strings from a reference to a value.
368pub trait BuilderFromRef<Input: ?Sized>: Builder {
369    /// Add a typed parameter by reference to the builder.
370    #[must_use]
371    fn push(self, i: &Input) -> Self;
372}
373
374/// Trait to create localized strings from a value.
375pub trait BuilderFromValue<Input>: Builder {
376    /// Add a typed parameter by value to the builder.
377    #[must_use]
378    fn push(self, i: Input) -> Self;
379}