How can I distribute localized outputs on crates.io

I am considering using fluent- rs for localization, however I am unsure how to include the bundle with my published crate without complex steps like downloading or separately.
Additionally, if possible how can I optimize the the bundle so that if only a few out of all the languages are 'used' (aka it's stated as used in some external file or something like that), the library only includes the translations for those specific languages and excludes the rest of the bubble bundles. How can I achieve this?

You could use include_str! to bundle the .ftl files in your binary (there are crates that take a whole directory, too). With that in place, you can also use #[cfg(...)] attributes to include and exclude individual files during compile time. You could, for example, have Cargo features for each language and use #[cfg(feature = "some_lang")] to include the file for it.

The crates.io registry isn't really a place for distributing assets, so depending on your needs you'll probably want to have a "proper" installer rather than telling people to cargo install my-package.

That said, Cargo.toml has fields for including and excluding files from being uploaded to crates.io, but there is no way to modify that list as an end user (e.g. based on feature flags). This means if you follow @ogeon's suggestion of feature flags, your crate (as in the files published to crates.io and downloaded to ~/.cargo/registry/src/index.crates.io-xxx by cargo) will still include the translation files, but the compiled crate (i.e. the final *.rlib or executable) will only include the translations you want.

That might be just fine for your use case, though.

2 Likes

I agree, my suggestion is probably fine for a smaller amount of text. Like if it's the kind of text content you would otherwise have hard coded, then you would have distributed it through the same source code regardless, but at least this lets you keep it in dedicated files. I guessed this would be the case, since it's a library.

Maybe we can give better recommendations if we would know more about what it will be used for.

1 Like

I believe maybe using a combination of the above mentioned techniques can be used for me.
I can include translations for each locale for each part of my translations as a translations are done via one function only and not used any where else. EG:

fn etwas() {
    // Create a FluentBundle instance for translations
    let bundle = FluentBundle...//
    
    // Include locales for each translation used by the function
    // For example:
    // bundle.add_messages(&[
    //     ("en", "key1" => "Translation for key1 in English"),
    //     ("es", "key1" => "Traducción para key1 en español"),
    //     ...
    // ]);

    // Call translation in a specific language using bundle.format("key1", None)
}

And to answer the question of my use case it is for something like below (uses only English rn) for my library arkley_describe (not published yet). I have to update description in the specified Language eg English or Hindi etc

if c_sum == 0 {
            description += &format!("\nSince sum is 0 we can skip this column");
            continue;
        }

        if previous_carry != 0 {
            description += &format!("{previous_carry} = {c_sum}\n{previous_carry} came from the previous carry forward");
        }

        else if c_sum >= 10 {
            description += &format!("\nSince sum is greater than 10 we carry 1 forward");
        }

Thanks for the example. I think you will have to decide based on the volume of text and the number of languages you want to support. You can always try and see how large the files become.

Another consideration, do you want your users to be able to add more translations of their own? If so, you would either still need dynamic loading or make it possible for them to populate the bundle. At that point, you may want to consider only bundling one or more default languages, and have the rest as separate resources.

Based on my goals the volumes of text is quite low, it's mostly just repeated text with changes of numbers and the number of languages to support rn it's only the languages I can proof read so maybe like 5 but in the future I might add more
Also I don't want users to add custom translations.
And I guess dynamic loading is not a necessary but it would be a nice feature to add in the future.
I believe I have the answer too my OG question, now I just need to figure out how to use fluent and output string in a specified language :pensive::sweat_smile:

Sounds like a reasonable case to me. :slightly_smiling_face: I have been using fluent a bit, myself, and I can say that it's not particularly complicated and really quite nice. I prefer it over gettext style systems. The Rust implementation is just a bit bare-bones compared to the JavaScript version. You may have to add your own functions and maybe customize the number formatting, depending on your needs.

The only experience with localisation I have is using the garbage android string.xml system w/o compose, so I rly don't have an preferences. But I think I'll go with fluent for now , if you can ,could you give me a bare bones example for my use case , how to use get_message function to get it on target language as I am unable to find any tutorials or guides for this

Sure! The way I did it is probably way more complicated than what you need, so I have tried to strip away anything extra. Think twice before pulling in ICU, for example, since that's a whole different (although useful) can of worms. In essence, I would recommend setting up a wrapper struct that holds the bundle, or possibly some free functions that does the same thing. I went for the wrapper, myself.

This code is untested and may be missing parts. Also, keep in mind that I'm simplifying a lot of error handling here. You can choose to either propagate the errors or do something else.

pub struct Localize {
    bundle: FluentBundle<FluentResource>,
}

impl Localize {
    /// Initializes the bundle for a specific locale.
    /// The bundle creation and resource loading can be separated.
    pub fn new(locale: &str) -> Self {
        // Maybe fall back to English?
        let lang_id = locale.parse().expect("locale must be valid");
        let mut bundle = FluentBundle::new(vec![lang_id]);

        // Set up additional fluent functions somewhere here, if needed.

        // This is where you would select the right include_str! resource:
        for file_content in /*files for this locale*/ {
            let resource = FluentResource::try_new(file_content)
                .expect("file content should be valid");
            bundle.add_resource(resource)
                .expect("should be able to add resource");
        }

        Self { bundle }
    }

    /// Translates a key and a set of arguments. You may want to change this for your use case,
    /// but it's nice to get the boiler plate out of the way with a function like this.
    pub fn translate(&self, key: &str, args: FluentArgs) -> String {
        let Some(message) = self.bundle.get_message(&key) else {
            panic!("\"{key}\" is not a registered localization key");
        };

        let Some(pattern) = message.value() else {
            panic!("localization key \"{key}\" doesn't have a value");
        };

        let mut errors = Vec::new();

        let text = self
            .bundle
            .format_pattern(pattern, Some(&args), &mut errors);

        if !errors.is_empty() {
            // Find some nice way of showing the errors.
        }

        text.into()
    }
}

As mentioned, you may want to change the translate function a lot. It's really just the minimum requirements and doesn't care about avoiding copies (format_pattern returns a Cow<'_, str> that's trapped inside the function), but it wasn't something that I needed to do in my case. I didn't let my own version of it panic, so translate would return the key if it didn't succeed, but that's also up to you.

Thanks a lot of for up to date example , I thought there was a way to include multiple locales into one bundle and then some how get the target language but by the looks of it I should open up the bundle for the target language and get the string.
And I should be using a predefined bundle right instead of manually creating one each time open it (since I have many languages)

No problem! I think the locale list is only for fallbacks when formatting values. So one bundle is one language, if I understand it correctly, but you can of course have more than one bundle. I only needed one and replaced it if the language would change. There's also a number of helper crates that you may want to look into as well: https://crates.io/search?q=fluent

And I should be using a predefined bundle right instead of manually creating one each time open it (since I have many languages)

You create the ones you will need during some initialization phase and re-use them throughout the program. So in the example above, you can pass Localize around as an argument, store it in an Rc or Arc, or put it in some kind of globally accessible place.

I found this, by the way. It sounds a like what you are trying to do. https://crates.io/crates/i18n-embed

Thanks for your help , I too found something like what I need.
Now I will try both and see which one suits me better.
Thanks for your help!

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.