use alloc::{format, string::String};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
use crate::cbor::canonical;
const I18N_ID_PREFIX: &str = "i18n:v1:";
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct I18nTag(String);
impl I18nTag {
pub fn normalize_tag(input: &str) -> Result<Self, I18nTagError> {
let langid: LanguageIdentifier = input.parse()?;
Ok(Self(langid.to_string()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl core::fmt::Display for I18nTag {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Error)]
pub enum I18nTagError {
#[error("invalid locale tag: {0}")]
Invalid(#[from] LanguageIdentifierError),
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct I18nId(String);
impl I18nId {
pub fn parse(value: &str) -> Result<Self, I18nIdError> {
if !value.starts_with(I18N_ID_PREFIX) {
return Err(I18nIdError::InvalidPrefix);
}
let encoded = &value[I18N_ID_PREFIX.len()..];
canonical::decode_base32_crockford(encoded)?;
Ok(Self(value.to_owned()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl core::fmt::Display for I18nId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.0)
}
}
pub fn id_for_tag(tag: &I18nTag) -> Result<I18nId, I18nIdError> {
let canonical_bytes = canonical::to_canonical_cbor(&tag.as_str())?;
let digest = canonical::blake3_128(&canonical_bytes);
let encoded = canonical::encode_base32_crockford(&digest);
Ok(I18nId(format!("{I18N_ID_PREFIX}{encoded}")))
}
#[derive(Debug, Error)]
pub enum I18nIdError {
#[error("i18n ID must begin with {I18N_ID_PREFIX}")]
InvalidPrefix,
#[error("invalid base32 payload: {0}")]
Base32(#[from] canonical::Base32Error),
#[error(transparent)]
Canonical(#[from] canonical::CanonicalError),
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Direction {
#[default]
Ltr,
Rtl,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MinimalI18nProfile {
pub language: String,
pub region: Option<String>,
pub script: Option<String>,
pub direction: Direction,
pub calendar: String,
pub currency: String,
pub decimal_separator: String,
pub timezone: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_tag_case() {
let tag = match I18nTag::normalize_tag("en-gb") {
Ok(tag) => tag,
Err(err) => panic!("normalize failed: {err:?}"),
};
assert_eq!(tag.as_str(), "en-GB");
}
#[test]
fn tag_id_roundtrip() {
let tag = match I18nTag::normalize_tag("en-US") {
Ok(tag) => tag,
Err(err) => panic!("normalize failed: {err:?}"),
};
let id = match id_for_tag(&tag) {
Ok(id) => id,
Err(err) => panic!("id generation failed: {err:?}"),
};
let roundtrip = match I18nId::parse(id.as_str()) {
Ok(value) => value,
Err(err) => panic!("parse failed: {err:?}"),
};
assert_eq!(roundtrip.as_str(), id.as_str());
}
}