use std::path::PathBuf;
use std::process::Command;
use icu_provider::DataMarker as _;
use crate::meta::Project;
use crate::ops::status;
const CORE_LOCALES: &[&str] = &["en", "fr", "es", "de", "ja", "zh"];
const PINNED_ICU_PROVIDER_MINOR: &str = "2.2";
pub fn init_source_cache() {
if std::env::var_os("ICU4X_SOURCE_CACHE").is_none()
&& let Some(dir) = day_icu_dir()
{
unsafe { std::env::set_var("ICU4X_SOURCE_CACHE", dir.join("src")) };
}
}
pub fn apply(cmd: &mut Command, project: &Project) {
if let Some(dir) = ensure_thinned_data(project) {
cmd.env("ICU4X_DATA_DIR", &dir);
}
}
fn day_icu_dir() -> Option<PathBuf> {
#[allow(deprecated)] std::env::home_dir().map(|h| h.join(".day/icu"))
}
fn locale_set(project: &Project) -> Vec<String> {
let mut set: Vec<String> = CORE_LOCALES.iter().map(|s| s.to_string()).collect();
let dir = project.root.join("resource/locales");
if let Ok(rd) = std::fs::read_dir(dir) {
for entry in rd.flatten() {
if entry.path().is_dir()
&& let Some(name) = entry.file_name().to_str()
{
let base = name.split("-u-").next().unwrap_or(name);
if base != "en-XA" {
set.push(base.to_string());
}
}
}
}
set.sort();
set.dedup();
set
}
fn lock_icu_provider_version(project: &Project) -> Option<String> {
let lock = std::fs::read_to_string(project.root.join("Cargo.lock")).ok()?;
let mut lines = lock.lines().peekable();
while let Some(line) = lines.next() {
if line.trim() == "name = \"icu_provider\""
&& let Some(next) = lines.peek()
&& let Some(v) = next.trim().strip_prefix("version = \"")
{
return Some(v.trim_end_matches('"').to_string());
}
}
None
}
pub fn ensure_thinned_data(project: &Project) -> Option<PathBuf> {
if std::env::var_os("DAY_ICU_FULL_DATA").is_some() {
return None;
}
if let Some(v) = lock_icu_provider_version(project)
&& !v.starts_with(PINNED_ICU_PROVIDER_MINOR)
{
status(
"Warning",
&format!(
"icu_provider {v} in Cargo.lock doesn't match day's datagen \
({PINNED_ICU_PROVIDER_MINOR}.x) — building with full locale data"
),
);
return None;
}
let locales = locale_set(project);
let root = day_icu_dir()?;
let key = cache_key(&locales);
let dir = root.join("baked").join(&key);
if dir.join("day-icu.json").is_file() {
return Some(dir); }
let cache_populated = root.join("src").is_dir();
let offline = std::env::var_os("DAY_NO_ICU_FETCH").is_some()
|| std::env::var_os("DAY_NO_UPDATE_CHECK").is_some();
if offline && !cache_populated {
status(
"Warning",
"locale-data thinning skipped (DAY_NO_ICU_FETCH/DAY_NO_UPDATE_CHECK set and no \
cached CLDR source) — the app embeds all-locale data",
);
return None;
}
if !cache_populated {
status(
"Fetching",
"CLDR + ICU export data (one-time, ~100 MB, cached in ~/.day/icu/src; \
DAY_NO_ICU_FETCH=1 skips — apps then embed all-locale data)",
);
}
status(
"Baking",
&format!(
"ICU locale data for {} (~/.day/icu/baked)",
locales.join(", ")
),
);
match generate(&locales, &root, &dir) {
Ok(()) => Some(dir),
Err(e) => {
status(
"Warning",
&format!("locale-data thinning failed ({e}) — building with full locale data"),
);
None
}
}
}
fn cache_key(locales: &[String]) -> String {
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
h.update(locales.join(","));
h.update(":");
h.update(PINNED_ICU_PROVIDER_MINOR);
let out = h.finalize();
out.iter().take(12).map(|b| format!("{b:02x}")).collect()
}
macro_rules! cb {
($($marker_ty:ty:$marker:ident,)+ #[unstable] $($emarker_ty:ty:$emarker:ident,)+) => {
fn all_markers() -> Vec<icu_provider::DataMarkerInfo> {
vec![$(<$marker_ty>::INFO,)+]
}
};
}
icu_provider_registry::registry!(cb);
fn generate(
locales: &[String],
root: &std::path::Path,
dir: &std::path::Path,
) -> Result<(), String> {
use icu_provider_export::baked_exporter::{self, BakedExporter};
use icu_provider_export::prelude::*;
use icu_provider_source::SourceDataProvider;
let families: Vec<DataLocaleFamily> = locales
.iter()
.filter_map(|l| l.parse().ok())
.map(DataLocaleFamily::with_descendants)
.collect();
if families.is_empty() {
return Err("no parseable locales".into());
}
let tmp = root
.join("baked")
.join(format!(".tmp-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
std::fs::create_dir_all(&tmp).map_err(|e| e.to_string())?;
let provider = SourceDataProvider::new();
let fallbacker =
LocaleFallbacker::try_new_unstable(&provider).map_err(|e| format!("fallbacker: {e}"))?;
let exporter = {
let mut options = baked_exporter::Options::default();
options.overwrite = true;
options.use_internal_fallback = true;
BakedExporter::new(tmp.clone(), options).map_err(|e| format!("exporter: {e}"))?
};
ExportDriver::new(families, DeduplicationStrategy::Maximal.into(), fallbacker)
.with_markers(all_markers())
.export(&provider, exporter)
.map_err(|e| format!("datagen: {e}"))?;
std::fs::write(
tmp.join("day-icu.json"),
format!(
"{{\"locales\":\"{}\",\"icu_provider\":\"{}\"}}\n",
locales.join(","),
PINNED_ICU_PROVIDER_MINOR
),
)
.map_err(|e| e.to_string())?;
let _ = std::fs::remove_dir_all(dir);
std::fs::rename(&tmp, dir).map_err(|e| format!("cache rename: {e}"))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cache_key_is_stable_and_locale_sensitive() {
let a = cache_key(&["en".into(), "fr".into()]);
assert_eq!(a, cache_key(&["en".into(), "fr".into()]));
assert_ne!(a, cache_key(&["en".into(), "zh".into()]));
assert_eq!(a.len(), 24);
}
#[test]
fn core_locales_match_catalog() {
let catalog = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../day-l10n/catalog");
if !catalog.is_dir() {
return;
}
let mut found: Vec<String> = std::fs::read_dir(catalog)
.unwrap()
.flatten()
.filter_map(|e| {
e.file_name()
.to_str()?
.strip_suffix(".ftl")
.map(str::to_string)
})
.collect();
found.sort();
let mut ours: Vec<String> = CORE_LOCALES.iter().map(|s| s.to_string()).collect();
ours.sort();
assert_eq!(ours, found, "CORE_LOCALES drifted from day-l10n/catalog");
}
}