veloren_common_i18n/lib.rs
1use hashbrown::HashMap;
2use serde::{Deserialize, Serialize};
3
4// TODO: expose convinience macros ala 'fluent_args!'?
5
6/// The type to represent generic localization request, to be sent from server
7/// to client and then localized (or internationalized) there.
8// TODO: Ideally we would need to fully cover API of our `i18n::Language`, including
9// Fluent values.
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
11pub enum Content {
12 /// Plain(text)
13 ///
14 /// The content is a plaintext string that should be shown to the user
15 /// verbatim.
16 Plain(String),
17 /// Key(i18n_key)
18 ///
19 /// The content is defined just by the key
20 Key(String),
21 /// Attr(i18n_key, attr)
22 ///
23 /// The content is the attribute of the key
24 Attr(String, String),
25 /// The content is a localizable message with the given arguments.
26 // TODO: reduce usages of random i18n as much as possible
27 //
28 // It's ok to have random messages, just not at i18n step.
29 // Look for issue on `get_vartion` at Gitlab for more.
30 Localized {
31 /// i18n key
32 key: String,
33 /// Pseudorandom seed value that allows frontends to select a
34 /// deterministic (but pseudorandom) localised output
35 #[serde(default = "random_seed")]
36 seed: u16,
37 /// i18n arguments
38 #[serde(default)]
39 args: HashMap<String, LocalizationArg>,
40 },
41 /// All of the above, but can use an optional fallback
42 WithFallback(Box<Content>, Box<Content>),
43}
44
45/// A localisation argument for localised content (see [`Content::Localized`]).
46// TODO: Do we want it to be Enum or just wrapper around Content, to add
47// additional `impl From<T>` for our arguments?
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
49pub enum LocalizationArg {
50 /// The localisation argument is itself a section of content.
51 ///
52 /// Note that this allows [`Content`] to recursively refer to itself. It may
53 /// be tempting to decide to parameterise everything, having dialogue
54 /// generated with a compact tree. "It's simpler!", you might say. False.
55 /// Over-parameterisation is an anti-pattern that hurts translators. Where
56 /// possible, prefer fewer levels of nesting unless doing so would result
57 /// in an intractably larger number of combinations. See [here] for the
58 /// guidance provided by the docs for `fluent`, the localisation library
59 /// used by clients.
60 ///
61 /// [here]: https://github.com/projectfluent/fluent/wiki/Good-Practices-for-Developers#prefer-wet-over-dry
62 Content(Content),
63 /// The localisation argument is a natural number
64 Nat(u64),
65}
66
67impl From<Content> for LocalizationArg {
68 fn from(content: Content) -> Self { Self::Content(content) }
69}
70
71// TODO: Remove impl and make use of `Content(Plain(...))` explicit (to
72// discourage it)
73//
74// Or not?
75impl From<String> for LocalizationArg {
76 fn from(text: String) -> Self { Self::Content(Content::Plain(text)) }
77}
78
79// TODO: Remove impl and make use of `Content(Plain(...))` explicit (to
80// discourage it)
81//
82// Or not?
83impl<'a> From<&'a str> for LocalizationArg {
84 fn from(text: &'a str) -> Self { Self::Content(Content::Plain(text.to_string())) }
85}
86
87impl From<u64> for LocalizationArg {
88 fn from(n: u64) -> Self { Self::Nat(n) }
89}
90
91fn random_seed() -> u16 { rand::random() }
92
93impl Content {
94 pub fn dummy() -> Self { Self::Plain("".to_owned()) }
95
96 #[deprecated]
97 pub fn legacy(hardcoded: String) -> Self { Self::Plain(hardcoded) }
98
99 pub fn localized(key: impl ToString) -> Self {
100 Self::Localized {
101 key: key.to_string(),
102 seed: random_seed(),
103 args: HashMap::default(),
104 }
105 }
106
107 pub fn with_attr(key: impl ToString, attr: impl ToString) -> Self {
108 Self::Attr(key.to_string(), attr.to_string())
109 }
110
111 pub fn with_arg(mut self, key: &str, arg: impl Into<LocalizationArg>) -> Self {
112 if let Self::Localized { args, .. } = &mut self {
113 args.insert(key.to_string(), arg.into());
114 }
115 self
116 }
117
118 pub fn localized_with_args<'a, A: Into<LocalizationArg>>(
119 key: impl ToString,
120 args: impl IntoIterator<Item = (&'a str, A)>,
121 ) -> Self {
122 Self::Localized {
123 key: key.to_string(),
124 seed: rand::random(),
125 args: args
126 .into_iter()
127 .map(|(k, v)| (k.to_string(), v.into()))
128 .collect(),
129 }
130 }
131
132 pub fn localized_attr(key: impl ToString, attr: impl AsRef<str>) -> Self {
133 Self::Attr(key.to_string(), attr.as_ref().to_string())
134 }
135
136 pub fn as_plain(&self) -> Option<&str> {
137 match self {
138 Self::Plain(text) => Some(text.as_str()),
139 Self::Localized { .. }
140 | Self::Attr { .. }
141 | Self::Key { .. }
142 | Self::WithFallback { .. } => None,
143 }
144 }
145
146 /// As the name suggests, only use that as a last resort, and don't
147 /// expect great results
148 pub fn hacky_descriptor(&self) -> &str {
149 match self {
150 Self::Plain(text) => text.as_str(),
151 Self::Key(k) => k.as_str(),
152 Self::Attr(k, _) => k.as_str(),
153 Self::Localized { key, .. } => key.as_str(),
154 Self::WithFallback(first, _) => first.hacky_descriptor(),
155 }
156 }
157}