babel47 0.1.0

Multi-Language (BCP-47) Strings
Documentation
//! # Multi-Language (BCP-47) Strings
//!
//! This crate helps with handling translatable strings using
//! [BCP 47](https://www.rfc-editor.org/rfc/bcp/bcp47.txt) language tags.

use language_tags::LanguageTag;
use std::collections::HashMap;

#[cfg(feature = "taganak")]
use futures_util::stream::TryStreamExt;
#[cfg(feature = "taganak")]
use taganak_core::prelude::{GraphError, GraphView, Term};

/// A single translatable string
#[derive(Debug, Default, Clone)]
pub struct LangString {
    langs: HashMap<LanguageTag, String>,
    default: Option<String>,
}

impl LangString {
    /// Set the string for one language
    pub fn set(&mut self, language: LanguageTag, string: String) {
        self.langs.insert(language, string);
    }

    /// Set the default (fallback) string
    pub fn set_default(&mut self, string: String) {
        let _ = self.default.insert(string);
    }

    /// Unset the string for one language
    pub fn unset(&mut self, language: &LanguageTag) {
        self.langs.remove(language);
    }

    /// Unset the default (fallback) string
    pub fn unset_default(&mut self) {
        self.default.take();
    }

    /// Retrieve the string for a language or language range
    ///
    /// This method resolves the requested language into a string by:
    ///
    /// 1. Trying to look up the exact language tag
    /// 2. Trying to find a matching language range
    /// 3. Falling back to the default string
    /// 4. Returning [None] on failure
    ///
    /// # Examples
    ///
    /// ```
    /// use babel47::LangString;
    ///
    /// let mut ls = LangString::default();
    /// ls.set("de".parse().unwrap(), "Schildkröte".to_string());
    /// ls.set("de-CH".parse().unwrap(), "Schildchrot".to_string());
    /// ls.set("fr".parse().unwrap(), "tortoise".to_string());
    ///
    /// assert_eq!(ls.get(&"de-DE".parse().unwrap()), Some("Schildkröte"));
    /// assert_eq!(ls.get(&"de-AT".parse().unwrap()), Some("Schildkröte"));
    /// assert_eq!(ls.get(&"de-CH".parse().unwrap()), Some("Schildchrot"));
    /// assert_eq!(ls.get(&"fr-FR".parse().unwrap()), Some("tortoise"));
    /// assert_eq!(ls.get(&"es".parse().unwrap()), None);
    ///
    /// ls.set_default("Testudinata".to_string());
    /// assert_eq!(ls.get(&"es".parse().unwrap()), Some("Testudinata"));
    /// ```
    pub fn get(&self, language: &LanguageTag) -> Option<&str> {
        if let Some(string) = self.langs.get(language) {
            return Some(string);
        }

        for known_lang in self.langs.keys() {
            if !known_lang.is_language_range() {
                continue;
            }

            if known_lang.matches(language) {
                return self.langs.get(known_lang).map(|s| s.as_str());
            }
        }

        self.default.as_ref().map(|s| s.as_str())
    }

    #[cfg(feature = "taganak")]
    /// Load from an RDF graph using [taganak_core]
    ///
    /// # Examples
    ///
    /// ```
    /// use babel47::LangString;
    /// use taganak_core::prelude::Graph;
    /// use taganak_framework::lazy_graph;
    ///
    /// lazy_graph!(TEST_GRAPH, r#"
    /// @prefix eg: <http://example.com/> .
    ///
    /// eg:turtle
    ///   eg:name
    ///     "Schildkröte"@de, "Schildchrot"@de-CH, "tortoise"@fr,
    ///     "Testudinata" .
    /// "#, Some("http://example.com/"));
    ///
    /// # tokio_test::block_on(async {
    /// let ls = LangString::from_graph(
    ///     (*TEST_GRAPH).view().await,
    ///     &"<http://example.com/turtle>".try_into().unwrap(),
    ///     &"<http://example.com/name>".try_into().unwrap()
    /// ).await.unwrap();
    ///
    /// assert_eq!(ls.get(&"de-DE".parse().unwrap()), Some("Schildkröte"));
    /// assert_eq!(ls.get(&"de-AT".parse().unwrap()), Some("Schildkröte"));
    /// assert_eq!(ls.get(&"de-CH".parse().unwrap()), Some("Schildchrot"));
    /// assert_eq!(ls.get(&"fr-FR".parse().unwrap()), Some("tortoise"));
    /// assert_eq!(ls.get(&"es".parse().unwrap()), Some("Testudinata"));
    /// # });
    /// ```
    pub async fn from_graph<G>(
        graph: G,
        subject: &Term,
        predicate: &Term,
    ) -> Result<Self, GraphError>
    where
        G: GraphView + Clone,
    {
        use std::sync::Arc;

        let mut lang_string = Self::default();

        static LIMIT: usize = 256;
        let mut stream = graph
            .objects(Some(subject), Some(predicate), Some(LIMIT))
            .await?;

        while let Some(object) = stream.try_next().await? {
            let object: Arc<Term> = object;

            if !object.is_literal() {
                continue;
            }
            let literal = object.to_literal().expect("we just checked");

            match literal.datatype() {
                "http://www.w3.org/2001/XMLSchema#string" => {
                    lang_string.set_default(literal.lexical().to_string())
                }
                "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString"
                | "http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString" => lang_string.set(
                    literal.language().expect("we just checked").clone(),
                    literal.lexical().to_string(),
                ),
                _ => continue,
            }
        }

        Ok(lang_string)
    }
}