use crate::provider::*;
use icu_locale_core::LanguageIdentifier;
use icu_locale_core::subtags::{Language, Region, Script, region, script};
use icu_locale_fallback::provider::{LikelySubtagsForLanguage, LocaleLikelySubtagsLanguageV1};
use icu_provider::prelude::*;
use crate::TransformResult;
#[derive(Debug, Clone)]
pub struct LocaleExpander {
likely_subtags_l: DataPayload<LocaleLikelySubtagsLanguageV1>,
likely_subtags_sr: DataPayload<LocaleLikelySubtagsScriptRegionV1>,
likely_subtags_ext: Option<DataPayload<LocaleLikelySubtagsExtendedV1>>,
}
struct LocaleExpanderBorrowed<'a> {
likely_subtags_l: &'a LikelySubtagsForLanguage<'a>,
likely_subtags_sr: &'a LikelySubtagsForScriptRegion<'a>,
likely_subtags_ext: Option<&'a LikelySubtagsExtended<'a>>,
}
impl LocaleExpanderBorrowed<'_> {
fn get_l(&self, l: Language) -> Option<(Script, Region)> {
let key = &l.to_tinystr().to_unvalidated();
self.likely_subtags_l.language.get_copied(key).or_else(|| {
self.likely_subtags_ext
.and_then(|ext| ext.language.get_copied(key))
})
}
fn get_ls(&self, l: Language, s: Script) -> Option<Region> {
let key = &(
l.to_tinystr().to_unvalidated(),
s.to_tinystr().to_unvalidated(),
);
self.likely_subtags_l
.language_script
.get_copied(key)
.or_else(|| {
self.likely_subtags_ext
.and_then(|ext| ext.language_script.get_copied(key))
})
}
fn get_lr(&self, l: Language, r: Region) -> Option<Script> {
let key = &(
l.to_tinystr().to_unvalidated(),
r.to_tinystr().to_unvalidated(),
);
self.likely_subtags_l
.language_region
.get_copied(key)
.or_else(|| {
self.likely_subtags_ext
.and_then(|ext| ext.language_region.get_copied(key))
})
}
fn get_s(&self, s: Script) -> Option<(Language, Region)> {
let key = &s.to_tinystr().to_unvalidated();
self.likely_subtags_sr.script.get_copied(key).or_else(|| {
self.likely_subtags_ext
.and_then(|ext| ext.script.get_copied(key))
})
}
fn get_sr(&self, s: Script, r: Region) -> Option<Language> {
let key = &(
s.to_tinystr().to_unvalidated(),
r.to_tinystr().to_unvalidated(),
);
self.likely_subtags_sr
.script_region
.get_copied(key)
.or_else(|| {
self.likely_subtags_ext
.and_then(|ext| ext.script_region.get_copied(key))
})
}
fn get_r(&self, r: Region) -> Option<(Language, Script)> {
let key = &r.to_tinystr().to_unvalidated();
self.likely_subtags_sr.region.get_copied(key).or_else(|| {
self.likely_subtags_ext
.and_then(|ext| ext.region.get_copied(key))
})
}
fn get_und(&self) -> (Language, Script, Region) {
self.likely_subtags_l.und
}
}
#[inline]
fn update_langid(
language: Language,
script: Option<Script>,
region: Option<Region>,
langid: &mut LanguageIdentifier,
) -> TransformResult {
let mut modified = false;
if langid.language.is_unknown() && !language.is_unknown() {
langid.language = language;
modified = true;
}
if langid.script.is_none() && script.is_some() {
langid.script = script;
modified = true;
}
if langid.region.is_none() && region.is_some() {
langid.region = region;
modified = true;
}
if modified {
TransformResult::Modified
} else {
TransformResult::Unmodified
}
}
#[inline]
fn update_langid_minimize(
language: Language,
script: Option<Script>,
region: Option<Region>,
langid: &mut LanguageIdentifier,
) -> TransformResult {
let mut modified = false;
if langid.language != language {
langid.language = language;
modified = true;
}
if langid.script != script {
langid.script = script;
modified = true;
}
if langid.region != region {
langid.region = region;
modified = true;
}
if modified {
TransformResult::Modified
} else {
TransformResult::Unmodified
}
}
impl LocaleExpander {
#[cfg(feature = "compiled_data")]
pub const fn new_common() -> Self {
LocaleExpander {
likely_subtags_l: DataPayload::from_static_ref(
icu_locale_fallback::provider::Baked::SINGLETON_LOCALE_LIKELY_SUBTAGS_LANGUAGE_V1,
),
likely_subtags_sr: DataPayload::from_static_ref(
Baked::SINGLETON_LOCALE_LIKELY_SUBTAGS_SCRIPT_REGION_V1,
),
likely_subtags_ext: None,
}
}
icu_provider::gen_buffer_data_constructors!(() -> error: DataError,
functions: [
new_common: skip,
try_new_common_with_buffer_provider,
try_new_common_unstable,
Self
]);
#[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new_common)]
pub fn try_new_common_unstable<P>(provider: &P) -> Result<LocaleExpander, DataError>
where
P: DataProvider<LocaleLikelySubtagsLanguageV1>
+ DataProvider<LocaleLikelySubtagsScriptRegionV1>
+ ?Sized,
{
let likely_subtags_l = provider.load(Default::default())?.payload;
let likely_subtags_sr = provider.load(Default::default())?.payload;
Ok(LocaleExpander {
likely_subtags_l,
likely_subtags_sr,
likely_subtags_ext: None,
})
}
#[cfg(feature = "compiled_data")]
pub const fn new_extended() -> Self {
LocaleExpander {
likely_subtags_l: DataPayload::from_static_ref(
icu_locale_fallback::provider::Baked::SINGLETON_LOCALE_LIKELY_SUBTAGS_LANGUAGE_V1,
),
likely_subtags_sr: DataPayload::from_static_ref(
Baked::SINGLETON_LOCALE_LIKELY_SUBTAGS_SCRIPT_REGION_V1,
),
likely_subtags_ext: Some(DataPayload::from_static_ref(
Baked::SINGLETON_LOCALE_LIKELY_SUBTAGS_EXTENDED_V1,
)),
}
}
icu_provider::gen_buffer_data_constructors!(() -> error: DataError,
functions: [
new_extended: skip,
try_new_extended_with_buffer_provider,
try_new_extended_unstable,
Self
]);
#[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new_extended)]
pub fn try_new_extended_unstable<P>(provider: &P) -> Result<LocaleExpander, DataError>
where
P: DataProvider<LocaleLikelySubtagsLanguageV1>
+ DataProvider<LocaleLikelySubtagsScriptRegionV1>
+ DataProvider<LocaleLikelySubtagsExtendedV1>
+ ?Sized,
{
let likely_subtags_l = provider.load(Default::default())?.payload;
let likely_subtags_sr = provider.load(Default::default())?.payload;
let likely_subtags_ext = Some(provider.load(Default::default())?.payload);
Ok(LocaleExpander {
likely_subtags_l,
likely_subtags_sr,
likely_subtags_ext,
})
}
fn as_borrowed(&self) -> LocaleExpanderBorrowed<'_> {
LocaleExpanderBorrowed {
likely_subtags_l: self.likely_subtags_l.get(),
likely_subtags_sr: self.likely_subtags_sr.get(),
likely_subtags_ext: self.likely_subtags_ext.as_ref().map(|p| p.get()),
}
}
pub fn maximize(&self, langid: &mut LanguageIdentifier) -> TransformResult {
let data = self.as_borrowed();
let (und_l, und_s, und_r) = data.get_und();
let mut stripped_transform_result = TransformResult::Unmodified;
if langid.script == Some(script!("Zzzz")) {
langid.script = None;
stripped_transform_result = TransformResult::Modified;
}
if langid.region == Some(region!("ZZ")) {
langid.region = None;
stripped_transform_result = TransformResult::Modified;
}
if !langid.language.is_unknown() && langid.script.is_some() && langid.region.is_some() {
return stripped_transform_result;
}
if !langid.language.is_unknown() {
if let Some(region) = langid.region
&& let Some(script) = data.get_lr(langid.language, region)
{
return update_langid(Language::UNKNOWN, Some(script), None, langid);
}
if let Some(script) = langid.script
&& let Some(region) = data.get_ls(langid.language, script)
{
return update_langid(Language::UNKNOWN, None, Some(region), langid);
}
if let Some((script, region)) = data.get_l(langid.language) {
return update_langid(Language::UNKNOWN, Some(script), Some(region), langid);
}
return stripped_transform_result;
}
if let Some(script) = langid.script {
if let Some(region) = langid.region
&& let Some(language) = data
.get_sr(script, region)
.or_else(|| ((script, region) == (und_s, und_r)).then_some(und_l))
{
return update_langid(language, None, None, langid);
}
if let Some((language, region)) = data
.get_s(script)
.or_else(|| (script == und_s).then_some((und_l, und_r)))
{
return update_langid(language, None, Some(region), langid);
}
}
if let Some(region) = langid.region
&& let Some((language, script)) = data
.get_r(region)
.or_else(|| (region == und_r).then_some((und_l, und_s)))
{
return update_langid(language, Some(script), None, langid);
}
debug_assert!(langid.language.is_unknown());
stripped_transform_result
}
pub fn minimize(&self, langid: &mut LanguageIdentifier) -> TransformResult {
self.minimize_impl(langid, true)
}
pub fn minimize_favor_script(&self, langid: &mut LanguageIdentifier) -> TransformResult {
self.minimize_impl(langid, false)
}
fn minimize_impl(
&self,
langid: &mut LanguageIdentifier,
favor_region: bool,
) -> TransformResult {
let mut max = langid.clone();
self.maximize(&mut max);
let mut trial = max.clone();
trial.script = None;
trial.region = None;
self.maximize(&mut trial);
if trial == max {
return update_langid_minimize(max.language, None, None, langid);
}
if favor_region {
trial.script = None;
trial.region = max.region;
self.maximize(&mut trial);
if trial == max {
return update_langid_minimize(max.language, None, max.region, langid);
}
trial.script = max.script;
trial.region = None;
self.maximize(&mut trial);
if trial == max {
return update_langid_minimize(max.language, max.script, None, langid);
}
} else {
trial.script = max.script;
trial.region = None;
self.maximize(&mut trial);
if trial == max {
return update_langid_minimize(max.language, max.script, None, langid);
}
trial.script = None;
trial.region = max.region;
self.maximize(&mut trial);
if trial == max {
return update_langid_minimize(max.language, None, max.region, langid);
}
}
update_langid_minimize(max.language, max.script, max.region, langid)
}
#[inline]
#[doc(hidden)] pub fn get_likely_script(&self, langid: &LanguageIdentifier) -> Option<Script> {
langid
.script
.filter(|s| *s != script!("Zzzz"))
.or_else(|| self.infer_likely_script(langid.language, langid.region))
}
fn infer_likely_script(&self, language: Language, region: Option<Region>) -> Option<Script> {
let data = self.as_borrowed();
let (und_l, und_s, und_r) = data.get_und();
let region = region.filter(|r| *r != region!("ZZ"));
if !language.is_unknown() {
if let Some(region) = region
&& let Some(script) = data.get_lr(language, region)
{
return Some(script);
}
if let Some((script, _)) = data.get_l(language) {
return Some(script);
}
return None;
}
if let Some(region) = region
&& let Some((_, script)) = data
.get_r(region)
.or_else(|| (region == und_r).then_some((und_l, und_s)))
{
return Some(script);
}
debug_assert!(language.is_unknown());
None
}
}
impl AsRef<LocaleExpander> for LocaleExpander {
fn as_ref(&self) -> &LocaleExpander {
self
}
}
#[cfg(feature = "serde")]
#[cfg(test)]
mod tests {
use super::*;
use icu_locale_core::{locale, subtags::script};
#[test]
fn test_minimize_favor_script() {
let lc = LocaleExpander::new_common();
let mut locale = locale!("yue-Hans");
assert_eq!(
lc.minimize_favor_script(&mut locale.id),
TransformResult::Unmodified
);
assert_eq!(locale, locale!("yue-Hans"));
}
#[test]
fn test_minimize_favor_region() {
let lc = LocaleExpander::new_common();
let mut locale = locale!("yue-Hans");
assert_eq!(lc.minimize(&mut locale.id), TransformResult::Modified);
assert_eq!(locale, locale!("yue-CN"));
}
#[test]
fn test_get_likely_script() {
let lc = LocaleExpander::new_common();
assert_eq!(lc.get_likely_script(&locale!("und").id), None);
assert_eq!(
lc.get_likely_script(&locale!("en").id),
Some(script!("Latn"))
);
assert_eq!(
lc.get_likely_script(&locale!("en-US").id),
Some(script!("Latn"))
);
assert_eq!(
lc.get_likely_script(&locale!("und-US").id),
Some(script!("Latn"))
);
assert_eq!(
lc.get_likely_script(&locale!("und-SA").id),
Some(script!("Arab"))
);
assert_eq!(lc.get_likely_script(&locale!("tlh").id), None);
assert_eq!(lc.get_likely_script(&locale!("tlh-US").id), None);
assert_eq!(lc.get_likely_script(&locale!("tlh-SA").id), None);
assert_eq!(
lc.get_likely_script(&locale!("de-Zzzz").id),
Some(script!("Latn"))
);
assert_eq!(
lc.get_likely_script(&locale!("de-Zzzz-ZZ").id),
Some(script!("Latn"))
);
assert_eq!(lc.get_likely_script(&locale!("und-ZZ").id), None);
}
#[test]
fn test_maximize() {
let lc = LocaleExpander::new_common();
let mut locale;
locale = locale!("und");
assert_eq!(lc.maximize(&mut locale.id), TransformResult::Unmodified);
locale = locale!("en");
assert_eq!(lc.maximize(&mut locale.id), TransformResult::Modified);
assert_eq!(locale, locale!("en-Latn-US"));
locale = locale!("en-US");
assert_eq!(lc.maximize(&mut locale.id), TransformResult::Modified);
assert_eq!(locale, locale!("en-Latn-US"));
locale = locale!("und-US");
assert_eq!(lc.maximize(&mut locale.id), TransformResult::Modified);
assert_eq!(locale, locale!("en-Latn-US"));
locale = locale!("und-SA");
assert_eq!(lc.maximize(&mut locale.id), TransformResult::Modified);
assert_eq!(locale, locale!("ar-Arab-SA"));
locale = locale!("tlh");
assert_eq!(lc.maximize(&mut locale.id), TransformResult::Unmodified);
locale = locale!("tlh-US");
assert_eq!(lc.maximize(&mut locale.id), TransformResult::Unmodified);
locale = locale!("tlh-SA");
assert_eq!(lc.maximize(&mut locale.id), TransformResult::Unmodified);
}
#[test]
fn test_maximize_strips_placeholder_subtags() {
let lc = LocaleExpander::new_extended();
let mut locale = locale!("de-Latn-ZZ");
assert_eq!(lc.maximize(&mut locale.id), TransformResult::Modified);
assert_eq!(locale, locale!("de-Latn-DE"));
let mut locale = locale!("de-Zzzz-DE");
assert_eq!(lc.maximize(&mut locale.id), TransformResult::Modified);
assert_eq!(locale, locale!("de-Latn-DE"));
let mut locale = locale!("de-Zzzz-ZZ");
assert_eq!(lc.maximize(&mut locale.id), TransformResult::Modified);
assert_eq!(locale, locale!("de-Latn-DE"));
}
#[test]
fn test_maximize_strips_placeholder_subtags_no_further_data() {
let lc = LocaleExpander::new_extended();
let mut locale = locale!("und-Zzzz-ZZ");
assert_eq!(lc.maximize(&mut locale.id), TransformResult::Modified);
assert_eq!(locale, locale!("und"));
let mut locale = locale!("tlh-Zzzz-ZZ");
assert_eq!(lc.maximize(&mut locale.id), TransformResult::Modified);
assert_eq!(locale, locale!("tlh"));
}
}