Static mayhem & global setting for app behavior

Hello,

I'm trying to utilize the example provided as a part of fluent-template crate for my app localization...

Example shows how one can define constant "language id's, and use them to translate message into specific language. So far, everything is clear and example makes sense.

Then, rust beginner (me) comes in, wants to add global static variable, to allow runtime-change-able target language selection for the translation of UI...

`std::cell::RefCell<lang::SupportedLanguage>` cannot be shared between threads safely
the trait `std::marker::Sync` is not implemented for `std::cell::RefCell<lang::SupportedLanguage>`
shared static variables must have a type that implements `Sync`rustc(E0277)

What threads?!? I mean, nowhere I even realize I could be doing stuff even remotely related to threading... I do not think i need (not sure if correctly, or i misunderstand some basic rust concepts) multiple ownership either, thus Arc/Rc seem not to be needed/related to my use-case?

The code, that is basically crate example, extended with my enum SupportedLanguages, and static "CURRENT_LANGUAGE" to keep track of user-selected language for translations...

use fluent_templates::{Loader, static_loader};
use unic_langid::{LanguageIdentifier, langid};
use std::cell::RefCell;

pub enum SupportedLanguage {
    EnglishUs,
    Czech,
    Russian,
}

static LI_ENGLISH: LanguageIdentifier = langid!("en-US");
static LI_CZECH: LanguageIdentifier = langid!("cz");
static LI_RUSSIAN: LanguageIdentifier = langid!("ru");

impl SupportedLanguage {
    fn as_li_ptr(&self) -> &'static LanguageIdentifier {
        match self {
            SupportedLanguage::EnglishUs => LI_ENGLISH.as_ref(),
            SupportedLanguage::Czech => LI_CZECH.as_ref(),
            SupportedLanguage::Russian => LI_RUSSIAN.as_ref(),
        }
    }
}

static_loader! {
    static LOCALES = {
        locales: "./locales",
        fallback_language: "en-US",
    };
}

static CURRENT_LANGUAGE: RefCell<SupportedLanguage> = RefCell::new(SupportedLanguage::EnglishUs);

pub fn set_current_lang(lang: SupportedLanguage) {
    CURRENT_LANGUAGE.replace(lang);
}

fn current_lang() -> SupportedLanguage {
    *CURRENT_LANGUAGE.borrow()
}

pub fn translate(message: &str) -> String {
    let lang_id = current_lang();
    let translated = &*LOCALES.lookup(&lang_id.as_li_ptr(), message);
    translated.to_string()
}

When I try to create function returning langid!() with parametrized arg., I fail again , because macros do not work C-like apparently, and return type of this langid!() macro changes from LanguageIdentifier (as in original example) to TokenStream for some dark magic reason... (my head explodes)

Why do i get threading related error with simple enum & refcell that is supposed to work with single-threaded apps?

Why does langid!() macro seem to have LanguageIdentifier return type when used for assignment to const variable, but different return type when used as return value of function?

Did i choose the wrong approach to a static variable to keep track of target language of running app, and/or can the langid!() macro be forced somehow to return/cast into proper LaguageIdentifier that i could use to translate to specific language at runtime?

thanks a lot for guidelines...

static variables need to be thread-safe because there’s no way for the compiler to enforce that they’re only accessible through a single thread. There’s a few ways to fix this:

  • Use a RwLock in place of a RefCell; it does the same job, but in a thread-safe way. There’s no const constructor for these yet, so you’ll also need lazy_static or OnceCell to initialize it (external crates).
  • Specify your static via the thread_local! macro, which means each thread gets its own copy and thus doesn’t need synchronization.
  • Instead of static, pass the requested language as an argument to anything that needs it (possibly within some other object).
1 Like

thank you, I will look into and try first and second option...

third one i sort of tried before, but failed due to the mentioned strange behavior of the langid! macro when i try to pass string into macro dynamically (via variable)

I dug into this a little bit, and the langid! macro is parsing the string literal at compile time to produce a constant LanguageIdentifier. Due to technical limitations of Rust macros, this sort of thing only works when the string to be parsed is physically within the macro call— it has no access to any external definition.

langid!’s runtime equivalent is something like this:

let li: LanguageIdentifier = "en-US".parse()
    .expect("Failed to parse.");
2 Likes

thank you again for directions Euler!
in the end, i moved LanguageIdentifier consts to lazy_static! HashMap, and used lazy_static RwLock instead of RefCell for the current language config setting

1 Like