use crate::rayon_prelude::*;
use crate::FallbackMode;
use displaydoc::Display;
use icu_locid::extensions::unicode::key;
use icu_locid::LanguageIdentifier;
use icu_locid::ParserError;
use icu_locid_transform::fallback::LocaleFallbackIterator;
use icu_locid_transform::LocaleFallbacker;
use icu_provider::datagen::*;
use icu_provider::prelude::*;
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::collections::HashSet;
use std::fmt;
use std::hash::Hash;
use std::str::FromStr;
use std::time::Duration;
use std::time::Instant;
use writeable::Writeable;
#[non_exhaustive]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
pub struct NoFallbackOptions {}
#[non_exhaustive]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum RuntimeFallbackLocation {
Internal,
External,
}
#[non_exhaustive]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum DeduplicationStrategy {
Maximal,
RetainBaseLanguages,
None,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub(crate) struct LocaleFamilyAnnotations {
include_ancestors: bool,
include_descendants: bool,
}
impl LocaleFamilyAnnotations {
#[inline]
pub(crate) const fn with_descendants() -> Self {
Self {
include_ancestors: true,
include_descendants: true,
}
}
#[inline]
pub(crate) const fn without_descendants() -> Self {
Self {
include_ancestors: true,
include_descendants: false,
}
}
#[inline]
pub(crate) const fn without_ancestors() -> Self {
Self {
include_ancestors: false,
include_descendants: true,
}
}
#[inline]
pub(crate) const fn single() -> Self {
Self {
include_ancestors: false,
include_descendants: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct LocaleFamily {
langid: Option<LanguageIdentifier>,
annotations: LocaleFamilyAnnotations,
}
impl LocaleFamily {
pub const fn with_descendants(langid: LanguageIdentifier) -> Self {
Self {
langid: Some(langid),
annotations: LocaleFamilyAnnotations::with_descendants(),
}
}
pub const fn without_descendants(langid: LanguageIdentifier) -> Self {
Self {
langid: Some(langid),
annotations: LocaleFamilyAnnotations::without_descendants(),
}
}
pub const fn without_ancestors(langid: LanguageIdentifier) -> Self {
Self {
langid: Some(langid),
annotations: LocaleFamilyAnnotations::without_ancestors(),
}
}
pub const fn single(langid: LanguageIdentifier) -> Self {
Self {
langid: Some(langid),
annotations: LocaleFamilyAnnotations::single(),
}
}
pub const FULL: Self = Self {
langid: None,
annotations: LocaleFamilyAnnotations {
include_ancestors: false,
include_descendants: true,
},
};
pub(crate) fn into_parts(self) -> (Option<LanguageIdentifier>, LocaleFamilyAnnotations) {
(self.langid, self.annotations)
}
pub(crate) fn as_borrowed(&self) -> LocaleFamilyBorrowed {
LocaleFamilyBorrowed {
langid: self.langid.as_ref(),
annotations: self.annotations,
}
}
}
impl Writeable for LocaleFamily {
#[inline]
fn write_to<W: core::fmt::Write + ?Sized>(&self, sink: &mut W) -> core::fmt::Result {
self.as_borrowed().write_to(sink)
}
#[inline]
fn writeable_length_hint(&self) -> writeable::LengthHint {
self.as_borrowed().writeable_length_hint()
}
}
writeable::impl_display_with_writeable!(LocaleFamily);
pub(crate) struct LocaleFamilyBorrowed<'a> {
langid: Option<&'a LanguageIdentifier>,
annotations: LocaleFamilyAnnotations,
}
impl<'a> LocaleFamilyBorrowed<'a> {
pub(crate) fn from_parts(
inner: (&'a Option<LanguageIdentifier>, &LocaleFamilyAnnotations),
) -> Self {
Self {
langid: inner.0.as_ref(),
annotations: *inner.1,
}
}
}
impl Writeable for LocaleFamilyBorrowed<'_> {
fn write_to<W: core::fmt::Write + ?Sized>(&self, sink: &mut W) -> core::fmt::Result {
match (
&self.langid,
self.annotations.include_ancestors,
self.annotations.include_descendants,
) {
(Some(langid), true, true) => langid.write_to(sink),
(Some(langid), true, false) => {
sink.write_char('^')?;
langid.write_to(sink)
}
(Some(langid), false, true) => {
sink.write_char('%')?;
langid.write_to(sink)
}
(Some(langid), false, false) => {
sink.write_char('@')?;
langid.write_to(sink)
}
(None, _, _) => sink.write_str("full"),
}
}
fn writeable_length_hint(&self) -> writeable::LengthHint {
match (
&self.langid,
self.annotations.include_ancestors,
self.annotations.include_descendants,
) {
(Some(langid), true, true) => langid.writeable_length_hint(),
(Some(langid), true, false) => langid.writeable_length_hint() + 1,
(Some(langid), false, true) => langid.writeable_length_hint() + 1,
(Some(langid), false, false) => langid.writeable_length_hint() + 1,
(None, _, _) => writeable::LengthHint::exact(4),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Display)]
#[non_exhaustive]
pub enum LocaleFamilyParseError {
#[displaydoc("{0}")]
LanguageIdentifier(ParserError),
#[displaydoc("Invalid locale family")]
InvalidFamily,
}
impl From<ParserError> for LocaleFamilyParseError {
fn from(err: ParserError) -> Self {
Self::LanguageIdentifier(err)
}
}
impl std::error::Error for LocaleFamilyParseError {}
impl FromStr for LocaleFamily {
type Err = LocaleFamilyParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "full" {
return Ok(Self::FULL);
}
let (first, remainder) = s
.as_bytes()
.split_first()
.ok_or(LocaleFamilyParseError::InvalidFamily)?;
match first {
b'^' => Ok(Self {
langid: Some(LanguageIdentifier::try_from_bytes(remainder)?),
annotations: LocaleFamilyAnnotations::without_descendants(),
}),
b'%' => Ok(Self {
langid: Some(LanguageIdentifier::try_from_bytes(remainder)?),
annotations: LocaleFamilyAnnotations::without_ancestors(),
}),
b'@' => Ok(Self {
langid: Some(LanguageIdentifier::try_from_bytes(remainder)?),
annotations: LocaleFamilyAnnotations::single(),
}),
b if b.is_ascii_alphanumeric() => Ok(Self {
langid: Some(s.parse()?),
annotations: LocaleFamilyAnnotations::with_descendants(),
}),
_ => Err(LocaleFamilyParseError::InvalidFamily),
}
}
}
#[test]
fn test_locale_family_parsing() {
let valid_families = ["und", "de-CH", "^es", "@pt-BR", "%en-001", "full"];
let invalid_families = ["invalid", "@invalid", "-foo", "@full", "full-001"];
for family_str in valid_families {
let family = family_str.parse::<LocaleFamily>().unwrap();
let family_to_str = family.to_string();
assert_eq!(family_str, family_to_str);
}
for family_str in invalid_families {
assert!(family_str.parse::<LocaleFamily>().is_err());
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct FallbackOptions {
pub runtime_fallback_location: Option<RuntimeFallbackLocation>,
pub deduplication_strategy: Option<DeduplicationStrategy>,
}
#[derive(Debug, Clone)]
enum LocalesWithOrWithoutFallback {
WithFallback {
families: HashMap<Option<LanguageIdentifier>, LocaleFamilyAnnotations>,
options: FallbackOptions,
},
WithoutFallback {
langids: HashSet<LanguageIdentifier>,
},
}
#[derive(Debug, Clone)]
pub struct DatagenDriver {
keys: Option<HashSet<DataKey>>,
locales_fallback: Option<LocalesWithOrWithoutFallback>,
legacy_locales: Option<Option<Vec<LanguageIdentifier>>>,
legacy_fallback_mode: FallbackMode,
additional_collations: HashSet<String>,
segmenter_models: Vec<String>,
}
impl DatagenDriver {
#[allow(clippy::new_without_default)] pub fn new() -> Self {
Self {
keys: None,
locales_fallback: None,
legacy_fallback_mode: FallbackMode::default(),
legacy_locales: None,
additional_collations: HashSet::new(),
segmenter_models: Vec::new(),
}
.with_recommended_segmenter_models()
}
pub fn with_keys(self, keys: impl IntoIterator<Item = DataKey>) -> Self {
Self {
keys: Some(keys.into_iter().collect()),
..self
}
}
#[deprecated(
since = "1.5.0",
note = "use `with_locales_and_fallback` or `with_locales_no_fallback`"
)]
pub fn with_locales(self, locales: impl IntoIterator<Item = LanguageIdentifier>) -> Self {
Self {
legacy_locales: Some(Some(locales.into_iter().collect())),
..self
}
}
#[deprecated(since = "1.5.0", note = "use `with_locales_and_fallback`")]
pub fn with_all_locales(self) -> Self {
Self {
legacy_locales: Some(None),
..self
}
}
pub fn with_locales_no_fallback(
self,
locales: impl IntoIterator<Item = LanguageIdentifier>,
_options: NoFallbackOptions,
) -> Self {
Self {
locales_fallback: Some(LocalesWithOrWithoutFallback::WithoutFallback {
langids: locales.into_iter().collect(),
}),
..self
}
}
pub fn with_locales_and_fallback(
self,
locales: impl IntoIterator<Item = LocaleFamily>,
options: FallbackOptions,
) -> Self {
Self {
locales_fallback: Some(LocalesWithOrWithoutFallback::WithFallback {
families: locales.into_iter().map(LocaleFamily::into_parts).collect(),
options,
}),
..self
}
}
#[deprecated(
since = "1.5.0",
note = "use `with_locales_and_fallback` or `with_locales_no_fallback`"
)]
pub fn with_fallback_mode(self, fallback: FallbackMode) -> Self {
Self {
legacy_fallback_mode: fallback,
..self
}
}
pub fn with_additional_collations(
self,
additional_collations: impl IntoIterator<Item = String>,
) -> Self {
Self {
additional_collations: additional_collations.into_iter().collect(),
..self
}
}
pub fn with_recommended_segmenter_models(self) -> Self {
self.with_segmenter_models([
"cjdict".into(),
"burmesedict".into(),
"khmerdict".into(),
"laodict".into(),
"thaidict".into(),
"Burmese_codepoints_exclusive_model4_heavy".into(),
"Khmer_codepoints_exclusive_model4_heavy".into(),
"Lao_codepoints_exclusive_model4_heavy".into(),
"Thai_codepoints_exclusive_model4_heavy".into(),
])
}
pub fn with_segmenter_models(self, models: impl IntoIterator<Item = String>) -> Self {
Self {
segmenter_models: models.into_iter().collect(),
..self
}
}
pub fn export(
self,
provider: &impl ExportableProvider,
mut sink: impl DataExporter,
) -> Result<(), DataError> {
self.export_dyn(provider, &mut sink)
}
fn export_dyn(
self,
provider: &dyn ExportableProvider,
sink: &mut dyn DataExporter,
) -> Result<(), DataError> {
let Self {
keys,
locales_fallback,
legacy_locales,
legacy_fallback_mode,
additional_collations,
segmenter_models,
} = self;
let Some(keys) = keys else {
return Err(DataError::custom(
"`DatagenDriver::with_keys` needs to be called",
));
};
let map_legacy_locales_to_locales_with_expansion =
|legacy_locales: Option<Vec<LanguageIdentifier>>| match legacy_locales {
Some(v) => v
.into_iter()
.map(LocaleFamily::with_descendants)
.map(LocaleFamily::into_parts)
.collect(),
None => [LocaleFamily::FULL]
.into_iter()
.map(LocaleFamily::into_parts)
.collect(),
};
let locales_fallback = match (locales_fallback, legacy_locales, legacy_fallback_mode) {
(Some(locales_fallback), _, _) => locales_fallback,
(_, Some(legacy_locales), FallbackMode::PreferredForExporter) => {
LocalesWithOrWithoutFallback::WithFallback {
families: map_legacy_locales_to_locales_with_expansion(legacy_locales),
options: FallbackOptions {
runtime_fallback_location: None,
deduplication_strategy: None,
},
}
}
(_, Some(legacy_locales), FallbackMode::Runtime) => {
LocalesWithOrWithoutFallback::WithFallback {
families: map_legacy_locales_to_locales_with_expansion(legacy_locales),
options: FallbackOptions {
runtime_fallback_location: Some(RuntimeFallbackLocation::Internal),
deduplication_strategy: Some(DeduplicationStrategy::Maximal),
},
}
}
(_, Some(legacy_locales), FallbackMode::RuntimeManual) => {
LocalesWithOrWithoutFallback::WithFallback {
families: map_legacy_locales_to_locales_with_expansion(legacy_locales),
options: FallbackOptions {
runtime_fallback_location: Some(RuntimeFallbackLocation::External),
deduplication_strategy: Some(DeduplicationStrategy::Maximal),
},
}
}
(_, Some(Some(locales)), FallbackMode::Preresolved) => {
LocalesWithOrWithoutFallback::WithoutFallback {
langids: locales.into_iter().collect(),
}
}
(_, Some(None), FallbackMode::Preresolved) => {
return Err(DataError::custom(
"FallbackMode::Preresolved requires an explicit locale set",
));
}
(_, Some(legacy_locales), FallbackMode::Hybrid) => {
LocalesWithOrWithoutFallback::WithFallback {
families: map_legacy_locales_to_locales_with_expansion(legacy_locales),
options: FallbackOptions {
runtime_fallback_location: Some(RuntimeFallbackLocation::External),
deduplication_strategy: Some(DeduplicationStrategy::None),
},
}
}
_ => {
return Err(DataError::custom(
"`DatagenDriver::with_locales` or `with_all_locales` or `with_locales_and_fallback` or `with_locales_no_fallback` needs to be called",
));
}
};
if keys.is_empty() {
log::warn!("No keys selected");
}
let (uses_internal_fallback, deduplication_strategy) = match &locales_fallback {
LocalesWithOrWithoutFallback::WithoutFallback { langids } => {
let mut sorted_locale_strs = langids
.iter()
.map(|x| x.write_to_string())
.collect::<Vec<_>>();
sorted_locale_strs.sort_unstable();
log::info!(
"Datagen configured without fallback with these locales: {:?}",
sorted_locale_strs
);
(false, DeduplicationStrategy::None)
}
LocalesWithOrWithoutFallback::WithFallback { options, families } => {
let uses_internal_fallback = match options.runtime_fallback_location {
None => sink.supports_built_in_fallback(),
Some(RuntimeFallbackLocation::Internal) => true,
Some(RuntimeFallbackLocation::External) => false,
};
let deduplication_strategy = match options.deduplication_strategy {
None => {
if sink.supports_built_in_fallback() {
DeduplicationStrategy::Maximal
} else {
DeduplicationStrategy::None
}
}
Some(x) => x,
};
let mut sorted_locale_strs = families
.iter()
.map(LocaleFamilyBorrowed::from_parts)
.map(|family| family.write_to_string().into_owned())
.collect::<Vec<_>>();
sorted_locale_strs.sort_unstable();
log::info!(
"Datagen configured with {}, {}, and these locales: {:?}",
if uses_internal_fallback {
"internal fallback"
} else {
"external fallback"
},
match deduplication_strategy {
DeduplicationStrategy::Maximal => "maximal deduplication",
DeduplicationStrategy::RetainBaseLanguages =>
"deduplication retaining base languages",
DeduplicationStrategy::None => "no deduplication",
},
sorted_locale_strs
);
(uses_internal_fallback, deduplication_strategy)
}
};
let fallbacker =
Lazy::new(|| LocaleFallbacker::try_new_with_any_provider(&provider.as_any_provider()));
let load_with_fallback = |key, locale: &_| {
log::trace!("Generating key/locale: {key}/{locale:}");
let mut metadata = DataRequestMetadata::default();
metadata.silent = true;
let mut locale_iter: Option<LocaleFallbackIterator> = None;
loop {
let req = DataRequest {
locale: locale_iter.as_ref().map(|i| i.get()).unwrap_or(locale),
metadata,
};
match provider.load_data(key, req) {
Ok(data_response) => {
if let Some(iter) = locale_iter.as_ref() {
if iter.get().is_und() && !locale.is_und() {
log::debug!("Falling back to und: {key}/{locale}");
}
}
return Some(
data_response
.take_payload()
.map_err(|e| e.with_req(key, req)),
);
}
Err(DataError {
kind: DataErrorKind::MissingLocale,
..
}) => {
if let Some(iter) = locale_iter.as_mut() {
if iter.get().is_und() {
log::debug!("Could not find data for: {key}/{locale}");
return None;
}
iter.step();
} else {
match fallbacker.as_ref() {
Ok(fallbacker) => {
locale_iter = Some(
fallbacker
.for_config(key.fallback_config())
.fallback_for(locale.clone()),
)
}
Err(e) => return Some(Err(*e)),
}
}
}
Err(e) => return Some(Err(e.with_req(key, req))),
}
}
};
keys.clone().into_par_iter().try_for_each(|key| {
log::trace!("Generating key {key}");
let instant1 = Instant::now();
if key.metadata().singleton {
if provider.supported_locales_for_key(key)? != [Default::default()] {
return Err(
DataError::custom("Invalid supported locales for singleton key")
.with_key(key),
);
}
let payload = provider
.load_data(key, Default::default())
.and_then(DataResponse::take_payload)
.map_err(|e| e.with_req(key, Default::default()))?;
let transform_duration = instant1.elapsed();
sink.flush_singleton(key, &payload)
.map_err(|e| e.with_req(key, Default::default()))?;
let final_duration = instant1.elapsed();
let flush_duration = final_duration - transform_duration;
if final_duration > Duration::new(0, 500_000_000) {
log::info!(
"Generated key {key} ({}, flushed in {})",
DisplayDuration(final_duration),
DisplayDuration(flush_duration)
);
} else {
log::info!("Generated key {key}");
}
return Ok(());
}
let locales_to_export = select_locales_for_key(
provider,
key,
&locales_fallback,
&additional_collations,
&segmenter_models,
&fallbacker,
)?;
let (slowest_duration, slowest_locale) = match deduplication_strategy {
DeduplicationStrategy::Maximal => {
let payloads = locales_to_export
.into_par_iter()
.filter_map(|locale| {
let instant2 = Instant::now();
load_with_fallback(key, &locale)
.map(|r| r.map(|payload| (locale, (payload, instant2.elapsed()))))
})
.collect::<Result<HashMap<_, _>, _>>()?;
let fallbacker = fallbacker.as_ref().map_err(|e| *e)?;
deduplicate_payloads::<true>(key, &payloads, fallbacker, sink)?
}
DeduplicationStrategy::RetainBaseLanguages => {
let payloads = locales_to_export
.into_par_iter()
.filter_map(|locale| {
let instant2 = Instant::now();
load_with_fallback(key, &locale)
.map(|r| r.map(|payload| (locale, (payload, instant2.elapsed()))))
})
.collect::<Result<HashMap<_, _>, _>>()?;
let fallbacker = fallbacker.as_ref().map_err(|e| *e)?;
deduplicate_payloads::<false>(key, &payloads, fallbacker, sink)?
}
DeduplicationStrategy::None => locales_to_export
.into_par_iter()
.filter_map(|locale| {
let instant2 = Instant::now();
let result = load_with_fallback(key, &locale)?;
let result = result
.and_then(|payload| sink.put_payload(key, &locale, &payload))
.map(|_| (instant2.elapsed(), locale.write_to_string().into_owned()))
.map_err(|e| {
e.with_req(
key,
DataRequest {
locale: &locale,
metadata: Default::default(),
},
)
});
Some(result)
})
.collect::<Result<Vec<_>, DataError>>()?
.into_iter()
.max(),
}
.unwrap_or_default();
let transform_duration = instant1.elapsed();
if uses_internal_fallback && !key.path().get().starts_with("segmenter") {
sink.flush_with_built_in_fallback(key, BuiltInFallbackMode::Standard)
} else {
sink.flush(key)
}
.map_err(|e| e.with_key(key))?;
let final_duration = instant1.elapsed();
let flush_duration = final_duration - transform_duration;
if final_duration > Duration::new(0, 500_000_000) {
log::info!(
"Generated key {key} ({}, '{slowest_locale}' in {}, flushed in {})",
DisplayDuration(final_duration),
DisplayDuration(slowest_duration),
DisplayDuration(flush_duration)
);
} else {
log::info!("Generated key {key}");
}
Ok(())
})?;
sink.close()
}
}
fn select_locales_for_key(
provider: &dyn ExportableProvider,
key: DataKey,
locales_fallback: &LocalesWithOrWithoutFallback,
additional_collations: &HashSet<String>,
segmenter_models: &[String],
fallbacker: &Lazy<
Result<LocaleFallbacker, DataError>,
impl FnOnce() -> Result<LocaleFallbacker, DataError>,
>,
) -> Result<HashSet<icu_provider::DataLocale>, DataError> {
let mut supported_map = HashMap::<LanguageIdentifier, HashSet<DataLocale>>::new();
for locale in provider
.supported_locales_for_key(key)
.map_err(|e| e.with_key(key))?
{
supported_map
.entry(locale.get_langid())
.or_default()
.insert(locale);
}
if key.path().get().starts_with("segmenter/dictionary/") {
supported_map.retain(|_, locales| {
locales.retain(|locale| {
let model = crate::dictionary_data_locale_to_model_name(locale);
segmenter_models.iter().any(|m| Some(m.as_ref()) == model)
});
!locales.is_empty()
});
return Ok(supported_map.into_values().flatten().collect());
} else if key.path().get().starts_with("segmenter/lstm/") {
supported_map.retain(|_, locales| {
locales.retain(|locale| {
let model = crate::lstm_data_locale_to_model_name(locale);
segmenter_models.iter().any(|m| Some(m.as_ref()) == model)
});
!locales.is_empty()
});
return Ok(supported_map.into_values().flatten().collect());
} else if key.path().get().starts_with("collator/") {
supported_map.retain(|_, locales| {
locales.retain(|locale| {
let Some(collation) = locale
.get_unicode_ext(&key!("co"))
.and_then(|co| co.as_single_subtag().copied())
else {
return true;
};
additional_collations.contains(collation.as_str())
|| if collation.starts_with("search") {
additional_collations.contains("search*")
} else {
!["big5han", "gb2312"].contains(&collation.as_str())
}
});
!locales.is_empty()
});
}
let mut include_full = false;
let requested_families: HashMap<LanguageIdentifier, LocaleFamilyAnnotations> =
match locales_fallback {
LocalesWithOrWithoutFallback::WithFallback { families, .. } if families.is_empty() => {
[(LanguageIdentifier::UND, LocaleFamilyAnnotations::single())]
.into_iter()
.collect()
}
LocalesWithOrWithoutFallback::WithFallback { families, .. } => families
.iter()
.filter_map(|(langid, annotations)| {
if let Some(langid) = langid.as_ref() {
if *langid == LanguageIdentifier::UND {
Some((LanguageIdentifier::UND, LocaleFamilyAnnotations::single()))
} else {
Some((langid.clone(), *annotations))
}
} else {
debug_assert_eq!(annotations, &LocaleFamily::FULL.annotations);
include_full = true;
None
}
})
.collect(),
LocalesWithOrWithoutFallback::WithoutFallback { langids } => langids
.iter()
.map(|langid| (langid.clone(), LocaleFamilyAnnotations::single()))
.collect(),
};
if include_full && requested_families.is_empty() {
let selected_locales = supported_map.into_values().flatten().collect();
return Ok(selected_locales);
}
let fallbacker = fallbacker.as_ref().map_err(|e| *e)?;
let fallbacker_with_config = fallbacker.for_config(key.fallback_config());
let all_candidate_langids = supported_map
.keys()
.chain(requested_families.keys())
.collect::<HashSet<_>>();
let mut selected_langids = requested_families.keys().cloned().collect::<HashSet<_>>();
let expansion_map: HashMap<&LanguageIdentifier, HashSet<DataLocale>> = all_candidate_langids
.into_iter()
.map(|current_langid| {
let mut expansion = supported_map
.get(current_langid)
.cloned()
.unwrap_or_default();
if include_full && !selected_langids.contains(current_langid) {
log::trace!("Including {current_langid}: full locale family: {key}");
selected_langids.insert(current_langid.clone());
}
if current_langid.language.is_empty() && current_langid != &LanguageIdentifier::UND {
log::trace!("Including {current_langid}: und variant: {key}");
selected_langids.insert(current_langid.clone());
}
let include_ancestors = requested_families
.get(current_langid)
.map(|family| family.include_ancestors)
.unwrap_or(false);
let mut iter = fallbacker_with_config.fallback_for(current_langid.into());
loop {
let parent_langid: LanguageIdentifier = iter.get().get_langid();
let maybe_parent_locales = supported_map.get(&parent_langid);
let include_descendants = requested_families
.get(&parent_langid)
.map(|family| family.include_descendants)
.unwrap_or(false);
if include_descendants && !selected_langids.contains(current_langid) {
log::trace!("Including {current_langid}: descendant of {parent_langid}: {key}");
selected_langids.insert(current_langid.clone());
}
if include_ancestors && !selected_langids.contains(&parent_langid) {
log::trace!("Including {parent_langid}: ancestor of {current_langid}: {key}");
selected_langids.insert(parent_langid);
}
if let Some(parent_locales) = maybe_parent_locales {
for morphed_locale in parent_locales.iter() {
if morphed_locale.is_langid_und() && !morphed_locale.is_empty() {
continue;
}
let mut morphed_locale = morphed_locale.clone();
morphed_locale.set_langid(current_langid.clone());
expansion.insert(morphed_locale);
}
}
if iter.get().is_und() {
break;
}
iter.step();
}
(current_langid, expansion)
})
.collect();
let selected_locales = expansion_map
.into_iter()
.filter(|(langid, _)| selected_langids.contains(langid))
.flat_map(|(_, data_locales)| data_locales)
.collect();
Ok(selected_locales)
}
fn deduplicate_payloads<const MAXIMAL: bool>(
key: DataKey,
payloads: &HashMap<DataLocale, (DataPayload<ExportMarker>, Duration)>,
fallbacker: &LocaleFallbacker,
sink: &dyn DataExporter,
) -> Result<Option<(Duration, String)>, DataError> {
let fallbacker_with_config = fallbacker.for_config(key.fallback_config());
payloads
.iter()
.try_for_each(|(locale, (payload, _duration))| {
if locale.is_und() {
return sink.put_payload(key, locale, payload).map_err(|e| {
e.with_req(
key,
DataRequest {
locale,
metadata: Default::default(),
},
)
});
}
let mut iter = fallbacker_with_config.fallback_for(locale.clone());
loop {
if !MAXIMAL {
iter.step();
}
if iter.get().is_und() {
break;
}
if MAXIMAL {
iter.step();
}
if let Some((inherited_payload, _duration)) = payloads.get(iter.get()) {
if inherited_payload == payload {
log::trace!(
"Deduplicating {key}/{locale} (inherits from {})",
iter.get()
);
return Ok(());
} else {
break;
}
}
}
sink.put_payload(key, locale, payload).map_err(|e| {
e.with_req(
key,
DataRequest {
locale,
metadata: Default::default(),
},
)
})
})?;
Ok(payloads
.iter()
.map(|(locale, (_payload, duration))| (*duration, locale.write_to_string().into_owned()))
.max())
}
struct DisplayDuration(pub Duration);
impl fmt::Display for DisplayDuration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let nanos = self.0.as_nanos();
if nanos > 100_000_000 {
write!(f, "{:.3}s", self.0.as_secs_f64())
} else if nanos > 1_000_000 {
write!(f, "{:.3}ms", (nanos as f64) / 1e6)
} else if nanos > 1_000 {
write!(f, "{:.3}µs", (nanos as f64) / 1e3)
} else {
write!(f, "{}ns", nanos)
}
}
}
#[test]
fn test_collation_filtering() {
use icu_locid::langid;
use std::collections::BTreeSet;
#[derive(Debug)]
struct TestCase<'a> {
include_collations: &'a [&'a str],
language: LanguageIdentifier,
expected: &'a [&'a str],
}
let cases = [
TestCase {
include_collations: &[],
language: langid!("zh"),
expected: &["zh", "zh-u-co-stroke", "zh-u-co-unihan", "zh-u-co-zhuyin"],
},
TestCase {
include_collations: &["gb2312"],
language: langid!("zh"),
expected: &[
"zh",
"zh-u-co-gb2312",
"zh-u-co-stroke",
"zh-u-co-unihan",
"zh-u-co-zhuyin",
],
},
TestCase {
include_collations: &["big5han"],
language: langid!("zh"),
expected: &[
"zh",
"zh-u-co-big5han",
"zh-u-co-stroke",
"zh-u-co-unihan",
"zh-u-co-zhuyin",
],
},
TestCase {
include_collations: &["gb2312", "search*"],
language: langid!("zh"),
expected: &[
"zh",
"zh-u-co-gb2312",
"zh-u-co-stroke",
"zh-u-co-unihan",
"zh-u-co-zhuyin",
],
},
TestCase {
include_collations: &[],
language: langid!("ko"),
expected: &["ko", "ko-u-co-unihan"],
},
TestCase {
include_collations: &["search"],
language: langid!("ko"),
expected: &["ko", "ko-u-co-search", "ko-u-co-unihan"],
},
TestCase {
include_collations: &["searchjl"],
language: langid!("ko"),
expected: &["ko", "ko-u-co-searchjl", "ko-u-co-unihan"],
},
TestCase {
include_collations: &["search", "searchjl"],
language: langid!("ko"),
expected: &["ko", "ko-u-co-search", "ko-u-co-searchjl", "ko-u-co-unihan"],
},
TestCase {
include_collations: &["search*", "big5han"],
language: langid!("ko"),
expected: &["ko", "ko-u-co-search", "ko-u-co-searchjl", "ko-u-co-unihan"],
},
TestCase {
include_collations: &[],
language: langid!("und"),
expected: &["und", "und-u-co-emoji", "und-u-co-eor"],
},
];
for cas in cases {
let resolved_locales = select_locales_for_key(
&crate::provider::DatagenProvider::new_testing(),
icu_collator::provider::CollationDataV1Marker::KEY,
&LocalesWithOrWithoutFallback::WithoutFallback {
langids: [cas.language.clone()].into_iter().collect(),
},
&HashSet::from_iter(cas.include_collations.iter().copied().map(String::from)),
&[],
&once_cell::sync::Lazy::new(|| Ok(LocaleFallbacker::new_without_data())),
)
.unwrap()
.into_iter()
.map(|l| l.to_string())
.collect::<BTreeSet<_>>();
let expected_locales = cas
.expected
.iter()
.copied()
.map(String::from)
.collect::<BTreeSet<_>>();
assert_eq!(resolved_locales, expected_locales, "{cas:?}");
}
}
#[test]
fn test_family_precedence() {
let driver = DatagenDriver::new().with_locales_and_fallback(
[
"en".parse().unwrap(),
"%en".parse().unwrap(),
"@en".parse().unwrap(),
"%zh-TW".parse().unwrap(),
"^zh-TW".parse().unwrap(),
],
Default::default(),
);
let Some(LocalesWithOrWithoutFallback::WithFallback { families, .. }) = driver.locales_fallback
else {
panic!("expected locales with fallback")
};
assert_eq!(
families,
[
"@en".parse::<LocaleFamily>().unwrap().into_parts(),
"^zh-TW".parse::<LocaleFamily>().unwrap().into_parts()
]
.into_iter()
.collect::<HashMap<_, _>>()
);
}