Skip to content

Latest commit

 

History

History
271 lines (223 loc) · 7.65 KB

README.md

File metadata and controls

271 lines (223 loc) · 7.65 KB

Interly

Internalization in Rust

Usage

# locales/en.ftl
hello = Hello, { name }!
// main.rs
use fluent_i18n::localize;

#[localize]
pub(crate) struct Localize;

fn main() {
    println!("{}", tr!(hello, "en", "world")); // Hello, world!
}

// other/module.rs
use crate::tr;

fn your_function() {
    println!("{}", tr!(hello, "en", "world"));
}

Notes and current limitations

  • Default folder is locales.
  • - in filenames are converted to _, so hello-world and hello_world would be considered equivalent, and it would be an error.
  • Variables types not detected (I don't know how), only strings.
  • Only supported files structure is
locales
├── en.ftl
└── *.ftl
  • Languages accepted in form "en", "en_US", etc. Case insensitive, _ required.
  • Languages will always fallback to global fallback. For example, if you have languages ["en", "ru"] and call tr!(name, "ru_RU"), it will fallback to "en".
  • Macros should be called at crate top.

Roadmap

  • Default generation with simple .ftl files.
  • Support selectors (docs.rs)
  • Support attributes.
  • Support terms as static methods.
  • Macros parameters:
    • path - path to folder with localizations.
    • resolver - how files are stored:
      • files - {path}/*.ftl (current behaviour).
      • folder - {path}/{locale}/*.ftl.
    • set_locale - how to specify current locale:
      • init - set locale on startup.
      • state - store locale as state.
      • call - specify locale on each function call (current behaviour).
    • fallback - global fallback locale.
    • sources - how to load locales sources. Probably this could be solved by providing macro for embedding, and regular struct for manual initialization.
      • embed - embed sources to binary (current behaviour).
      • load - load sources at startup from file system.
    • errors - how to handle errors (probably not required):
      • ignore
      • log
      • panic (current behaviour)
  • Fallback with respect to region (e.g. "ru_RU" -> "ru").
    • Probably calculate fallbacks on compile time?
  • Support defining not at crate top (now just not tested, probably this already works).
    • Probably this just required generating correct #vis identifier inside tr!().
  • More translation formats support (long-term).

Q&A

Interly doesn't fit your use-case, but do you want to use this library?

Open an issue, probably interly wants this too!

What's generated

For source files

# locales/en.ftl
hello-world = Hello, { $name }!
# locales/ru.ftl
hello-world = Привет, { $name }!
use interly::localize;

#[localize]
pub(crate) struct Localize;

fn main() {
    println!("{}", tr!(hello_world, "en", "world"));
    println!("{}", tr!(hello_world, "ru", "мир"));
    println!("{}", tr!(hello_world, "ru-RU", "мир"));
}

Generated (unrelated parts removed):

// main.rs
pub(crate) struct Localize {
    bundles: __interly::Bundles,
}

pub(crate) mod __interly {
    use ::std::collections::HashMap;
    use ::std::sync::Arc;
    use ::interly::{
        FluentArgs,
        FluentBundle,
        FluentResource,
        IntlLangMemoizer,
        LanguageIdentifier,
        Lazy,
    };

    use super::Localize;

    pub(super) type Bundles = HashMap<
        LANG,
        FluentBundle<Arc<FluentResource>, IntlLangMemoizer>,
    >;

    impl Localize {
        const FALLBACK_LANG: LANG = LANG::EN;

        pub(crate) fn init() -> Self {
            use ::interly::unic_langid::langid;

            let mut resources: HashMap<LanguageIdentifier, Arc<FluentResource>> = HashMap::new();
            let mut locales = vec![];

            let lang = langid!("en");
            locales.push((lang.clone(), "en"));
            resources
                .insert(
                    lang,
                    Arc::new(
                        FluentResource::try_new("hello-world = Hello, { $name }!\n".to_string())
                            .expect("invalid ftl"),
                    ),
                );

            let lang = langid!("ru");
            locales.push((lang.clone(), "ru"));
            resources
                .insert(
                    lang,
                    Arc::new(
                        FluentResource::try_new("hello-world = Привет, { $name }!\n".to_string())
                            .expect("invalid ftl"),
                    ),
                );

            let mut bundles = HashMap::new();
            for lang in locales {
                let mut bundle = FluentBundle::new_concurrent(vec![lang.0.clone()]);
                let _ = bundle.add_resource(resources.get(&lang.0).unwrap().clone());
                bundles.insert(lang.1.into(), bundle);
            }

            Self { bundles }
        }

        pub(crate) fn languages() -> Vec<&'static str> {
            vec!["ru", "en"]
        }

        pub(crate) fn __format_msg(
            &self,
            msg_id: &'static str,
            lang: LANG,
            args: Option<&FluentArgs<'_>>,
        ) -> String {
            let lang = lang.into();
            let mut bundle = self.bundles.get(&lang).expect("no bundle");
            if !bundle.has_message(msg_id) {
                bundle = self
                    .bundles
                    .get(&Self::FALLBACK_LANG)
                    .expect("no fallback bundle");
            }
            let msg = bundle
                .get_message(msg_id)
                .expect("no message")
                .value()
                .expect("no value in message");
            let mut errs = vec![];
            bundle.format_pattern(msg, args, &mut errs).to_string()
        }

        pub(crate) fn hello_world(&self, lang: impl Into<LANG>, name: &str) -> String {
            self.__format_msg(
                "hello-world",
                lang.into(),
                Some(&FluentArgs::from_iter(vec![("name", name)])),
            )
        }
    }

    pub(crate) static LOCALIZE: Lazy<Localize> = Lazy::new(|| { Localize::init() });

    #[derive(PartialEq, Eq, Hash)]
    pub(crate) enum LANG {
        EN,
        RU,
    }

    impl From<&str> for LANG {
        fn from(lang: &str) -> Self {
            match lang.to_lowercase().as_str() {
                "en" => Self::EN,
                "ru" => Self::RU,
                _ => Self::EN,
            }
        }
    }

    impl From<&::std::string::String> for LANG {
        fn from(lang: &::std::string::String) -> Self {
            lang.as_str().into()
        }
    }
}

#[allow(unused)]
macro_rules! tr {
    ($e:ident, $lang:expr) => {
        tr!($e, $lang,)
    };
    ($e:ident, $lang:expr, $($v:expr),*) => {
        $crate::__interly::LOCALIZE.$e($lang, $($v),*)
    };
}

fn main() {
    println!("{}", crate::__interly::LOCALIZE.hello_world("en", "world"));
    println!("{}", crate::__interly::LOCALIZE.hello_world("ru", "мир"));
    println!("{}", crate::__interly::LOCALIZE.hello_world("ru-RU", "мир");
}

Output:

Hello, world!
Привет, мир!
Hello, мир!