Global enum as a i18n indicator

Currently, I'm trying deserilize item id (stored as u16) as either Chinese or English names of the item, into json files, and have no idea how to switch the language since #[serde::Deserialize] only accept one custom deserilizing function without any configuration.

AFAIK, a global enumerator might help, but what the exact implementation that enumerator should be?

First idea, static Arc<Mutex<T>>, which needs a lot of noise to read and modify the language.
Second idea, static AtomicU32, which seems more reasonable read and modify, but would set invalid value easily.
Third idea, private static AtomicU32 and a lot of impl that ensure only valid values could be assigned. the only problem is that, when matching the static variable, we must match _ part even such value is invalid and do not occur in the program at any chance.


Currently I'm using the third method

use std::sync::atomic::{AtomicU32, Ordering};
#[derive(PartialEq,Eq,Copy,Clone)]
pub struct Lang {_lang:u32}

static _current_lang:AtomicU32=AtomicU32::new(0);
impl Lang {
    const fn _new(_lang:u32)->Self{
        // must be private to disable illegal usages
        Self {_lang}
    }
    pub const EMPTY:Self=Self::_new(0);
    pub const ZH:Self=Self::_new(1);
    pub const EN:Self=Self::_new(2);
    pub fn switch(&self){
        _current_lang.store(self._lang,Ordering::SeqCst)
    }
    pub fn current()->Self{
        Self::_new(_current_lang.load(Ordering::SeqCst))
    }
}
fn status(a:Lang){
    match a {
        Lang::EMPTY=>println!("empty"),
        Lang::ZH=>println!("Chinese"),
        Lang::EN=>println!("English"),
        _=>()
    }
}
fn main(){
    status(Lang::current());
    Lang::ZH.switch();
    status(Lang::current());
}

Is it the best choice?
Or should I open a pre-rfc about global atomic enums in IRLO?

First, and I'm putting this behind a click because you might already know it, the standard spiel about why locales should not be a global flag set for the entire application.

Rust intentionally avoided having a global locale in the standard library, even though POSIX C has one, because there are too many use cases where it isn't helpful. Consider a web server with people who natively speak different languages accessing it at the same time: the locale should be scoped to the request, not the whole server. Consider someone opening a file with data inside it that isn't written in their preferred UI language: the UI should continue to use their preferred language, but the display of the file's contents should match the file. Consider the depressingly common case where someone runs their computer in English, even though it's not their native language, because the third-party help resources are better: they're going to mostly read and write text in their own language, which is different from the language they want the UI to use.

The other reason why Rust doesn't do this is that using localized string operations is a footgun when you're implementing text-based protocols. JSON always uses period / full stop . as a decimal separator, even if the data inside is localized with , comma as a decimal point. This is probably the reason why serde itself does nothing to support it; the syntax isn't normally going to be locale-dependent.

If your goal is just to work around serde, I would use a thread global instead of a true global, would design it to fail if you forget to set it, and would carefully scope it so that you can't accidentally leave it set.

For example, like this:

pub mod localized_context {
    use std::cell::Cell;
    thread_local! {
        static LANG: Cell<Option<Lang>> = Cell::new(None);
    }
    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    pub enum Lang {
        En,
        Zh,
    }
    /// Enter a localized context. This context is thread local; don't try to use it with a thread pool
    /// without calling `get_locale()` and passing it down to the workers.
    ///
    /// When inside this context, you can get the current locale by calling `localized_context::get_locale()`,
    /// which will panic if no locale is set.
    ///
    /// When the supplied closure returns (or panics), the context is automatically reset to whatever it
    /// was before you entered it.
    ///
    /// # Example
    ///
    /// ```rust
    /// use localized_context::*;
    /// let inside_locale = run_in_localized_context(Lang::Zh, || get_locale());
    /// assert_eq!(inside_locale, Lang::Zh);
    /// ```
    pub fn run_in_localized_context<T, F: FnOnce() -> T>(lang: Lang, f: F) -> T {
        struct ScopedLang(Lang);
        impl Drop for ScopedLang {
            fn drop(&mut self) { LANG.with(|cx| cx.set(self.0)); }
        }
        let _old_lang = LANG.with(|cx| { let prev = cx.get(); cx.set(lang); ScopedLang(prev) });
        f()
    }
    /// Get the current locale. Panics if there isn't one.
    pub fn get_locale() -> Lang {
        LANG.with(|cx| cx.get()).expect("do not call `get_locale` when lo locale is set")
    }
    /// Get the current locale. Panics if there isn't one.
    pub fn try_get_locale() -> Option<Lang> {
        LANG.with(|cx| cx.get())
    }
}

Then document, on your deserializer, that it needs to be called within a locale context.

4 Likes

Thank you for your great idea.
There is a small mistake in the example code, since LANG is Option but some of the set or get ops using Lang rather than Option<Lang> as its parameter.

Other things are fine, and such method works prefect with serde.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.