use oxifont::db::{FontDatabase, Query};
fn locale_to_generic(bcp47: &str) -> &'static str {
let lang = bcp47.split('-').next().unwrap_or(bcp47);
match lang {
"ja" | "zh" | "ko" => "sans-serif-cjk",
_ => "sans-serif",
}
}
pub struct LocaleFontSelector {
db: FontDatabase,
}
impl LocaleFontSelector {
pub fn from_system() -> Result<Self, String> {
let db = FontDatabase::system().map_err(|e| e.to_string())?;
Ok(Self { db })
}
pub fn from_db(db: FontDatabase) -> Self {
Self { db }
}
pub fn database(&self) -> &FontDatabase {
&self.db
}
pub fn family_for_locale(&self, bcp47: &str) -> Option<String> {
let generic = locale_to_generic(bcp47);
self.query_family(bcp47, generic, 400)
}
pub fn locale_name_for_locale(&self, bcp47: &str) -> Option<String> {
let generic = locale_to_generic(bcp47);
let face = Query::new(&self.db)
.family(generic)
.weight(400)
.locale(bcp47)
.match_best()?;
Some(face.family_for_locale(bcp47).to_owned())
}
pub fn query_family(&self, bcp47: &str, generic: &str, weight: u16) -> Option<String> {
let face = Query::new(&self.db)
.family(generic)
.weight(weight)
.locale(bcp47)
.match_best()?;
Some(face.family.clone())
}
pub fn families_for_locale(&self, bcp47: &str, generic: &str, weight: u16) -> Vec<String> {
let faces = Query::new(&self.db)
.family(generic)
.weight(weight)
.locale(bcp47)
.match_all();
let mut seen = std::collections::HashSet::new();
let mut names = Vec::new();
for face in faces {
if seen.insert(face.family.to_lowercase()) {
names.push(face.family.clone());
}
}
names
}
pub fn batch_resolve(&self, locales: &[&str]) -> Vec<Option<String>> {
locales
.iter()
.map(|&bcp47| self.family_for_locale(bcp47))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn locale_to_generic_cjk() {
assert_eq!(locale_to_generic("ja"), "sans-serif-cjk");
assert_eq!(locale_to_generic("ja-JP"), "sans-serif-cjk");
assert_eq!(locale_to_generic("zh"), "sans-serif-cjk");
assert_eq!(locale_to_generic("zh-CN"), "sans-serif-cjk");
assert_eq!(locale_to_generic("zh-TW"), "sans-serif-cjk");
assert_eq!(locale_to_generic("ko"), "sans-serif-cjk");
assert_eq!(locale_to_generic("ko-KR"), "sans-serif-cjk");
}
#[test]
fn locale_to_generic_non_cjk() {
assert_eq!(locale_to_generic("en"), "sans-serif");
assert_eq!(locale_to_generic("en-US"), "sans-serif");
assert_eq!(locale_to_generic("de-DE"), "sans-serif");
assert_eq!(locale_to_generic("ar-SA"), "sans-serif");
assert_eq!(locale_to_generic("ru-RU"), "sans-serif");
}
#[test]
fn from_system_does_not_panic() {
let _result = LocaleFontSelector::from_system();
}
#[test]
fn family_for_locale_no_panic() {
let Ok(sel) = LocaleFontSelector::from_system() else {
return; };
let _ja = sel.family_for_locale("ja-JP");
let _en = sel.family_for_locale("en-US");
let _zh = sel.family_for_locale("zh-CN");
}
#[test]
fn locale_name_for_locale_no_panic() {
let Ok(sel) = LocaleFontSelector::from_system() else {
return;
};
let _name = sel.locale_name_for_locale("ja-JP");
}
#[test]
fn query_family_standard_generics() {
let Ok(sel) = LocaleFontSelector::from_system() else {
return;
};
for generic in &["sans-serif", "serif", "monospace", "cursive", "fantasy"] {
let _r = sel.query_family("en-US", generic, 400);
}
}
#[test]
fn families_for_locale_deduped() {
let Ok(sel) = LocaleFontSelector::from_system() else {
return;
};
let names = sel.families_for_locale("en-US", "sans-serif", 400);
let lower: std::collections::HashSet<String> =
names.iter().map(|n| n.to_lowercase()).collect();
assert_eq!(
lower.len(),
names.len(),
"families_for_locale must deduplicate"
);
}
#[test]
fn batch_resolve_length_matches_input() {
let Ok(sel) = LocaleFontSelector::from_system() else {
return;
};
let locales = ["en-US", "ja-JP", "zh-CN", "de-DE"];
let results = sel.batch_resolve(&locales);
assert_eq!(results.len(), locales.len());
}
#[test]
fn from_db_empty_returns_none() {
let empty_db = FontDatabase::new();
let sel = LocaleFontSelector::from_db(empty_db);
assert!(sel.family_for_locale("en-US").is_none());
assert!(sel.locale_name_for_locale("ja-JP").is_none());
assert!(sel.query_family("zh-CN", "sans-serif", 400).is_none());
assert!(sel
.families_for_locale("ko-KR", "sans-serif", 400)
.is_empty());
assert_eq!(sel.batch_resolve(&["en-US", "ja-JP"]), vec![None, None]);
}
#[test]
fn database_accessor() {
let db = FontDatabase::new();
let sel = LocaleFontSelector::from_db(db);
let stats = sel.database().stats();
assert_eq!(stats.face_count, 0);
}
}