#internationalization #i18n #simple-i18n


Simple I18N implementation with the ability to update localizations in real time

3 releases

Uses new Rust 2021

0.1.2 Nov 7, 2021
0.1.1 Nov 6, 2021
0.1.0 Oct 31, 2021

#35 in Internationalization (i18n)

Download history 16/week @ 2021-10-28 30/week @ 2021-11-04 5/week @ 2021-11-11 5/week @ 2021-11-18

56 downloads per month

MIT license

555 lines

Simple i18n

ci docs.rs crates
Simple implementation to load locale.



Basic usage assumes that you are not using any features in the project.

sorrow-i18n = "0.1.0"



A simple feature for loading static files into a project.



Added macros for static kernel initialization init! and init_dir! if you use the incl_dir feature. After initialization, use the macro at any point: i18n!




Base usage with static files

Firstly add dependency sorrow-i18n in your project. A typical view of the structure of a file is FileStructure. Data scheme - scheme/locale-scheme.json. And so, we will create the actual minimal file for work:

kind: I18N
locale: EN
description: test en
  name: "Test"

And the second localization:

kind: I18N
locale: RU
description: test ru
  name: "Тест"

Now it's time for the codebase, the files we created earlier will be placed, for example, in locale/.

use sorrow_i18n::{GetData, InternationalCore};
let core = InternationalCore::new("locale/");

Having created the core, we can get our localizations and work with them.

let eu = core.get_by_locale("EN")?;
let ru = core.get_by_locale("RU")?;


let eu = core.get_by_locale("EN")?;
let ru = core.get_by_locale("RU")?;

What are the differences between these methods? First, when the get_by_locale method is called, a reference to the mutable data is returned, and in the case of get_by_locale, you just get a reference to the data (in both cases, you are working with a wrapper) that cannot change from outside. Plus the first approach, if your localization can change during the execution of the program, then you will receive up-to-date data, but there are additional costs for blocking. Plus the second approach, if your files are static, loaded into the project, and they cannot be changed, in this case, you just work with HashMap.
But how can we track or choose a file tracking strategy? And how are we going to get those updates? The provider will help us with this! By default, we provide you with several of these, namely:

  • StaticFileProvider - static file. It is not being watched. Default option if the provider is not specified in the file structure
  • FileProvider - dynamically watcher for file. If the provider is not specified, the default is StaticFileProvider. The question remains, how to choose this provider? Simple enough, here's an example where the provider is explicitly specified:
kind: I18N
locale: RU
description: test ru
provider: FileProvider
  name: "Тест"


kind: I18N
locale: RU
description: test ru
provider: StaticFileProvider
  name: "Тест"

You can read more about providers below.
Finally, we got our locales, it remains to get what we want! Namely: data.name.

 assert_eq!("Test", eu.get("data.name")?);
 assert_eq!("Тест", ru.get("data.name")?);

As you can see, everything is quite simple, there is also a similar method for getting the value by default (this will be the same key that you requested)

 assert_eq!("Test", eu.get_or_default("data.name"));
 assert_eq!("Тест", ru.get_or_default("data.name"));
 assert_eq!("keykey", eu.get_or_default("keykey"));

You can see more examples in examples/*


As we said earlier, the provider is responsible for the data update strategy. Its main method is watch.

pub trait WatchProvider {
    /// The main observer method that is called to observe the state.
    fn watch(&mut self) -> Result<(), Error>;

    /// Setter for data reference.
    fn set_data(&mut self, data: Arc<RwLock<HashMap<String, String>>>) -> Result<(), Error>;


It is he who observes the change in data. In the case of a static observer, we have an empty method because we don't need to observe.

impl WatchProvider for StaticFileProvider {
    fn watch(&mut self) -> Result<(), Error> {

    fn set_data(&mut self, _data: Arc<RwLock<HashMap<String, String>>>) -> Result<(), Error> {


But with FileProvider, everything is not much more complicated. The notify library is used to constantly monitor the state of the file. The only event tracked is the file change.

let event = result.map_err(|e| Error::WatchError { message: e.to_string() }).unwrap();
if event.kind.is_modify() {

Every time we change the file, we get a lock on our data, but first we load the updated file itself (to validate the structure). Actually poisoning the blockage in this way is very, very difficult.

// Validation file
let structure = load_struct(&path.clone()).unwrap();

// Lock data and clear
let mut w_holder = holder.write().unwrap();

// Clone internal state.
let l_holder = structure.messages.write().unwrap().clone();

Custom provider

There are situations when it is necessary, for example, to load project locales first, and later maintain a connection to a database or some other data source, to constantly update the data itself. For this we can create our own data provider! The simplest example and illustrative example is in examples/custom_provider.rs
Well, now, point by point, to begin with, let's create a simple structure that will monitor our data.

pub struct CustomProvider {
    data: Arc<RwLock<HashMap<String, String>>>,

And we will implement our provider for it:

impl WatchProvider for CustomProvider {
    fn watch(&mut self) -> Result<(), sorrow_i18n::Error> {
        println!("Accepted custom provider");
        let data = self.data.write();
        let mut un = data.unwrap();
        // Print all current data
        un.iter().for_each(|kv| {
            println!("Key: {}, Value: {}", kv.0, kv.1);
        // Add new key
        un.insert("Hello".to_string(), "World".to_string());
        // Print all data, current data has been contains key "Hello"
        un.iter().for_each(|kv| {
            println!("Key: {}, Value: {}", kv.0, kv.1);

    fn set_data(&mut self, data: Arc<RwLock<HashMap<String, String>>>) -> Result<(), Error> {
        self.data = data;
        println!("Data has been set");

There is a minimum left, to add our data provider to any locale.

    let mut core = InternationalCore::new("locale/");
    core.add_provider("EN", Box::new(CustomProvider::new()))?;

If such a locale exists, the following actions will be performed:

  • holder -> getting current data
  • provider -> set_data(current_data_in_holder)
  • provider -> watch()

Macro usage

Add dependencies

sorrow-i18n = { version = "0.1.0", features = ["macro"] }


There are two methods used for initialization (depending on whether you include incl_dir depending.)

  • init_i18n! - A macro that allows you to initialize the i18n core. (InternationalCore)
    • Example for usage: init_i18n!("my_locale_folder");
  • init_i18n_static_dir! - The same thing, only for the feature incl_dir
    • Example for usage: const PROJECT_DIR: Dir = include_dir!("resources/en_ru"); init_i18n_static_dir!(PROJECT_DIR);


The main macro for interaction will be i18n!. The first argument is the locale, the second argument is the key. If the locale is not found or the key is not found, the key itself is returned instead of the value.

Usage example:

    let manifest = format!("{}{}", env!("CARGO_MANIFEST_DIR"), "/resources/en_ru");
    // Init i18n core
    // Getting data.name key by ru locale
    let test = i18n!("RU", "data.name");
    println!("test: {}", &*test);
    assert_eq!("Тест", &*test);

Case where locale or key not found:

    let not_found_data = i18n!("RANDOM_LOCALE_NAME", "data.not_found_me");
    println!("data not found: {}", &*not_found_data);
    assert_eq!("data.not_found_me", &*not_found_data);

Example with custom data provider

Incl_dir usage

Add dependencies

sorrow-i18n = { version = "0.1.0", features = ["incl_dir"] }

Initial and usage

const PROJECT_DIR: Dir = include_dir!("resources/en_ru");
let core = InternationalCore::from(PROJECT_DIR);


~91K SLoC