atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
4b690ff501b89a8185e0f0c9ceaa61a89ce1a056
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git

Blame: src/locale.rs

ef7d6dee5976atagen2026-10-10 13:23 UTC1
//! The game is written in en-GB. Other locales are produced by rewriting regional phrases.
ef7d6dee5976atagen2026-10-10 13:23 UTC2
ef7d6dee5976atagen2026-10-10 13:23 UTC3
use serde_json::Value;
ef7d6dee5976atagen2026-10-10 13:23 UTC4
ef7d6dee5976atagen2026-10-10 13:23 UTC5
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
ef7d6dee5976atagen2026-10-10 13:23 UTC6
pub enum Locale {
ef7d6dee5976atagen2026-10-10 13:23 UTC7
    #[default]
ef7d6dee5976atagen2026-10-10 13:23 UTC8
    British,
ef7d6dee5976atagen2026-10-10 13:23 UTC9
    American,
ef7d6dee5976atagen2026-10-10 13:23 UTC10
    Australian,
ef7d6dee5976atagen2026-10-10 13:23 UTC11
}
ef7d6dee5976atagen2026-10-10 13:23 UTC12
ef7d6dee5976atagen2026-10-10 13:23 UTC13
/// Ordered so that longer phrases are rewritten before the words inside them.
ef7d6dee5976atagen2026-10-10 13:23 UTC14
const EN_US: &[(&str, &str)] = &[
ef7d6dee5976atagen2026-10-10 13:23 UTC15
    (
ef7d6dee5976atagen2026-10-10 13:23 UTC16
        "Nan's house at Christmas",
ef7d6dee5976atagen2026-10-10 13:23 UTC17
        "Grandma's house for Thanksgiving",
ef7d6dee5976atagen2026-10-10 13:23 UTC18
    ),
ef7d6dee5976atagen2026-10-10 13:23 UTC19
    ("Christmas dinner", "Thanksgiving dinner"),
ef7d6dee5976atagen2026-10-10 13:23 UTC20
    (
ef7d6dee5976atagen2026-10-10 13:23 UTC21
        "The King's speech is about to start.",
ef7d6dee5976atagen2026-10-10 13:23 UTC22
        "The game is about to start.",
ef7d6dee5976atagen2026-10-10 13:23 UTC23
    ),
ef7d6dee5976atagen2026-10-10 13:23 UTC24
    ("wrapping presents", "carving the turkey"),
ef7d6dee5976atagen2026-10-10 13:23 UTC25
    ("folding the washing", "folding laundry"),
ef7d6dee5976atagen2026-10-10 13:23 UTC26
    ("your washing", "your laundry"),
ef7d6dee5976atagen2026-10-10 13:23 UTC27
    ("changing room", "locker room"),
ef7d6dee5976atagen2026-10-10 13:23 UTC28
    ("flatmate", "roommate"),
ef7d6dee5976atagen2026-10-10 13:23 UTC29
    ("authorisation", "authorization"),
ef7d6dee5976atagen2026-10-10 13:23 UTC30
    ("neighbour", "neighbor"),
ef7d6dee5976atagen2026-10-10 13:23 UTC31
    ("cupboard", "pantry"),
ef7d6dee5976atagen2026-10-10 13:23 UTC32
    ("Christmas", "Thanksgiving"),
ef7d6dee5976atagen2026-10-10 13:23 UTC33
    ("Mum", "Mom"),
ef7d6dee5976atagen2026-10-10 13:23 UTC34
    ("mum", "mom"),
ef7d6dee5976atagen2026-10-10 13:23 UTC35
    ("Nan", "Grandma"),
ef7d6dee5976atagen2026-10-10 13:23 UTC36
    ("nan", "grandma"),
ef7d6dee5976atagen2026-10-10 13:23 UTC37
    ("zip", "zipper"),
ef7d6dee5976atagen2026-10-10 13:23 UTC38
    ("£", "$"),
ef7d6dee5976atagen2026-10-10 13:23 UTC39
];
ef7d6dee5976atagen2026-10-10 13:23 UTC40
ef7d6dee5976atagen2026-10-10 13:23 UTC41
const EN_AU: &[(&str, &str)] = &[
ef7d6dee5976atagen2026-10-10 13:23 UTC42
    ("Christmas dinner", "Christmas lunch"),
ef7d6dee5976atagen2026-10-10 13:23 UTC43
    (
ef7d6dee5976atagen2026-10-10 13:23 UTC44
        "The King's speech is about to start.",
ef7d6dee5976atagen2026-10-10 13:23 UTC45
        "The cricket is about to start.",
ef7d6dee5976atagen2026-10-10 13:23 UTC46
    ),
ef7d6dee5976atagen2026-10-10 13:23 UTC47
    ("wrapping presents", "getting ice from the servo"),
ef7d6dee5976atagen2026-10-10 13:23 UTC48
    ("changing room", "change rooms"),
ef7d6dee5976atagen2026-10-10 13:23 UTC49
    ("flatmate", "housemate"),
ef7d6dee5976atagen2026-10-10 13:23 UTC50
    ("cupboard", "pantry"),
ef7d6dee5976atagen2026-10-10 13:23 UTC51
    ("zip", "fly"),
ef7d6dee5976atagen2026-10-10 13:23 UTC52
    ("£", "$"),
ef7d6dee5976atagen2026-10-10 13:23 UTC53
];
ef7d6dee5976atagen2026-10-10 13:23 UTC54
ef7d6dee5976atagen2026-10-10 13:23 UTC55
const EXCUSES: &[&str] = &["wrapping presents", "folding the washing"];
ef7d6dee5976atagen2026-10-10 13:23 UTC56
ef7d6dee5976atagen2026-10-10 13:23 UTC57
fn bounded(text: &str, at: usize, len: usize) -> bool {
ef7d6dee5976atagen2026-10-10 13:23 UTC58
    let before = text[..at].chars().next_back();
ef7d6dee5976atagen2026-10-10 13:23 UTC59
    let after = text[at + len..].chars().next();
ef7d6dee5976atagen2026-10-10 13:23 UTC60
    let edge = |c: Option<char>| c.is_none_or(|c| !c.is_alphanumeric());
ef7d6dee5976atagen2026-10-10 13:23 UTC61
    edge(before) && edge(after)
ef7d6dee5976atagen2026-10-10 13:23 UTC62
}
ef7d6dee5976atagen2026-10-10 13:23 UTC63
ef7d6dee5976atagen2026-10-10 13:23 UTC64
fn replace_words(text: &str, from: &str, to: &str) -> String {
ef7d6dee5976atagen2026-10-10 13:23 UTC65
    let mut out = String::with_capacity(text.len());
ef7d6dee5976atagen2026-10-10 13:23 UTC66
    let mut rest = 0;
ef7d6dee5976atagen2026-10-10 13:23 UTC67
    for (at, _) in text.match_indices(from) {
ef7d6dee5976atagen2026-10-10 13:23 UTC68
        if at < rest || !bounded(text, at, from.len()) {
ef7d6dee5976atagen2026-10-10 13:23 UTC69
            continue;
ef7d6dee5976atagen2026-10-10 13:23 UTC70
        }
ef7d6dee5976atagen2026-10-10 13:23 UTC71
        out.push_str(&text[rest..at]);
ef7d6dee5976atagen2026-10-10 13:23 UTC72
        out.push_str(to);
ef7d6dee5976atagen2026-10-10 13:23 UTC73
        rest = at + from.len();
ef7d6dee5976atagen2026-10-10 13:23 UTC74
    }
ef7d6dee5976atagen2026-10-10 13:23 UTC75
    out.push_str(&text[rest..]);
ef7d6dee5976atagen2026-10-10 13:23 UTC76
    out
ef7d6dee5976atagen2026-10-10 13:23 UTC77
}
ef7d6dee5976atagen2026-10-10 13:23 UTC78
ef7d6dee5976atagen2026-10-10 13:23 UTC79
impl Locale {
ef7d6dee5976atagen2026-10-10 13:23 UTC80
    pub fn tag(self) -> &'static str {
ef7d6dee5976atagen2026-10-10 13:23 UTC81
        match self {
ef7d6dee5976atagen2026-10-10 13:23 UTC82
            Self::British => "en-GB",
ef7d6dee5976atagen2026-10-10 13:23 UTC83
            Self::American => "en-US",
ef7d6dee5976atagen2026-10-10 13:23 UTC84
            Self::Australian => "en-AU",
ef7d6dee5976atagen2026-10-10 13:23 UTC85
        }
ef7d6dee5976atagen2026-10-10 13:23 UTC86
    }
ef7d6dee5976atagen2026-10-10 13:23 UTC87
ef7d6dee5976atagen2026-10-10 13:23 UTC88
    fn parse(tag: &str) -> Option<Self> {
ef7d6dee5976atagen2026-10-10 13:23 UTC89
        match tag.trim().to_ascii_lowercase().as_str() {
ef7d6dee5976atagen2026-10-10 13:23 UTC90
            "en-gb" | "en" => Some(Self::British),
ef7d6dee5976atagen2026-10-10 13:23 UTC91
            "en-us" => Some(Self::American),
ef7d6dee5976atagen2026-10-10 13:23 UTC92
            "en-au" => Some(Self::Australian),
ef7d6dee5976atagen2026-10-10 13:23 UTC93
            _ => None,
ef7d6dee5976atagen2026-10-10 13:23 UTC94
        }
ef7d6dee5976atagen2026-10-10 13:23 UTC95
    }
ef7d6dee5976atagen2026-10-10 13:23 UTC96
ef7d6dee5976atagen2026-10-10 13:23 UTC97
    /// Picks the most preferred supported locale from an `Accept-Language` header (RFC 9110 §12.5.4).
ef7d6dee5976atagen2026-10-10 13:23 UTC98
    pub fn negotiate(accept_language: Option<&str>) -> Self {
ef7d6dee5976atagen2026-10-10 13:23 UTC99
        let Some(header) = accept_language else {
ef7d6dee5976atagen2026-10-10 13:23 UTC100
            return Self::default();
ef7d6dee5976atagen2026-10-10 13:23 UTC101
        };
ef7d6dee5976atagen2026-10-10 13:23 UTC102
        header
ef7d6dee5976atagen2026-10-10 13:23 UTC103
            .split(',')
ef7d6dee5976atagen2026-10-10 13:23 UTC104
            .filter_map(|range| {
ef7d6dee5976atagen2026-10-10 13:23 UTC105
                let mut parts = range.split(';');
ef7d6dee5976atagen2026-10-10 13:23 UTC106
                let locale = Self::parse(parts.next()?)?;
ef7d6dee5976atagen2026-10-10 13:23 UTC107
                let quality = parts
ef7d6dee5976atagen2026-10-10 13:23 UTC108
                    .find_map(|p| p.trim().strip_prefix("q="))
ef7d6dee5976atagen2026-10-10 13:23 UTC109
                    .and_then(|q| q.parse::<f32>().ok())
ef7d6dee5976atagen2026-10-10 13:23 UTC110
                    .unwrap_or(1.0);
ef7d6dee5976atagen2026-10-10 13:23 UTC111
                (quality > 0.0).then_some((locale, quality))
ef7d6dee5976atagen2026-10-10 13:23 UTC112
            })
ef7d6dee5976atagen2026-10-10 13:23 UTC113
            .max_by(|a, b| a.1.total_cmp(&b.1))
ef7d6dee5976atagen2026-10-10 13:23 UTC114
            .map_or_else(Self::default, |(locale, _)| locale)
ef7d6dee5976atagen2026-10-10 13:23 UTC115
    }
ef7d6dee5976atagen2026-10-10 13:23 UTC116
ef7d6dee5976atagen2026-10-10 13:23 UTC117
    fn phrases(self) -> &'static [(&'static str, &'static str)] {
ef7d6dee5976atagen2026-10-10 13:23 UTC118
        match self {
ef7d6dee5976atagen2026-10-10 13:23 UTC119
            Self::British => &[],
ef7d6dee5976atagen2026-10-10 13:23 UTC120
            Self::American => EN_US,
ef7d6dee5976atagen2026-10-10 13:23 UTC121
            Self::Australian => EN_AU,
ef7d6dee5976atagen2026-10-10 13:23 UTC122
        }
ef7d6dee5976atagen2026-10-10 13:23 UTC123
    }
ef7d6dee5976atagen2026-10-10 13:23 UTC124
ef7d6dee5976atagen2026-10-10 13:23 UTC125
    pub fn localise(self, text: &str) -> String {
ef7d6dee5976atagen2026-10-10 13:23 UTC126
        self.phrases()
ef7d6dee5976atagen2026-10-10 13:23 UTC127
            .iter()
ef7d6dee5976atagen2026-10-10 13:23 UTC128
            .fold(text.to_owned(), |text, (from, to)| {
ef7d6dee5976atagen2026-10-10 13:23 UTC129
                replace_words(&text, from, to)
ef7d6dee5976atagen2026-10-10 13:23 UTC130
            })
ef7d6dee5976atagen2026-10-10 13:23 UTC131
    }
ef7d6dee5976atagen2026-10-10 13:23 UTC132
ef7d6dee5976atagen2026-10-10 13:23 UTC133
    /// Translates the excuses a player types back into the en-GB phrases the game checks against.
ef7d6dee5976atagen2026-10-10 13:23 UTC134
    pub fn canonicalise(self, text: &str) -> String {
ef7d6dee5976atagen2026-10-10 13:23 UTC135
        let local = |excuse: &str| self.localise(excuse);
ef7d6dee5976atagen2026-10-10 13:23 UTC136
        EXCUSES.iter().fold(text.to_owned(), |text, excuse| {
ef7d6dee5976atagen2026-10-10 13:23 UTC137
            replace_words(&text, &local(excuse), excuse)
ef7d6dee5976atagen2026-10-10 13:23 UTC138
        })
ef7d6dee5976atagen2026-10-10 13:23 UTC139
    }
ef7d6dee5976atagen2026-10-10 13:23 UTC140
ef7d6dee5976atagen2026-10-10 13:23 UTC141
    pub fn localise_json(self, value: &mut Value) {
ef7d6dee5976atagen2026-10-10 13:23 UTC142
        match value {
ef7d6dee5976atagen2026-10-10 13:23 UTC143
            Value::String(text) if !text.starts_with('/') && !text.starts_with("http") => {
ef7d6dee5976atagen2026-10-10 13:23 UTC144
                *text = self.localise(text);
ef7d6dee5976atagen2026-10-10 13:23 UTC145
            }
ef7d6dee5976atagen2026-10-10 13:23 UTC146
            Value::Array(items) => items.iter_mut().for_each(|item| self.localise_json(item)),
ef7d6dee5976atagen2026-10-10 13:23 UTC147
            Value::Object(fields) => {
ef7d6dee5976atagen2026-10-10 13:23 UTC148
                for (key, field) in fields.iter_mut() {
ef7d6dee5976atagen2026-10-10 13:23 UTC149
                    if !matches!(key.as_str(), "href" | "type" | "instance") {
ef7d6dee5976atagen2026-10-10 13:23 UTC150
                        self.localise_json(field);
ef7d6dee5976atagen2026-10-10 13:23 UTC151
                    }
ef7d6dee5976atagen2026-10-10 13:23 UTC152
                }
ef7d6dee5976atagen2026-10-10 13:23 UTC153
            }
ef7d6dee5976atagen2026-10-10 13:23 UTC154
            _ => {}
ef7d6dee5976atagen2026-10-10 13:23 UTC155
        }
ef7d6dee5976atagen2026-10-10 13:23 UTC156
    }
ef7d6dee5976atagen2026-10-10 13:23 UTC157
}
ef7d6dee5976atagen2026-10-10 13:23 UTC158
ef7d6dee5976atagen2026-10-10 13:23 UTC159
#[cfg(test)]
ef7d6dee5976atagen2026-10-10 13:23 UTC160
mod tests {
ef7d6dee5976atagen2026-10-10 13:23 UTC161
    use super::*;
ef7d6dee5976atagen2026-10-10 13:23 UTC162
ef7d6dee5976atagen2026-10-10 13:23 UTC163
    #[test]
ef7d6dee5976atagen2026-10-10 13:23 UTC164
    fn negotiates_by_quality() {
ef7d6dee5976atagen2026-10-10 13:23 UTC165
        assert_eq!(Locale::negotiate(None), Locale::British);
ef7d6dee5976atagen2026-10-10 13:23 UTC166
        assert_eq!(Locale::negotiate(Some("en-US")), Locale::American);
ef7d6dee5976atagen2026-10-10 13:23 UTC167
        assert_eq!(
ef7d6dee5976atagen2026-10-10 13:23 UTC168
            Locale::negotiate(Some("fr, en-AU;q=0.8, en-US;q=0.5")),
ef7d6dee5976atagen2026-10-10 13:23 UTC169
            Locale::Australian
ef7d6dee5976atagen2026-10-10 13:23 UTC170
        );
ef7d6dee5976atagen2026-10-10 13:23 UTC171
        assert_eq!(Locale::negotiate(Some("de")), Locale::British);
ef7d6dee5976atagen2026-10-10 13:23 UTC172
    }
ef7d6dee5976atagen2026-10-10 13:23 UTC173
ef7d6dee5976atagen2026-10-10 13:23 UTC174
    #[test]
ef7d6dee5976atagen2026-10-10 13:23 UTC175
    fn rewrites_whole_words_only() {
ef7d6dee5976atagen2026-10-10 13:23 UTC176
        let us = Locale::American;
ef7d6dee5976atagen2026-10-10 13:23 UTC177
        assert_eq!(
ef7d6dee5976atagen2026-10-10 13:23 UTC178
            us.localise("Mum's zip, not gzip."),
ef7d6dee5976atagen2026-10-10 13:23 UTC179
            "Mom's zipper, not gzip."
ef7d6dee5976atagen2026-10-10 13:23 UTC180
        );
ef7d6dee5976atagen2026-10-10 13:23 UTC181
        assert_eq!(us.localise("1.1 mum"), "1.1 mom");
ef7d6dee5976atagen2026-10-10 13:23 UTC182
        assert_eq!(
ef7d6dee5976atagen2026-10-10 13:23 UTC183
            Locale::Australian.localise("The request conflicts with the current state of the zip."),
ef7d6dee5976atagen2026-10-10 13:23 UTC184
            "The request conflicts with the current state of the fly."
ef7d6dee5976atagen2026-10-10 13:23 UTC185
        );
ef7d6dee5976atagen2026-10-10 13:23 UTC186
    }
ef7d6dee5976atagen2026-10-10 13:23 UTC187
ef7d6dee5976atagen2026-10-10 13:23 UTC188
    #[test]
ef7d6dee5976atagen2026-10-10 13:23 UTC189
    fn excuses_come_back_to_en_gb() {
ef7d6dee5976atagen2026-10-10 13:23 UTC190
        assert_eq!(
ef7d6dee5976atagen2026-10-10 13:23 UTC191
            Locale::American.canonicalise("Digest username=\"carving the turkey\""),
ef7d6dee5976atagen2026-10-10 13:23 UTC192
            "Digest username=\"wrapping presents\""
ef7d6dee5976atagen2026-10-10 13:23 UTC193
        );
ef7d6dee5976atagen2026-10-10 13:23 UTC194
    }
ef7d6dee5976atagen2026-10-10 13:23 UTC195
}