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
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};

// TODO: expose convinience macros ala 'fluent_args!'?

/// The type to represent generic localization request, to be sent from server
/// to client and then localized (or internationalized) there.
// TODO: Ideally we would need to fully cover API of our `i18n::Language`, including
// Fluent values.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Content {
    /// Plain(text)
    ///
    /// The content is a plaintext string that should be shown to the user
    /// verbatim.
    Plain(String),
    /// Key(i18n_key)
    ///
    /// The content is defined just by the key
    Key(String),
    /// Attr(i18n_key, attr)
    ///
    /// The content is the attribute of the key
    Attr(String, String),
    /// The content is a localizable message with the given arguments.
    // TODO: reduce usages of random i18n as much as possible
    //
    // It's ok to have random messages, just not at i18n step.
    // Look for issue on `get_vartion` at Gitlab for more.
    Localized {
        /// i18n key
        key: String,
        /// Pseudorandom seed value that allows frontends to select a
        /// deterministic (but pseudorandom) localised output
        #[serde(default = "random_seed")]
        seed: u16,
        /// i18n arguments
        #[serde(default)]
        args: HashMap<String, LocalizationArg>,
    },
}

// TODO: Remove impl and make use of `Plain(...)` explicit (to discourage it)
impl From<String> for Content {
    fn from(text: String) -> Self { Self::Plain(text) }
}

// TODO: Remove impl and make use of `Plain(...)` explicit (to discourage it)
impl<'a> From<&'a str> for Content {
    fn from(text: &'a str) -> Self { Self::Plain(text.to_string()) }
}

/// A localisation argument for localised content (see [`Content::Localized`]).
// TODO: Do we want it to be Enum or just wrapper around Content, to add
// additional `impl From<T>` for our arguments?
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum LocalizationArg {
    /// The localisation argument is itself a section of content.
    ///
    /// Note that this allows [`Content`] to recursively refer to itself. It may
    /// be tempting to decide to parameterise everything, having dialogue
    /// generated with a compact tree. "It's simpler!", you might say. False.
    /// Over-parameterisation is an anti-pattern that hurts translators. Where
    /// possible, prefer fewer levels of nesting unless doing so would result
    /// in an intractably larger number of combinations. See [here] for the
    /// guidance provided by the docs for `fluent`, the localisation library
    /// used by clients.
    ///
    /// [here]: https://github.com/projectfluent/fluent/wiki/Good-Practices-for-Developers#prefer-wet-over-dry
    Content(Content),
    /// The localisation argument is a natural number
    Nat(u64),
}

impl From<Content> for LocalizationArg {
    fn from(content: Content) -> Self { Self::Content(content) }
}

// TODO: Remove impl and make use of `Content(Plain(...))` explicit (to
// discourage it)
//
// Or not?
impl From<String> for LocalizationArg {
    fn from(text: String) -> Self { Self::Content(Content::Plain(text)) }
}

// TODO: Remove impl and make use of `Content(Plain(...))` explicit (to
// discourage it)
//
// Or not?
impl<'a> From<&'a str> for LocalizationArg {
    fn from(text: &'a str) -> Self { Self::Content(Content::Plain(text.to_string())) }
}

impl From<u64> for LocalizationArg {
    fn from(n: u64) -> Self { Self::Nat(n) }
}

fn random_seed() -> u16 { rand::random() }

impl Content {
    pub fn localized(key: impl ToString) -> Self {
        Self::Localized {
            key: key.to_string(),
            seed: random_seed(),
            args: HashMap::default(),
        }
    }

    pub fn localized_with_args<'a, A: Into<LocalizationArg>>(
        key: impl ToString,
        args: impl IntoIterator<Item = (&'a str, A)>,
    ) -> Self {
        Self::Localized {
            key: key.to_string(),
            seed: rand::random(),
            args: args
                .into_iter()
                .map(|(k, v)| (k.to_string(), v.into()))
                .collect(),
        }
    }

    pub fn as_plain(&self) -> Option<&str> {
        match self {
            Self::Plain(text) => Some(text.as_str()),
            Self::Localized { .. } | Self::Attr { .. } | Self::Key { .. } => None,
        }
    }
}