use crate::common::errors::CardError;
use crate::prelude::Pip;
use fluent_templates::{LanguageIdentifier, Loader, langid, static_loader};
use std::fmt::Display;
use std::str::FromStr;
use std::string::ToString;
static_loader! {
pub static LOCALES = {
locales: "./src/localization/locales",
fallback_language: "en-US",
core_locales: "./src/localization/locales/core.ftl",
};
}
pub trait Named<'a> {
const US_ENGLISH: LanguageIdentifier = langid!("en-US");
const DEUTSCH: LanguageIdentifier = langid!("de");
const FLUENT_INDEX_SECTION: &'a str = "index";
const FLUENT_LONG_SECTION: &'a str = "long";
const FLUENT_SYMBOL_SECTION: &'a str = "symbol";
const FLUENT_WEIGHT_SECTION: &'a str = "weight";
const FLUENT_PRIME_SECTION: &'a str = "prime";
fn new_with_weight(name_str: &str, weight: u32) -> Self;
#[must_use]
fn weighted_vector(names: &[&'static str]) -> Vec<Self>
where
Self: Sized,
{
let mut weight = u32::try_from(names.len()).unwrap_or(0);
names
.iter()
.map(|name| {
weight -= 1;
Self::new_with_weight(name, weight)
})
.collect()
}
fn fluent_name(&self) -> &FluentName;
fn fluent_name_string(&self) -> &String;
fn is_blank(&self) -> bool;
fn fluent_value(&self, key_section: &str, lid: &LanguageIdentifier) -> String {
let id = format!("{}-{}", self.fluent_name_string(), key_section);
LOCALES.lookup(lid, id.as_str())
}
fn index(&self, lid: &LanguageIdentifier) -> String {
self.fluent_value(Self::FLUENT_INDEX_SECTION, lid)
}
fn index_char(&self, lid: &LanguageIdentifier) -> char {
self.index(lid).chars().next().unwrap_or(Pip::BLANK_INDEX)
}
fn index_default(&self) -> String {
self.index(&Self::US_ENGLISH)
}
fn long(&self, lid: &LanguageIdentifier) -> String {
self.fluent_value(Self::FLUENT_LONG_SECTION, lid)
}
fn long_default(&self) -> String {
self.long(&Self::US_ENGLISH)
}
fn weight(&self) -> u32 {
let weight = self.fluent_value(Self::FLUENT_WEIGHT_SECTION, &Self::US_ENGLISH);
weight.parse().unwrap_or(0)
}
fn prime(&self) -> u32 {
let prime = self.fluent_value(Self::FLUENT_PRIME_SECTION, &Self::US_ENGLISH);
prime.parse().unwrap_or(0)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct FluentName(String);
impl FluentName {
pub const BLANK: &'static str = "blank";
#[must_use]
pub fn new(name_str: &str) -> Self {
if Self::is_alphanumeric_hyphen_dash(name_str) {
Self(name_str.to_string())
} else {
log::warn!("Invalid name: {name_str} - Defaulting to 'blank'.");
Self(Self::BLANK.to_string())
}
}
fn is_alphanumeric_hyphen_dash(s: &str) -> bool {
s.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '–' || c == '—')
}
}
impl Default for FluentName {
fn default() -> Self {
Self(Self::BLANK.to_string())
}
}
impl Display for FluentName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.clone())
}
}
impl FromStr for FluentName {
type Err = CardError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if Self::is_alphanumeric_hyphen_dash(s) {
Ok(Self(s.to_string()))
} else {
Err(CardError::InvalidFluentName(s.to_string()))
}
}
}
impl Named<'_> for FluentName {
fn new_with_weight(_name_str: &str, _weight: u32) -> Self {
todo!()
}
fn fluent_name(&self) -> &FluentName {
self
}
fn fluent_name_string(&self) -> &String {
&self.0
}
fn is_blank(&self) -> bool {
self.fluent_name_string() == Self::BLANK
}
}
#[cfg(test)]
#[allow(non_snake_case)]
mod fluent_tests {
use super::*;
#[test]
fn new() {
assert_eq!(FluentName("queen".to_string()), FluentName::new("queen"));
}
#[test]
fn is_alphanumeric_hyphen_dash() {
assert!(FluentName::is_alphanumeric_hyphen_dash("Hello-World"));
assert!(FluentName::is_alphanumeric_hyphen_dash("HelloWorld"));
assert!(!FluentName::is_alphanumeric_hyphen_dash("🁚"));
assert!(!FluentName::is_alphanumeric_hyphen_dash(" "));
}
#[test]
fn from_str() {
assert_eq!(
"hierophant",
FluentName::from_str("hierophant")
.unwrap()
.fluent_name_string()
);
}
#[test]
fn from_str__error() {
let sut = FluentName::from_str("I'm a bad bad fluent string name.");
let my_err = sut.unwrap_err();
assert_eq!(
CardError::InvalidFluentName("I'm a bad bad fluent string name.".to_string()),
my_err
);
assert_eq!(
"Invalid FluentName: `I'm a bad bad fluent string name.`. Must be alphanumeric with hyphens, en-dashes, or em-dashes.",
my_err.to_string()
);
}
#[test]
fn named__fluent_value() {
assert_eq!(
"Daus",
FluentName::new("daus").fluent_value("long", &FluentName::DEUTSCH)
);
assert_eq!(
"_",
FluentName::new("+++").fluent_value("symbol", &FluentName::US_ENGLISH)
);
}
#[test]
fn named__is_blank() {
assert!(FluentName::new("blank").is_blank());
assert!(!FluentName::new("long").is_blank());
}
#[test]
fn named__index() {
assert_eq!(
"S",
FluentName::new("spades").index(&FluentName::US_ENGLISH)
);
assert_eq!(
"P",
FluentName::new("pentacles").index(&FluentName::US_ENGLISH)
);
assert_eq!("K", FluentName::new("clubs").index(&FluentName::DEUTSCH));
}
#[test]
fn named__index_default() {
assert_eq!("S", FluentName::new("spades").index_default());
assert_eq!("P", FluentName::new("pentacles").index_default());
}
#[allow(dead_code)]
struct WeightedName {
name: FluentName,
weight: u32,
}
impl Named<'_> for WeightedName {
fn new_with_weight(name_str: &str, weight: u32) -> Self {
WeightedName {
name: FluentName::new(name_str),
weight,
}
}
fn fluent_name(&self) -> &FluentName {
&self.name
}
fn fluent_name_string(&self) -> &String {
self.name.fluent_name_string()
}
fn is_blank(&self) -> bool {
self.name.is_blank()
}
}
#[test]
fn named__weighted_vector__not_empty() {
let names = &["ace", "king", "queen", "jack"];
let result = WeightedName::weighted_vector(names);
assert!(!result.is_empty());
assert_eq!(result.len(), names.len());
}
#[test]
fn named__weighted_vector__weights_decrease() {
let names = &["ace", "king", "queen", "jack", "ten"];
let result = WeightedName::weighted_vector(names);
for i in 0..(result.len() - 1) {
assert!(
result[i].weight > result[i + 1].weight,
"Expected weight[{i}] ({}) > weight[{}] ({})",
result[i].weight,
i + 1,
result[i + 1].weight
);
}
}
#[test]
fn fluent_name__fmt__not_empty() {
let name = FluentName::new("spades");
let s = format!("{name}");
assert!(!s.is_empty());
assert_eq!(s, "spades");
}
#[test]
fn fluent_name__accessor__returns_self() {
let name = FluentName::new("hearts");
assert_eq!(name.fluent_name(), &name);
}
#[test]
fn is_alphanumeric_hyphen_dash__en_dash() {
assert!(FluentName::is_alphanumeric_hyphen_dash(
"hello\u{2013}world"
));
}
#[test]
fn is_alphanumeric_hyphen_dash__em_dash() {
assert!(FluentName::is_alphanumeric_hyphen_dash(
"hello\u{2014}world"
));
}
#[test]
fn is_alphanumeric_hyphen_dash__invalid() {
assert!(!FluentName::is_alphanumeric_hyphen_dash("not valid!"));
assert!(!FluentName::is_alphanumeric_hyphen_dash(" "));
}
}