|
| 1 | +//! ResolveLocale support for `Intl.DateTimeFormat`'s `ca`, `hc`, and `nu` |
| 2 | +//! relevant extension keys. |
| 3 | +
|
| 4 | +use super::*; |
| 5 | + |
| 6 | +/// The calendars Perry exposes through `Intl.supportedValuesOf("calendar")`. |
| 7 | +/// CreateDateTimeFormat may only retain one of these values; well-formed future |
| 8 | +/// or implementation-specific identifiers fall back through ResolveLocale. |
| 9 | +const SUPPORTED_CALENDARS: &[&str] = &[ |
| 10 | + "buddhist", |
| 11 | + "chinese", |
| 12 | + "coptic", |
| 13 | + "dangi", |
| 14 | + "ethioaa", |
| 15 | + "ethiopic", |
| 16 | + "gregory", |
| 17 | + "hebrew", |
| 18 | + "indian", |
| 19 | + "islamic", |
| 20 | + "islamic-civil", |
| 21 | + "islamic-rgsa", |
| 22 | + "islamic-tbla", |
| 23 | + "islamic-umalqura", |
| 24 | + "iso8601", |
| 25 | + "japanese", |
| 26 | + "persian", |
| 27 | + "roc", |
| 28 | +]; |
| 29 | + |
| 30 | +pub(super) struct DateTimeLocaleResolution { |
| 31 | + pub(super) locale: String, |
| 32 | + pub(super) calendar: String, |
| 33 | + pub(super) numbering_system: String, |
| 34 | + /// Effective `hc` override from the extension or options. Locale defaults |
| 35 | + /// remain absent so ICU can select its own CLDR preference. |
| 36 | + pub(super) hour_cycle: Option<String>, |
| 37 | +} |
| 38 | + |
| 39 | +fn base_locale(locale: &str) -> String { |
| 40 | + locale |
| 41 | + .split('-') |
| 42 | + .take_while(|part| part.len() != 1) |
| 43 | + .collect::<Vec<_>>() |
| 44 | + .join("-") |
| 45 | +} |
| 46 | + |
| 47 | +fn supported_calendar(value: &str) -> Option<String> { |
| 48 | + let canonical = canonicalize_calendar_id(value)?; |
| 49 | + SUPPORTED_CALENDARS |
| 50 | + .contains(&canonical.as_str()) |
| 51 | + .then_some(canonical) |
| 52 | +} |
| 53 | + |
| 54 | +fn supported_hour_cycle(value: &str) -> Option<String> { |
| 55 | + ["h11", "h12", "h23", "h24"] |
| 56 | + .contains(&value) |
| 57 | + .then(|| value.to_string()) |
| 58 | +} |
| 59 | + |
| 60 | +fn push_keyword(locale: &mut String, started: &mut bool, key: &str, value: &str) { |
| 61 | + if !*started { |
| 62 | + locale.push_str("-u"); |
| 63 | + *started = true; |
| 64 | + } |
| 65 | + locale.push('-'); |
| 66 | + locale.push_str(key); |
| 67 | + locale.push('-'); |
| 68 | + locale.push_str(value); |
| 69 | +} |
| 70 | + |
| 71 | +/// Apply ResolveLocale to DateTimeFormat's relevant extension keys. Only a |
| 72 | +/// supported `ca`, `hc`, or `nu` keyword may survive in the resolved locale; |
| 73 | +/// unrelated keys and unsupported values are removed. A supported explicit |
| 74 | +/// option wins, while an unsupported option leaves a supported extension value |
| 75 | +/// in place. `hour12` suppresses both the `hourCycle` option and the `hc` |
| 76 | +/// extension, as required by CreateDateTimeFormat. |
| 77 | +pub(super) fn resolve_date_time_locale( |
| 78 | + requested: &str, |
| 79 | + calendar_option: Option<&str>, |
| 80 | + numbering_option: Option<&str>, |
| 81 | + hour12: Option<bool>, |
| 82 | + hour_cycle_option: Option<&str>, |
| 83 | +) -> DateTimeLocaleResolution { |
| 84 | + let ext_calendar = unicode_extension_keyword(requested, "ca") |
| 85 | + .as_deref() |
| 86 | + .and_then(supported_calendar); |
| 87 | + let opt_calendar = calendar_option.and_then(supported_calendar); |
| 88 | + let calendar = opt_calendar |
| 89 | + .or_else(|| ext_calendar.clone()) |
| 90 | + .unwrap_or_else(|| "gregory".to_string()); |
| 91 | + |
| 92 | + let ext_numbering = unicode_extension_keyword(requested, "nu") |
| 93 | + .map(|value| value.to_ascii_lowercase()) |
| 94 | + .filter(|value| numbering_system::is_supported_numbering_system(value)); |
| 95 | + let opt_numbering = numbering_option |
| 96 | + .map(|value| value.to_ascii_lowercase()) |
| 97 | + .filter(|value| numbering_system::is_supported_numbering_system(value)); |
| 98 | + let numbering_system = opt_numbering |
| 99 | + .or_else(|| ext_numbering.clone()) |
| 100 | + .unwrap_or_else(|| "latn".to_string()); |
| 101 | + |
| 102 | + let ext_hour_cycle = unicode_extension_keyword(requested, "hc") |
| 103 | + .as_deref() |
| 104 | + .and_then(supported_hour_cycle); |
| 105 | + let hour_cycle = if hour12.is_some() { |
| 106 | + None |
| 107 | + } else { |
| 108 | + hour_cycle_option |
| 109 | + .and_then(supported_hour_cycle) |
| 110 | + .or_else(|| ext_hour_cycle.clone()) |
| 111 | + }; |
| 112 | + |
| 113 | + let mut locale = base_locale(requested); |
| 114 | + let mut started = false; |
| 115 | + if ext_calendar.as_deref() == Some(calendar.as_str()) { |
| 116 | + push_keyword(&mut locale, &mut started, "ca", &calendar); |
| 117 | + } |
| 118 | + if hour12.is_none() && ext_hour_cycle.as_deref() == hour_cycle.as_deref() { |
| 119 | + if let Some(ref hc) = hour_cycle { |
| 120 | + push_keyword(&mut locale, &mut started, "hc", hc); |
| 121 | + } |
| 122 | + } |
| 123 | + if ext_numbering.as_deref() == Some(numbering_system.as_str()) { |
| 124 | + push_keyword(&mut locale, &mut started, "nu", &numbering_system); |
| 125 | + } |
| 126 | + |
| 127 | + DateTimeLocaleResolution { |
| 128 | + locale, |
| 129 | + calendar, |
| 130 | + numbering_system, |
| 131 | + hour_cycle, |
| 132 | + } |
| 133 | +} |
| 134 | + |
| 135 | +#[cfg(test)] |
| 136 | +mod tests { |
| 137 | + use super::*; |
| 138 | + |
| 139 | + #[test] |
| 140 | + fn resolves_supported_and_unsupported_calendar_values() { |
| 141 | + let kept = resolve_date_time_locale("en-u-ca-iso8601", Some("invalid"), None, None, None); |
| 142 | + assert_eq!(kept.locale, "en-u-ca-iso8601"); |
| 143 | + assert_eq!(kept.calendar, "iso8601"); |
| 144 | + |
| 145 | + let replaced = |
| 146 | + resolve_date_time_locale("en-u-ca-gregory", Some("iso8601"), None, None, None); |
| 147 | + assert_eq!(replaced.locale, "en"); |
| 148 | + assert_eq!(replaced.calendar, "iso8601"); |
| 149 | + |
| 150 | + let future = resolve_date_time_locale("en-u-ca-bangla", Some("vikram"), None, None, None); |
| 151 | + assert_eq!(future.locale, "en"); |
| 152 | + assert_eq!(future.calendar, "gregory"); |
| 153 | + |
| 154 | + let alias = resolve_date_time_locale("en", Some("ethiopic-amete-alem"), None, None, None); |
| 155 | + assert_eq!(alias.calendar, "ethioaa"); |
| 156 | + |
| 157 | + let existing = resolve_date_time_locale("en", Some("islamic"), None, None, None); |
| 158 | + assert_eq!(existing.calendar, "islamic"); |
| 159 | + } |
| 160 | + |
| 161 | + #[test] |
| 162 | + fn removes_irrelevant_extensions_and_resolves_hc_and_nu() { |
| 163 | + let irrelevant = |
| 164 | + resolve_date_time_locale("ja-JP-u-cu-usd-tz-usnyc", None, None, None, None); |
| 165 | + assert_eq!(irrelevant.locale, "ja-JP"); |
| 166 | + |
| 167 | + let overridden = |
| 168 | + resolve_date_time_locale("en-u-hc-h23-nu-arab", None, None, None, Some("h11")); |
| 169 | + assert_eq!(overridden.locale, "en-u-nu-arab"); |
| 170 | + assert_eq!(overridden.hour_cycle.as_deref(), Some("h11")); |
| 171 | + assert_eq!(overridden.numbering_system, "arab"); |
| 172 | + |
| 173 | + let hour12 = resolve_date_time_locale("en-u-hc-h11", None, None, Some(false), Some("h23")); |
| 174 | + assert_eq!(hour12.locale, "en"); |
| 175 | + assert_eq!(hour12.hour_cycle, None); |
| 176 | + } |
| 177 | +} |
0 commit comments