atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
7d3fb63208bf5064f04782aac0c0273305f8f35f
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
    ("authorisation", "authorization"),
7d3fb63208bfatagen2026-10-11 04:38 UTC29
    ("revising", "studying"),
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
    ("cupboard", "pantry"),
ef7d6dee5976atagen2026-10-10 13:23 UTC50
    ("zip", "fly"),
ef7d6dee5976atagen2026-10-10 13:23 UTC51
    ("£", "$"),
ef7d6dee5976atagen2026-10-10 13:23 UTC52
];
ef7d6dee5976atagen2026-10-10 13:23 UTC53
ef7d6dee5976atagen2026-10-10 13:23 UTC54
const EXCUSES: &[&str] = &["wrapping presents", "folding the washing"];
ef7d6dee5976atagen2026-10-10 13:23 UTC55
ef7d6dee5976atagen2026-10-10 13:23 UTC56
fn bounded(text: &str, at: usize, len: usize) -> bool {
ef7d6dee5976atagen2026-10-10 13:23 UTC57
    let before = text[..at].chars().next_back();
ef7d6dee5976atagen2026-10-10 13:23 UTC58
    let after = text[at + len..].chars().next();
ef7d6dee5976atagen2026-10-10 13:23 UTC59
    let edge = |c: Option<char>| c.is_none_or(|c| !c.is_alphanumeric());
ef7d6dee5976atagen2026-10-10 13:23 UTC60
    edge(before) && edge(after)
ef7d6dee5976atagen2026-10-10 13:23 UTC61
}
ef7d6dee5976atagen2026-10-10 13:23 UTC62
ef7d6dee5976atagen2026-10-10 13:23 UTC63
fn replace_words(text: &str, from: &str, to: &str) -> String {
ef7d6dee5976atagen2026-10-10 13:23 UTC64
    let mut out = String::with_capacity(text.len());
ef7d6dee5976atagen2026-10-10 13:23 UTC65
    let mut rest = 0;
ef7d6dee5976atagen2026-10-10 13:23 UTC66
    for (at, _) in text.match_indices(from) {
ef7d6dee5976atagen2026-10-10 13:23 UTC67
        if at < rest || !bounded(text, at, from.len()) {
ef7d6dee5976atagen2026-10-10 13:23 UTC68
            continue;
ef7d6dee5976atagen2026-10-10 13:23 UTC69
        }
ef7d6dee5976atagen2026-10-10 13:23 UTC70
        out.push_str(&text[rest..at]);
ef7d6dee5976atagen2026-10-10 13:23 UTC71
        out.push_str(to);
ef7d6dee5976atagen2026-10-10 13:23 UTC72
        rest = at + from.len();
ef7d6dee5976atagen2026-10-10 13:23 UTC73
    }
ef7d6dee5976atagen2026-10-10 13:23 UTC74
    out.push_str(&text[rest..]);
ef7d6dee5976atagen2026-10-10 13:23 UTC75
    out
ef7d6dee5976atagen2026-10-10 13:23 UTC76
}
ef7d6dee5976atagen2026-10-10 13:23 UTC77
ef7d6dee5976atagen2026-10-10 13:23 UTC78
impl Locale {
ef7d6dee5976atagen2026-10-10 13:23 UTC79
    pub fn tag(self) -> &'static str {
ef7d6dee5976atagen2026-10-10 13:23 UTC80
        match self {
ef7d6dee5976atagen2026-10-10 13:23 UTC81
            Self::British => "en-GB",
ef7d6dee5976atagen2026-10-10 13:23 UTC82
            Self::American => "en-US",
ef7d6dee5976atagen2026-10-10 13:23 UTC83
            Self::Australian => "en-AU",
ef7d6dee5976atagen2026-10-10 13:23 UTC84
        }
ef7d6dee5976atagen2026-10-10 13:23 UTC85
    }
ef7d6dee5976atagen2026-10-10 13:23 UTC86
ef7d6dee5976atagen2026-10-10 13:23 UTC87
    fn parse(tag: &str) -> Option<Self> {
ef7d6dee5976atagen2026-10-10 13:23 UTC88
        match tag.trim().to_ascii_lowercase().as_str() {
ef7d6dee5976atagen2026-10-10 13:23 UTC89
            "en-gb" | "en" => Some(Self::British),
ef7d6dee5976atagen2026-10-10 13:23 UTC90
            "en-us" => Some(Self::American),
ef7d6dee5976atagen2026-10-10 13:23 UTC91
            "en-au" => Some(Self::Australian),
ef7d6dee5976atagen2026-10-10 13:23 UTC92
            _ => None,
ef7d6dee5976atagen2026-10-10 13:23 UTC93
        }
ef7d6dee5976atagen2026-10-10 13:23 UTC94
    }
ef7d6dee5976atagen2026-10-10 13:23 UTC95
ef7d6dee5976atagen2026-10-10 13:23 UTC96
    /// Picks the most preferred supported locale from an `Accept-Language` header (RFC 9110 §12.5.4).
ef7d6dee5976atagen2026-10-10 13:23 UTC97
    pub fn negotiate(accept_language: Option<&str>) -> Self {
ef7d6dee5976atagen2026-10-10 13:23 UTC98
        let Some(header) = accept_language else {
ef7d6dee5976atagen2026-10-10 13:23 UTC99
            return Self::default();
ef7d6dee5976atagen2026-10-10 13:23 UTC100
        };
ef7d6dee5976atagen2026-10-10 13:23 UTC101
        header
ef7d6dee5976atagen2026-10-10 13:23 UTC102
            .split(',')
ef7d6dee5976atagen2026-10-10 13:23 UTC103
            .filter_map(|range| {
ef7d6dee5976atagen2026-10-10 13:23 UTC104
                let mut parts = range.split(';');
ef7d6dee5976atagen2026-10-10 13:23 UTC105
                let locale = Self::parse(parts.next()?)?;
ef7d6dee5976atagen2026-10-10 13:23 UTC106
                let quality = parts
ef7d6dee5976atagen2026-10-10 13:23 UTC107
                    .find_map(|p| p.trim().strip_prefix("q="))
ef7d6dee5976atagen2026-10-10 13:23 UTC108
                    .and_then(|q| q.parse::<f32>().ok())
ef7d6dee5976atagen2026-10-10 13:23 UTC109
                    .unwrap_or(1.0);
ef7d6dee5976atagen2026-10-10 13:23 UTC110
                (quality > 0.0).then_some((locale, quality))
ef7d6dee5976atagen2026-10-10 13:23 UTC111
            })
ef7d6dee5976atagen2026-10-10 13:23 UTC112
            .max_by(|a, b| a.1.total_cmp(&b.1))
ef7d6dee5976atagen2026-10-10 13:23 UTC113
            .map_or_else(Self::default, |(locale, _)| locale)
ef7d6dee5976atagen2026-10-10 13:23 UTC114
    }
ef7d6dee5976atagen2026-10-10 13:23 UTC115
ef7d6dee5976atagen2026-10-10 13:23 UTC116
    fn phrases(self) -> &'static [(&'static str, &'static str)] {
ef7d6dee5976atagen2026-10-10 13:23 UTC117
        match self {
ef7d6dee5976atagen2026-10-10 13:23 UTC118
            Self::British => &[],
ef7d6dee5976atagen2026-10-10 13:23 UTC119
            Self::American => EN_US,
ef7d6dee5976atagen2026-10-10 13:23 UTC120
            Self::Australian => EN_AU,
ef7d6dee5976atagen2026-10-10 13:23 UTC121
        }
ef7d6dee5976atagen2026-10-10 13:23 UTC122
    }
ef7d6dee5976atagen2026-10-10 13:23 UTC123
ef7d6dee5976atagen2026-10-10 13:23 UTC124
    pub fn localise(self, text: &str) -> String {
ef7d6dee5976atagen2026-10-10 13:23 UTC125
        self.phrases()
ef7d6dee5976atagen2026-10-10 13:23 UTC126
            .iter()
ef7d6dee5976atagen2026-10-10 13:23 UTC127
            .fold(text.to_owned(), |text, (from, to)| {
ef7d6dee5976atagen2026-10-10 13:23 UTC128
                replace_words(&text, from, to)
ef7d6dee5976atagen2026-10-10 13:23 UTC129
            })
ef7d6dee5976atagen2026-10-10 13:23 UTC130
    }
ef7d6dee5976atagen2026-10-10 13:23 UTC131
ef7d6dee5976atagen2026-10-10 13:23 UTC132
    /// Translates the excuses a player types back into the en-GB phrases the game checks against.
ef7d6dee5976atagen2026-10-10 13:23 UTC133
    pub fn canonicalise(self, text: &str) -> String {
ef7d6dee5976atagen2026-10-10 13:23 UTC134
        let local = |excuse: &str| self.localise(excuse);
ef7d6dee5976atagen2026-10-10 13:23 UTC135
        EXCUSES.iter().fold(text.to_owned(), |text, excuse| {
ef7d6dee5976atagen2026-10-10 13:23 UTC136
            replace_words(&text, &local(excuse), excuse)
ef7d6dee5976atagen2026-10-10 13:23 UTC137
        })
ef7d6dee5976atagen2026-10-10 13:23 UTC138
    }
ef7d6dee5976atagen2026-10-10 13:23 UTC139
ef7d6dee5976atagen2026-10-10 13:23 UTC140
    pub fn localise_json(self, value: &mut Value) {
ef7d6dee5976atagen2026-10-10 13:23 UTC141
        match value {
ef7d6dee5976atagen2026-10-10 13:23 UTC142
            Value::String(text) if !text.starts_with('/') && !text.starts_with("http") => {
ef7d6dee5976atagen2026-10-10 13:23 UTC143
                *text = self.localise(text);
ef7d6dee5976atagen2026-10-10 13:23 UTC144
            }
ef7d6dee5976atagen2026-10-10 13:23 UTC145
            Value::Array(items) => items.iter_mut().for_each(|item| self.localise_json(item)),
ef7d6dee5976atagen2026-10-10 13:23 UTC146
            Value::Object(fields) => {
ef7d6dee5976atagen2026-10-10 13:23 UTC147
                for (key, field) in fields.iter_mut() {
ef7d6dee5976atagen2026-10-10 13:23 UTC148
                    if !matches!(key.as_str(), "href" | "type" | "instance") {
ef7d6dee5976atagen2026-10-10 13:23 UTC149
                        self.localise_json(field);
ef7d6dee5976atagen2026-10-10 13:23 UTC150
                    }
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
#[cfg(test)]
ef7d6dee5976atagen2026-10-10 13:23 UTC159
mod tests {
ef7d6dee5976atagen2026-10-10 13:23 UTC160
    use super::*;
ef7d6dee5976atagen2026-10-10 13:23 UTC161
ef7d6dee5976atagen2026-10-10 13:23 UTC162
    #[test]
ef7d6dee5976atagen2026-10-10 13:23 UTC163
    fn negotiates_by_quality() {
ef7d6dee5976atagen2026-10-10 13:23 UTC164
        assert_eq!(Locale::negotiate(None), Locale::British);
ef7d6dee5976atagen2026-10-10 13:23 UTC165
        assert_eq!(Locale::negotiate(Some("en-US")), Locale::American);
ef7d6dee5976atagen2026-10-10 13:23 UTC166
        assert_eq!(
ef7d6dee5976atagen2026-10-10 13:23 UTC167
            Locale::negotiate(Some("fr, en-AU;q=0.8, en-US;q=0.5")),
ef7d6dee5976atagen2026-10-10 13:23 UTC168
            Locale::Australian
ef7d6dee5976atagen2026-10-10 13:23 UTC169
        );
ef7d6dee5976atagen2026-10-10 13:23 UTC170
        assert_eq!(Locale::negotiate(Some("de")), Locale::British);
ef7d6dee5976atagen2026-10-10 13:23 UTC171
    }
ef7d6dee5976atagen2026-10-10 13:23 UTC172
ef7d6dee5976atagen2026-10-10 13:23 UTC173
    #[test]
ef7d6dee5976atagen2026-10-10 13:23 UTC174
    fn rewrites_whole_words_only() {
ef7d6dee5976atagen2026-10-10 13:23 UTC175
        let us = Locale::American;
ef7d6dee5976atagen2026-10-10 13:23 UTC176
        assert_eq!(
ef7d6dee5976atagen2026-10-10 13:23 UTC177
            us.localise("Mum's zip, not gzip."),
ef7d6dee5976atagen2026-10-10 13:23 UTC178
            "Mom's zipper, not gzip."
ef7d6dee5976atagen2026-10-10 13:23 UTC179
        );
ef7d6dee5976atagen2026-10-10 13:23 UTC180
        assert_eq!(us.localise("1.1 mum"), "1.1 mom");
ef7d6dee5976atagen2026-10-10 13:23 UTC181
        assert_eq!(
ef7d6dee5976atagen2026-10-10 13:23 UTC182
            Locale::Australian.localise("The request conflicts with the current state of the zip."),
ef7d6dee5976atagen2026-10-10 13:23 UTC183
            "The request conflicts with the current state of the fly."
ef7d6dee5976atagen2026-10-10 13:23 UTC184
        );
ef7d6dee5976atagen2026-10-10 13:23 UTC185
    }
ef7d6dee5976atagen2026-10-10 13:23 UTC186
ef7d6dee5976atagen2026-10-10 13:23 UTC187
    #[test]
ef7d6dee5976atagen2026-10-10 13:23 UTC188
    fn excuses_come_back_to_en_gb() {
ef7d6dee5976atagen2026-10-10 13:23 UTC189
        assert_eq!(
ef7d6dee5976atagen2026-10-10 13:23 UTC190
            Locale::American.canonicalise("Digest username=\"carving the turkey\""),
ef7d6dee5976atagen2026-10-10 13:23 UTC191
            "Digest username=\"wrapping presents\""
ef7d6dee5976atagen2026-10-10 13:23 UTC192
        );
ef7d6dee5976atagen2026-10-10 13:23 UTC193
    }
ef7d6dee5976atagen2026-10-10 13:23 UTC194
}