interly-macros 0.1.0

Internalization unified
Documentation
# Interly

Internalization in Rust

## Usage

```fluent
# locales/en.ftl
hello = Hello, { name }!
```

```rs
// 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
```sh
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

- [x] Default generation with simple .ftl files.
- [ ] Support [selectors] [docs.rs][expression_select]
- [ ] Support [attributes][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][unic_langid_LanguageIdentifier] (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][tr-formats-list] support (long-term).

[selectors]: https://projectfluent.org/fluent/guide/selectors.html
[expression_select]: https://docs.rs/fluent-syntax/latest/fluent_syntax/ast/enum.Expression.html#variant.Select
[attributes]: https://projectfluent.org/fluent/guide/attributes.html
[terms]: https://projectfluent.org/fluent/guide/terms.html
[unic_langid_LanguageIdentifier]: https://docs.rs/unic-langid/latest/unic_langid/struct.LanguageIdentifier.html
[tr-formats-list]: https://docs.weblate.org/en/latest/formats.html

## 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

<details>

For source files

```fluent
# locales/en.ftl
hello-world = Hello, { $name }!
```

```fluent
# locales/ru.ftl
hello-world = Привет, { $name }!
```

```rs
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):

```rs
// 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, мир!
```

</details>