docs: written docs

This commit is contained in:
zaaarf 2024-02-12 21:07:22 +01:00
parent ecfe19f7e8
commit a1433a32ee
No known key found for this signature in database
GPG key ID: 102E445F4C3F829B
4 changed files with 69 additions and 6 deletions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 zaaarf
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,2 +1,4 @@
# Fluent, fluently # Fluent, fluently
I found myself needing to do localisation, one time. I decided I'd want it to happen at runtime, so that any spelling fixes may be applied without needing to recompile the whole program. [Fluent](https://github.com/projectfluent/fluent-rs) provides an API, but no real way to use it. I couldn't find any existing implementation fitting my use case, so I wrote this. A small Rust library handling loading runtime loading of [Fluent](https://github.com/projectfluent/fluent-rs) localisation. By design, Fluent does not touch the IO part, only providing String parsing. This library takes care of that.
I intentionally kept this as simple as possible to reflect my very basic use case. Check out [fluent-localization](https://github.com/AEnterprise/fluent-localization) for something with more features, namely compile-time validation and localisation struct generation for easier access.

View file

@ -4,11 +4,14 @@ use fluent::FluentResource;
pub type Result<T> = StdResult<T, Error>; pub type Result<T> = StdResult<T, Error>;
/// Simple wrapper around the errors that may occur during the program's execution.
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
GenericError(String),
IoError(std::io::Error), IoError(std::io::Error),
LanguageIdentifierError(unic_langid::LanguageIdentifierError), LanguageIdentifierError(unic_langid::LanguageIdentifierError),
FluentError(Vec<fluent::FluentError>) FluentError(Vec<fluent::FluentError>),
MissingMessageError(String)
} }
impl From<std::io::Error> for Error { impl From<std::io::Error> for Error {

View file

@ -1,18 +1,29 @@
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use fluent::{bundle::FluentBundle, FluentResource}; use fluent::{bundle::FluentBundle, FluentResource, FluentMessage};
use intl_memoizer::concurrent::IntlLangMemoizer; use intl_memoizer::concurrent::IntlLangMemoizer;
use unic_langid::LanguageIdentifier; use unic_langid::LanguageIdentifier;
use crate::error::Result; use crate::error::Result;
pub mod error; pub mod error;
/// Shorthand type handling the [FluentBundle]'s generic types.
type TypedFluentBundle = FluentBundle<Arc<FluentResource>, IntlLangMemoizer>; type TypedFluentBundle = FluentBundle<Arc<FluentResource>, IntlLangMemoizer>;
/// The main struct of the program.
/// You can obtain a new instance by calling [Self::try_load()].
pub struct Localiser { pub struct Localiser {
pub bundles: HashMap<LanguageIdentifier, TypedFluentBundle>, pub bundles: HashMap<LanguageIdentifier, TypedFluentBundle>,
pub default_language: LanguageIdentifier pub default_language: LanguageIdentifier
} }
impl Localiser { impl Localiser {
/// Tries to create a new [Localiser] instance given a path and the name of the default language.
/// The path's direct children will only be considered if their names are valid language codes as
/// defined by [LanguageIdentifier], and if they are either files with the `.ftl` extension or
/// directories. In the first case they will be read directly and converted in [FluentResource]s,
/// in the second case the same will be done to their chilren instead.
/// [FluentResource]s within a same folder will be considered part of a same [FluentBundle],
/// forming a single localisation for all intents and purposes.
pub fn try_load(path: String, default_language: String) -> Result<Self> { pub fn try_load(path: String, default_language: String) -> Result<Self> {
let mut bundles = HashMap::new(); let mut bundles = HashMap::new();
let paths = std::fs::read_dir(path)? let paths = std::fs::read_dir(path)?
@ -26,7 +37,6 @@ impl Localiser {
} }
}).collect::<Vec<_>>(); }).collect::<Vec<_>>();
//TODO load default first and call bundle.add_resource_overriding(default_bundle) on others
let default_language = default_language.parse::<LanguageIdentifier>()?; let default_language = default_language.parse::<LanguageIdentifier>()?;
for path in paths { for path in paths {
@ -63,17 +73,44 @@ impl Localiser {
}) })
} }
/// Reads all files in a certain folder and all of its subfolders that have the `.ftl`
/// extension, parses them into [FluentResource]s and returns them in a [Vec].
fn path_to_resources(path: &std::path::PathBuf) -> Result<Vec<Arc<FluentResource>>> { fn path_to_resources(path: &std::path::PathBuf) -> Result<Vec<Arc<FluentResource>>> {
let mut res = Vec::new(); let mut res = Vec::new();
for entry in walkdir::WalkDir::new(path).follow_links(true).into_iter().filter_map(|e| e.ok()) { for entry in walkdir::WalkDir::new(path).follow_links(true).into_iter().filter_map(|e| e.ok()) {
let entry_path = entry.path().to_path_buf(); let entry_path = entry.path().to_path_buf();
let entry_extension = entry_path.extension();
if entry_extension.is_none() || entry_extension.unwrap() != "ftl" {
continue;
}
res.push(Self::file_to_resource(&entry_path)?); res.push(Self::file_to_resource(&entry_path)?);
} }
Ok(res) Ok(res)
} }
/// Reads the file at the given path, and tries to parse it into a [FluentResource].
fn file_to_resource(path: &std::path::PathBuf) -> Result<Arc<FluentResource>> { fn file_to_resource(path: &std::path::PathBuf) -> Result<Arc<FluentResource>> {
let content = std::fs::read_to_string(path)?; Ok(Arc::new(FluentResource::try_new(std::fs::read_to_string(path)?)?))
Ok(Arc::new(FluentResource::try_new(content)?)) }
/// Extracts a message from the requested bundle, or from the default one if absent.
pub fn get_message(&self, key: String, language: LanguageIdentifier) -> Result<FluentMessage> {
let bundle = self.bundles.get(&language)
.or_else(|| self.bundles.get(&self.default_language))
.ok_or(error::Error::GenericError("Failed to get default bundle! This is not supposed to happen!".to_string()))?;
bundle.get_message(&key)
.ok_or(error::Error::MissingMessageError(format!("No such message {} for language {}!", key, language)))
}
/// Returns a [HashMap] tying each [LanguageIdentifier] to its [String] equivalent, to simplify retrieval.
/// Call this as little as possible, as it's rather unoptimised and may scale poorly.
pub fn available_languages(&self) -> HashMap<String, LanguageIdentifier> {
let mut res = HashMap::new();
for lang in self.bundles.keys() {
res.insert(lang.to_string(), lang.clone());
}
res
} }
} }