feat(render): add support for localized dates. Fix #156

This commit is contained in:
Federico Terzi 2022-02-15 21:56:59 +01:00
parent ec24100260
commit 36a507488b
3 changed files with 475 additions and 12 deletions

37
Cargo.lock generated
View File

@ -212,6 +212,7 @@ dependencies = [
"libc",
"num-integer",
"num-traits",
"pure-rust-locales",
"time",
"winapi 0.3.9",
]
@ -399,6 +400,16 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "cstr_core"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "644828c273c063ab0d39486ba42a5d1f3a499d35529c759e763a9c6cb8a0fb08"
dependencies = [
"cty",
"memchr",
]
[[package]]
name = "ctor"
version = "0.1.20"
@ -409,6 +420,12 @@ dependencies = [
"syn 1.0.67",
]
[[package]]
name = "cty"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35"
[[package]]
name = "dbus"
version = "0.9.1"
@ -851,6 +868,7 @@ dependencies = [
"log",
"rand 0.8.3",
"regex",
"sys-locale",
"thiserror",
]
@ -2040,6 +2058,12 @@ dependencies = [
"unicode-xid 0.2.1",
]
[[package]]
name = "pure-rust-locales"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b45c49fc4f91f35bae654f85ebb3a44d60ac64f11b3166ffa609def390c732d8"
[[package]]
name = "quote"
version = "0.3.15"
@ -2577,6 +2601,19 @@ dependencies = [
"unicode-xid 0.0.4",
]
[[package]]
name = "sys-locale"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91f89ebb59fa30d4f65fafc2d68e94f6975256fd87e812dd99cb6e020c8563df"
dependencies = [
"cc",
"cstr_core",
"libc",
"web-sys",
"winapi 0.3.9",
]
[[package]]
name = "tempdir"
version = "0.3.7"

View File

@ -10,6 +10,7 @@ anyhow = "1.0.38"
thiserror = "1.0.23"
regex = "1.4.3"
lazy_static = "1.4.0"
chrono = "0.4.19"
chrono = {version = "0.4.19", features=["unstable-locales"]}
enum-as-inner = "0.3.3"
rand = "0.8.3"
rand = "0.8.3"
sys-locale = "0.1.0"

View File

@ -17,22 +17,30 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use chrono::{DateTime, Duration, Local};
use chrono::{DateTime, Duration, Local, Locale};
use crate::{Extension, ExtensionOutput, ExtensionResult, Number, Params, Value};
pub struct DateExtension {
pub trait LocaleProvider {
fn get_system_locale(&self) -> String;
}
pub struct DateExtension<'a> {
fixed_date: Option<DateTime<Local>>,
locale_provider: &'a dyn LocaleProvider,
}
#[allow(clippy::new_without_default)]
impl DateExtension {
pub fn new() -> Self {
Self { fixed_date: None }
impl<'a> DateExtension<'a> {
pub fn new(locale_provider: &'a dyn LocaleProvider) -> Self {
Self {
fixed_date: None,
locale_provider,
}
}
}
impl Extension for DateExtension {
impl<'a> Extension for DateExtension<'a> {
fn name(&self) -> &str {
"date"
}
@ -53,9 +61,14 @@ impl Extension for DateExtension {
}
let format = params.get("format");
let locale = params
.get("locale")
.and_then(|val| val.as_string())
.map(String::from)
.unwrap_or_else(|| self.locale_provider.get_system_locale());
let date = if let Some(Value::String(format)) = format {
now.format(format).to_string()
DateExtension::format_date_with_locale_string(now, format, &locale)
} else {
now.to_rfc2822()
};
@ -64,7 +77,7 @@ impl Extension for DateExtension {
}
}
impl DateExtension {
impl<'a> DateExtension<'a> {
fn get_date(&self) -> DateTime<Local> {
if let Some(fixed_date) = self.fixed_date {
fixed_date
@ -72,6 +85,339 @@ impl DateExtension {
Local::now()
}
}
fn format_date_with_locale(date: DateTime<Local>, format: &str, locale: Locale) -> String {
date.format_localized(format, locale).to_string()
}
fn format_date_with_locale_string(
date: DateTime<Local>,
format: &str,
locale_str: &str,
) -> String {
let locale = convert_locale_string_to_locale(locale_str).unwrap_or(Locale::en_US);
Self::format_date_with_locale(date, format, locale)
}
}
fn convert_locale_string_to_locale(locale_str: &str) -> Option<Locale> {
match locale_str {
"aa-DJ" => Some(Locale::aa_DJ),
"aa-ER" => Some(Locale::aa_ER),
"aa-ET" => Some(Locale::aa_ET),
"af-ZA" => Some(Locale::af_ZA),
"agr-PE" => Some(Locale::agr_PE),
"ak-GH" => Some(Locale::ak_GH),
"am-ET" => Some(Locale::am_ET),
"an-ES" => Some(Locale::an_ES),
"anp-IN" => Some(Locale::anp_IN),
"ar-AE" => Some(Locale::ar_AE),
"ar-BH" => Some(Locale::ar_BH),
"ar-DZ" => Some(Locale::ar_DZ),
"ar-EG" => Some(Locale::ar_EG),
"ar-IN" => Some(Locale::ar_IN),
"ar-IQ" => Some(Locale::ar_IQ),
"ar-JO" => Some(Locale::ar_JO),
"ar-KW" => Some(Locale::ar_KW),
"ar-LB" => Some(Locale::ar_LB),
"ar-LY" => Some(Locale::ar_LY),
"ar-MA" => Some(Locale::ar_MA),
"ar-OM" => Some(Locale::ar_OM),
"ar-QA" => Some(Locale::ar_QA),
"ar-SA" => Some(Locale::ar_SA),
"ar-SD" => Some(Locale::ar_SD),
"ar-SS" => Some(Locale::ar_SS),
"ar-SY" => Some(Locale::ar_SY),
"ar-TN" => Some(Locale::ar_TN),
"ar-YE" => Some(Locale::ar_YE),
"as-IN" => Some(Locale::as_IN),
"ast-ES" => Some(Locale::ast_ES),
"ayc-PE" => Some(Locale::ayc_PE),
"az-AZ" => Some(Locale::az_AZ),
"az-IR" => Some(Locale::az_IR),
"be-BY" => Some(Locale::be_BY),
"bem-ZM" => Some(Locale::bem_ZM),
"ber-DZ" => Some(Locale::ber_DZ),
"ber-MA" => Some(Locale::ber_MA),
"bg-BG" => Some(Locale::bg_BG),
"bhb-IN" => Some(Locale::bhb_IN),
"bho-IN" => Some(Locale::bho_IN),
"bho-NP" => Some(Locale::bho_NP),
"bi-VU" => Some(Locale::bi_VU),
"bn-BD" => Some(Locale::bn_BD),
"bn-IN" => Some(Locale::bn_IN),
"bo-CN" => Some(Locale::bo_CN),
"bo-IN" => Some(Locale::bo_IN),
"br-FR" => Some(Locale::br_FR),
"brx-IN" => Some(Locale::brx_IN),
"bs-BA" => Some(Locale::bs_BA),
"byn-ER" => Some(Locale::byn_ER),
"ca-AD" => Some(Locale::ca_AD),
"ca-ES" => Some(Locale::ca_ES),
"ca-FR" => Some(Locale::ca_FR),
"ca-IT" => Some(Locale::ca_IT),
"ce-RU" => Some(Locale::ce_RU),
"chr-US" => Some(Locale::chr_US),
"cmn-TW" => Some(Locale::cmn_TW),
"crh-UA" => Some(Locale::crh_UA),
"cs-CZ" => Some(Locale::cs_CZ),
"csb-PL" => Some(Locale::csb_PL),
"cv-RU" => Some(Locale::cv_RU),
"cy-GB" => Some(Locale::cy_GB),
"da-DK" => Some(Locale::da_DK),
"de-AT" => Some(Locale::de_AT),
"de-BE" => Some(Locale::de_BE),
"de-CH" => Some(Locale::de_CH),
"de-DE" => Some(Locale::de_DE),
"de-IT" => Some(Locale::de_IT),
"de-LI" => Some(Locale::de_LI),
"de-LU" => Some(Locale::de_LU),
"doi-IN" => Some(Locale::doi_IN),
"dsb-DE" => Some(Locale::dsb_DE),
"dv-MV" => Some(Locale::dv_MV),
"dz-BT" => Some(Locale::dz_BT),
"el-CY" => Some(Locale::el_CY),
"el-GR" => Some(Locale::el_GR),
"en-AG" => Some(Locale::en_AG),
"en-AU" => Some(Locale::en_AU),
"en-BW" => Some(Locale::en_BW),
"en-CA" => Some(Locale::en_CA),
"en-DK" => Some(Locale::en_DK),
"en-GB" => Some(Locale::en_GB),
"en-HK" => Some(Locale::en_HK),
"en-IE" => Some(Locale::en_IE),
"en-IL" => Some(Locale::en_IL),
"en-IN" => Some(Locale::en_IN),
"en-NG" => Some(Locale::en_NG),
"en-NZ" => Some(Locale::en_NZ),
"en-PH" => Some(Locale::en_PH),
"en-SC" => Some(Locale::en_SC),
"en-SG" => Some(Locale::en_SG),
"en-US" => Some(Locale::en_US),
"en-ZA" => Some(Locale::en_ZA),
"en-ZM" => Some(Locale::en_ZM),
"en-ZW" => Some(Locale::en_ZW),
"eo" => Some(Locale::eo),
"es-AR" => Some(Locale::es_AR),
"es-BO" => Some(Locale::es_BO),
"es-CL" => Some(Locale::es_CL),
"es-CO" => Some(Locale::es_CO),
"es-CR" => Some(Locale::es_CR),
"es-CU" => Some(Locale::es_CU),
"es-DO" => Some(Locale::es_DO),
"es-EC" => Some(Locale::es_EC),
"es-ES" => Some(Locale::es_ES),
"es-GT" => Some(Locale::es_GT),
"es-HN" => Some(Locale::es_HN),
"es-MX" => Some(Locale::es_MX),
"es-NI" => Some(Locale::es_NI),
"es-PA" => Some(Locale::es_PA),
"es-PE" => Some(Locale::es_PE),
"es-PR" => Some(Locale::es_PR),
"es-PY" => Some(Locale::es_PY),
"es-SV" => Some(Locale::es_SV),
"es-US" => Some(Locale::es_US),
"es-UY" => Some(Locale::es_UY),
"es-VE" => Some(Locale::es_VE),
"et-EE" => Some(Locale::et_EE),
"eu-ES" => Some(Locale::eu_ES),
"fa-IR" => Some(Locale::fa_IR),
"ff-SN" => Some(Locale::ff_SN),
"fi-FI" => Some(Locale::fi_FI),
"fil-PH" => Some(Locale::fil_PH),
"fo-FO" => Some(Locale::fo_FO),
"fr-BE" => Some(Locale::fr_BE),
"fr-CA" => Some(Locale::fr_CA),
"fr-CH" => Some(Locale::fr_CH),
"fr-FR" => Some(Locale::fr_FR),
"fr-LU" => Some(Locale::fr_LU),
"fur-IT" => Some(Locale::fur_IT),
"fy-DE" => Some(Locale::fy_DE),
"fy-NL" => Some(Locale::fy_NL),
"ga-IE" => Some(Locale::ga_IE),
"gd-GB" => Some(Locale::gd_GB),
"gez-ER" => Some(Locale::gez_ER),
"gez-ET" => Some(Locale::gez_ET),
"gl-ES" => Some(Locale::gl_ES),
"gu-IN" => Some(Locale::gu_IN),
"gv-GB" => Some(Locale::gv_GB),
"ha-NG" => Some(Locale::ha_NG),
"hak-TW" => Some(Locale::hak_TW),
"he-IL" => Some(Locale::he_IL),
"hi-IN" => Some(Locale::hi_IN),
"hif-FJ" => Some(Locale::hif_FJ),
"hne-IN" => Some(Locale::hne_IN),
"hr-HR" => Some(Locale::hr_HR),
"hsb-DE" => Some(Locale::hsb_DE),
"ht-HT" => Some(Locale::ht_HT),
"hu-HU" => Some(Locale::hu_HU),
"hy-AM" => Some(Locale::hy_AM),
"ia-FR" => Some(Locale::ia_FR),
"id-ID" => Some(Locale::id_ID),
"ig-NG" => Some(Locale::ig_NG),
"ik-CA" => Some(Locale::ik_CA),
"is-IS" => Some(Locale::is_IS),
"it-CH" => Some(Locale::it_CH),
"it-IT" => Some(Locale::it_IT),
"iu-CA" => Some(Locale::iu_CA),
"ja-JP" => Some(Locale::ja_JP),
"ka-GE" => Some(Locale::ka_GE),
"kab-DZ" => Some(Locale::kab_DZ),
"kk-KZ" => Some(Locale::kk_KZ),
"kl-GL" => Some(Locale::kl_GL),
"km-KH" => Some(Locale::km_KH),
"kn-IN" => Some(Locale::kn_IN),
"ko-KR" => Some(Locale::ko_KR),
"kok-IN" => Some(Locale::kok_IN),
"ks-IN" => Some(Locale::ks_IN),
"ku-TR" => Some(Locale::ku_TR),
"kw-GB" => Some(Locale::kw_GB),
"ky-KG" => Some(Locale::ky_KG),
"lb-LU" => Some(Locale::lb_LU),
"lg-UG" => Some(Locale::lg_UG),
"li-BE" => Some(Locale::li_BE),
"li-NL" => Some(Locale::li_NL),
"lij-IT" => Some(Locale::lij_IT),
"ln-CD" => Some(Locale::ln_CD),
"lo-LA" => Some(Locale::lo_LA),
"lt-LT" => Some(Locale::lt_LT),
"lv-LV" => Some(Locale::lv_LV),
"lzh-TW" => Some(Locale::lzh_TW),
"mag-IN" => Some(Locale::mag_IN),
"mai-IN" => Some(Locale::mai_IN),
"mai-NP" => Some(Locale::mai_NP),
"mfe-MU" => Some(Locale::mfe_MU),
"mg-MG" => Some(Locale::mg_MG),
"mhr-RU" => Some(Locale::mhr_RU),
"mi-NZ" => Some(Locale::mi_NZ),
"miq-NI" => Some(Locale::miq_NI),
"mjw-IN" => Some(Locale::mjw_IN),
"mk-MK" => Some(Locale::mk_MK),
"ml-IN" => Some(Locale::ml_IN),
"mn-MN" => Some(Locale::mn_MN),
"mni-IN" => Some(Locale::mni_IN),
"mnw-MM" => Some(Locale::mnw_MM),
"mr-IN" => Some(Locale::mr_IN),
"ms-MY" => Some(Locale::ms_MY),
"mt-MT" => Some(Locale::mt_MT),
"my-MM" => Some(Locale::my_MM),
"nan-TW" => Some(Locale::nan_TW),
"nb-NO" => Some(Locale::nb_NO),
"nds-DE" => Some(Locale::nds_DE),
"nds-NL" => Some(Locale::nds_NL),
"ne-NP" => Some(Locale::ne_NP),
"nhn-MX" => Some(Locale::nhn_MX),
"niu-NU" => Some(Locale::niu_NU),
"niu-NZ" => Some(Locale::niu_NZ),
"nl-AW" => Some(Locale::nl_AW),
"nl-BE" => Some(Locale::nl_BE),
"nl-NL" => Some(Locale::nl_NL),
"nn-NO" => Some(Locale::nn_NO),
"nr-ZA" => Some(Locale::nr_ZA),
"nso-ZA" => Some(Locale::nso_ZA),
"oc-FR" => Some(Locale::oc_FR),
"om-ET" => Some(Locale::om_ET),
"om-KE" => Some(Locale::om_KE),
"or-IN" => Some(Locale::or_IN),
"os-RU" => Some(Locale::os_RU),
"pa-IN" => Some(Locale::pa_IN),
"pa-PK" => Some(Locale::pa_PK),
"pap-AW" => Some(Locale::pap_AW),
"pap-CW" => Some(Locale::pap_CW),
"pl-PL" => Some(Locale::pl_PL),
"ps-AF" => Some(Locale::ps_AF),
"pt-BR" => Some(Locale::pt_BR),
"pt-PT" => Some(Locale::pt_PT),
"quz-PE" => Some(Locale::quz_PE),
"raj-IN" => Some(Locale::raj_IN),
"ro-RO" => Some(Locale::ro_RO),
"ru-RU" => Some(Locale::ru_RU),
"ru-UA" => Some(Locale::ru_UA),
"rw-RW" => Some(Locale::rw_RW),
"sa-IN" => Some(Locale::sa_IN),
"sah-RU" => Some(Locale::sah_RU),
"sat-IN" => Some(Locale::sat_IN),
"sc-IT" => Some(Locale::sc_IT),
"sd-IN" => Some(Locale::sd_IN),
"se-NO" => Some(Locale::se_NO),
"sgs-LT" => Some(Locale::sgs_LT),
"shn-MM" => Some(Locale::shn_MM),
"shs-CA" => Some(Locale::shs_CA),
"si-LK" => Some(Locale::si_LK),
"sid-ET" => Some(Locale::sid_ET),
"sk-SK" => Some(Locale::sk_SK),
"sl-SI" => Some(Locale::sl_SI),
"sm-WS" => Some(Locale::sm_WS),
"so-DJ" => Some(Locale::so_DJ),
"so-ET" => Some(Locale::so_ET),
"so-KE" => Some(Locale::so_KE),
"so-SO" => Some(Locale::so_SO),
"sq-AL" => Some(Locale::sq_AL),
"sq-MK" => Some(Locale::sq_MK),
"sr-ME" => Some(Locale::sr_ME),
"sr-RS" => Some(Locale::sr_RS),
"ss-ZA" => Some(Locale::ss_ZA),
"st-ZA" => Some(Locale::st_ZA),
"sv-FI" => Some(Locale::sv_FI),
"sv-SE" => Some(Locale::sv_SE),
"sw-KE" => Some(Locale::sw_KE),
"sw-TZ" => Some(Locale::sw_TZ),
"szl-PL" => Some(Locale::szl_PL),
"ta-IN" => Some(Locale::ta_IN),
"ta-LK" => Some(Locale::ta_LK),
"tcy-IN" => Some(Locale::tcy_IN),
"te-IN" => Some(Locale::te_IN),
"tg-TJ" => Some(Locale::tg_TJ),
"th-TH" => Some(Locale::th_TH),
"the-NP" => Some(Locale::the_NP),
"ti-ER" => Some(Locale::ti_ER),
"ti-ET" => Some(Locale::ti_ET),
"tig-ER" => Some(Locale::tig_ER),
"tk-TM" => Some(Locale::tk_TM),
"tl-PH" => Some(Locale::tl_PH),
"tn-ZA" => Some(Locale::tn_ZA),
"to-TO" => Some(Locale::to_TO),
"tpi-PG" => Some(Locale::tpi_PG),
"tr-CY" => Some(Locale::tr_CY),
"tr-TR" => Some(Locale::tr_TR),
"ts-ZA" => Some(Locale::ts_ZA),
"tt-RU" => Some(Locale::tt_RU),
"ug-CN" => Some(Locale::ug_CN),
"uk-UA" => Some(Locale::uk_UA),
"unm-US" => Some(Locale::unm_US),
"ur-IN" => Some(Locale::ur_IN),
"ur-PK" => Some(Locale::ur_PK),
"uz-UZ" => Some(Locale::uz_UZ),
"ve-ZA" => Some(Locale::ve_ZA),
"vi-VN" => Some(Locale::vi_VN),
"wa-BE" => Some(Locale::wa_BE),
"wae-CH" => Some(Locale::wae_CH),
"wal-ET" => Some(Locale::wal_ET),
"wo-SN" => Some(Locale::wo_SN),
"xh-ZA" => Some(Locale::xh_ZA),
"yi-US" => Some(Locale::yi_US),
"yo-NG" => Some(Locale::yo_NG),
"yue-HK" => Some(Locale::yue_HK),
"yuw-PG" => Some(Locale::yuw_PG),
"zh-CN" => Some(Locale::zh_CN),
"zh-HK" => Some(Locale::zh_HK),
"zh-SG" => Some(Locale::zh_SG),
"zh-TW" => Some(Locale::zh_TW),
"zu-ZA" => Some(Locale::zu_ZA),
_ => None,
}
}
pub struct DefaultLocaleProvider {}
impl LocaleProvider for DefaultLocaleProvider {
fn get_system_locale(&self) -> String {
sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"))
}
}
impl DefaultLocaleProvider {
pub fn new() -> Self {
Self {}
}
}
#[cfg(test)]
@ -79,9 +425,30 @@ mod tests {
use super::*;
use chrono::offset::TimeZone;
struct MockLocaleProvider {
locale: String,
}
impl LocaleProvider for MockLocaleProvider {
fn get_system_locale(&self) -> String {
self.locale.clone()
}
}
impl MockLocaleProvider {
pub fn new() -> Self {
Self {
locale: "en-US".to_string(),
}
}
pub fn new_with_locale(locale: String) -> Self {
Self { locale }
}
}
#[test]
fn date_formatted_correctly() {
let mut extension = DateExtension::new();
let locale_provider = MockLocaleProvider::new();
let mut extension = DateExtension::new(&locale_provider);
extension.fixed_date = Some(Local.ymd(2014, 7, 8).and_hms(9, 10, 11));
let param = vec![("format".to_string(), Value::String("%H:%M:%S".to_string()))]
@ -98,7 +465,8 @@ mod tests {
#[test]
fn offset_works_correctly() {
let mut extension = DateExtension::new();
let locale_provider = MockLocaleProvider::new();
let mut extension = DateExtension::new(&locale_provider);
extension.fixed_date = Some(Local.ymd(2014, 7, 8).and_hms(9, 10, 11));
let param = vec![
@ -115,4 +483,61 @@ mod tests {
ExtensionOutput::Single("10:10:11".to_string())
);
}
#[test]
fn default_locale_works_correctly() {
let locale_provider = MockLocaleProvider::new_with_locale("it-IT".to_string());
let mut extension = DateExtension::new(&locale_provider);
extension.fixed_date = Some(Local.ymd(2014, 7, 8).and_hms(9, 10, 11));
let param = vec![("format".to_string(), Value::String("%A".to_string()))]
.into_iter()
.collect::<Params>();
assert_eq!(
extension
.calculate(&Default::default(), &Default::default(), &param)
.into_success()
.unwrap(),
ExtensionOutput::Single("martedì".to_string())
);
}
#[test]
fn invalid_locale_should_default_to_en_us() {
let locale_provider = MockLocaleProvider::new_with_locale("invalid".to_string());
let mut extension = DateExtension::new(&locale_provider);
extension.fixed_date = Some(Local.ymd(2014, 7, 8).and_hms(9, 10, 11));
let param = vec![("format".to_string(), Value::String("%A".to_string()))]
.into_iter()
.collect::<Params>();
assert_eq!(
extension
.calculate(&Default::default(), &Default::default(), &param)
.into_success()
.unwrap(),
ExtensionOutput::Single("Tuesday".to_string())
);
}
#[test]
fn override_locale() {
let locale_provider = MockLocaleProvider::new();
let mut extension = DateExtension::new(&locale_provider);
extension.fixed_date = Some(Local.ymd(2014, 7, 8).and_hms(9, 10, 11));
let param = vec![
("format".to_string(), Value::String("%A".to_string())),
("locale".to_string(), Value::String("it-IT".to_string())),
]
.into_iter()
.collect::<Params>();
assert_eq!(
extension
.calculate(&Default::default(), &Default::default(), &param)
.into_success()
.unwrap(),
ExtensionOutput::Single("martedì".to_string())
);
}
}