Main change is switching from individual String
allocations to a shared buffer, along with caching icu4x
formatters to reduce duplicated work.
RA3H7PWCI7WHONXB32IWYECQ3VKPKEXXYAJDIOHWNAGHDO7L5ZLQC
4MRF5E76QSW3EPICI6TNEGJ2KSBWODWMIDQPLYALDWBYWKAV5LJAC
P6FW2GGOW24UZZAWQ6IDDI66JBWTIY26TATMCIOETZ4GRRGGUI3AC
NO3PDO7PY7J3WPADNCS5VD6HKFY63E23I3SDR4DHXNVQJTG27RAAC
F5LG7WENUUDRSCTDMA4M6BAC5RWTGQO45C4ZEBZDX6FHCTTHBVGQC
HHJDRLLNN36UNIA7STAXEEVBCEMPJNB7SJQOS3TJLLYN4AEZ4MHQC
BFL2Y7GN6NBXXNAUSD4M6T6CIVQ2OLERPE2CAFSLRF377WFFTVCQC
KZLFC7OWYNK3G5YNHRANUK3VUVCM6W6J34N7UABYA24XMZWAVVHQC
7U2DXFMPZO4P53AMWYCVXG3EPB7UIAPEY4PDDINX4TTABHD5NGMQC
CESJ4CTO26X4GBZBPXRXLOJT3JQJOGFN5EJSNAAZELNQRZF7QSYAC
IRW6JACS3KVVA6HW5SBNBOHOQ2WRBHYGDND3FUWJYKJC7ZMOAVOQC
3NMKD6I57ONAGHEN4PZIAV2KPYESVR4JL3DTWSHXKCMVJBEQ4GIQC
C6W7N6N57UCNHEV55HEZ3G7WN2ZOBGMFBB5M5ZPDB2HNNHHTOPBQC
WWDZWJTRJWSLVFMQFHS7JMDPK5VNDIQ6IHSMES7BVKYHZY6WRYKAC
7M4UI3TWQIAA333GQ577HDWDWZPSZKWCYG556L6SBRLB6SZDQYPAC
7JPOCQEISAIOD7LV4JYBE6NNUWUKKNE73MEPQYTIZ7PP44ZAD2RAC
MABGENI7CW5F5D3BFUJ7BS2H7XPYG4F3UMWGDPFFSMCCZKUUDLDQC
LU6IFZFGPIKF3CBWZWITWVBSCYWF7Q4UXJDXVRWZ4XV7PKE5RSTQC
JUV7C6ET4ZQJNLO7B4JB7XMLV2YUBZB4ARJHN4JEPHDGGBWGLEVAC
BAH2JCJPTDXAE6XGSLIPBQZU4GQY65HI66Q4XTFNL65MV6VSNF2QC
U2PHMYPDFQQYTPDVVJLWDJM5G45ILXLWDDDTZVV2NBOSCED323MQC
QJC4IQITOQP65AFLA5CMH2EXHB6B3SOLW2XBV72U5ZQU2KOR2EIAC
QM64L3XOUB74M2D7TXDJWXGJNQN46IMF22Y24VNFQ5FEWODLVBLAC
NB7K77TZAT5ESYFZATMSGYPKOW3GGVWZLLNDAD4JKZEG7KUAX2HAC
6XEMHUGSNX5YSWZYM7PZUTTUMFODMGO74QLHGEXQ5LAC7LPS7JNQC
KFFAQIZUWCJGRHOPDYXZNZM5DESD6XYU4PK3YH7T25OIMRR6O2MQC
AE3AZFVKJBURLY6T6H5477BSP5LISUQYPSPDRSPXRO435KGYTRZAC
S26YOXQIUO3B7FCWZ33RI54OHFVXUDSFKBMVAAND3BW3H5WRGNRAC
RUCC2HKZZTUHN3G6IWS4NK3VYGXAI6PORJH2YZKPRAYSDWH63ESQC
USKESL6XR6C7676X3PO3SFFL5EMKMA7EQMPZAA72A7F7UZSONOIQC
7YOM2QEFZ5HWVEISP3VIR2GKF2NNH4KKTLTWEMYMMWNYKICXWTZAC
EKXWNEPK4FTYKT2RJL2L7HTM64VQGDD3DYD6NZIDGMMV6ITHUVZAC
PGBXJWIHSVTRD7CGDSCPC4YHI65EBKMQFEX62RZWL4EZB63622XAC
IZ67IMRIPBOYLOAR5WE5NYA7MHOT7TXXEE7WM63MU4JSH6OM7YQQC
BC22FLOQBQ6EOUSCN6THUXQDZYZPWSF6QJX5Z6WA4GYFR6BK4DZAC
XSRT5QWX3WE6RQC2Y3NTAEVV5GQDGDR6UCQMNW2EFLSNRRRKNPIAC
YUW3BUXXSWXHLNQNVDOCHSZ44G4NJ64T75DFOO6KKIFFTYXBBF7QC
AS7RDZT74V3SSWFSJYEGHT64QCEOFEZ46GUPF4ZAKEHTHBBNG7KQC
R2BAN2V6VS4OBNG6MK5BOUYQCJMGLD37IIE5RX4HXZLQ3C5AQRHAC
XDJBTEXUZNIAC2TKC4Z3OZORWAXR4ZWYUHNM6OEGXTL6WZDXOVZQC
2HHBS7VWRQRDNDCCSV3BJJJHA7LGN3L4CMKIP7URWQFXPI6QFNDQC
XPGOKS6XWM2Q2R74DDEHQRMZH6BWQH2FMQGAXBLO7FW52J3VFKSQC
Q7LUHXXBN3ACNMGT4O2SKY5EGDEJQXMVJGYETTKNWIUAZCQV6HGAC
LYOV6ZIRUE34ZJG6X6BVPZ6R4LLHLMMWC5FHVLGW73HIHQUHDJYQC
XGNME3WRU3MJDTFHUFJYARLVXWBZIH5ODBOIIFTXHNCBTZQH2R7QC
O77KA6C4UJGZXVGPEA7WCRQH6XYQJPWETSPDXI3VOKOSRQND7JEQC
5TEX4MNUC4LDDRMNEOVCFNUUEZAGUXMKO3OIEQFXWRQKXSHY2NRQC
VZYZRAO4EXCHW2LBVFG5ELSWG5SCNDREMJ6RKQ4EKQGI2T7SD3ZQC
UKFEFT6LSI4K7X6UHQFZYD52DILKXMZMYSO2UYS2FCHNPXIF4BEQC
pub fn new<L: Localize>(
environment: &'environment InteractionEnvironment,
prompt: L,
) -> Self {
let localized_prompt = prompt.localize_for(&environment.locale);
pub fn new<L: Localize>(environment: &'environment InteractionEnvironment, prompt: L) -> Self {
let mut localized_prompt = String::new();
prompt.localize(&environment.localization_context, &mut localized_prompt);
let confirmation_text = confirmation.localize_for(&self.environment.locale);
let mismatch_error_text = mismatch_error.localize_for(&self.environment.locale);
let mut localized_prompt = String::new();
confirmation_prompt.localize(
&self.environment.localization_context,
&mut localized_prompt,
);
let mut localized_mismatch_error = String::new();
mismatch_error.localize(
&self.environment.localization_context,
&mut localized_mismatch_error,
);
Err(message) => {
// Localize the error message
Err(message.localize_for(&locale))
Err(error_message) => {
let mut localized_error = String::new();
error_message.localize(&localization_context, &mut localized_error);
Err(localized_error)
pub fn new<L: Localize>(
environment: &'environment InteractionEnvironment,
prompt: L,
) -> Self {
let localized_prompt = prompt.localize_for(&environment.locale);
pub fn new<L: Localize>(environment: &'environment InteractionEnvironment, prompt: L) -> Self {
let mut localized_prompt = String::new();
prompt.localize(&environment.localization_context, &mut localized_prompt);
let localized_text = default.localize_for(&self.environment.locale);
self.default = Some(localized_text);
let mut localized_default = String::new();
default.localize(
&self.environment.localization_context,
&mut localized_default,
);
self.default = Some(localized_default);
self.progress_bars
.println(message.localize_for(&self.locale))?;
let mut localized_message = String::new();
message.localize(&self.localization_context, &mut localized_message);
self.progress_bars.println(localized_message)?;
let result = message.localize_for(&locale);
pretty_assertions::assert_eq!(result, expected.as_ref());
let context = Context::new(locale);
let mut buffer = String::new();
message.localize(&context, &mut buffer);
pretty_assertions::assert_eq!(expected.as_ref(), buffer);
let mut buffer = String::new();
// TODO: handle different rule types according to fluent code (not just cardinal)
// TODO: only generate this when needed in message
const PLURAL_RULE_TYPE: ::l10n_embed::macro_prelude::icu_plurals::PluralRuleType =
::l10n_embed::macro_prelude::icu_plurals::PluralRuleType::Cardinal;
let plural_options = ::l10n_embed::macro_prelude::icu_plurals::PluralRulesOptions::default()
.with_type(PLURAL_RULE_TYPE);
let plural_rules = ::l10n_embed::macro_prelude::icu_plurals::PluralRules::try_new(
locale.into(),
plural_options,
)
.unwrap();
quote!(plural_rules.category_for(#category_reference))
quote! {
{
// TODO: handle different rule types according to fluent code (not just cardinal)
let plural_rules = context.plural_rule(::l10n_embed::macro_prelude::icu_plurals::PluralRuleType::Cardinal).unwrap();
plural_rules.category_for(#category_reference)
}
}
use fixed_decimal::{Decimal, FloatPrecision};
use icu_experimental::relativetime::{
RelativeTimeFormatter, RelativeTimeFormatterOptions, options::Numeric,
};
use icu_locale::Locale;
use fixed_decimal::Decimal;
let formatter = match selected_unit {
Unit::Year => {
RelativeTimeFormatter::try_new_long_year(locale.into(), FORMATTER_OPTIONS)
}
Unit::Month => {
RelativeTimeFormatter::try_new_long_month(locale.into(), FORMATTER_OPTIONS)
}
Unit::Week => {
RelativeTimeFormatter::try_new_long_week(locale.into(), FORMATTER_OPTIONS)
}
Unit::Day => RelativeTimeFormatter::try_new_long_day(locale.into(), FORMATTER_OPTIONS),
Unit::Hour => {
RelativeTimeFormatter::try_new_long_hour(locale.into(), FORMATTER_OPTIONS)
}
Unit::Minute => {
RelativeTimeFormatter::try_new_long_minute(locale.into(), FORMATTER_OPTIONS)
}
Unit::Second => {
RelativeTimeFormatter::try_new_long_second(locale.into(), FORMATTER_OPTIONS)
}
_ => unreachable!(),
}
.unwrap();
let formatter = context.relative_time_formatter(selected_unit).unwrap();
fn localize_for(&self, locale: &icu_locale::Locale) -> String {
let message = self.message.localize_for(locale);
match self.style {
Some(style) => format!("{style}{message}{style:#}"),
None => message,
fn localize(&self, context: &Context, buffer: &mut String) {
// Prefix with the terminal color codes
if let Some(style) = self.style {
let prefix = style.render().to_string();
buffer.push_str(&prefix);
}
self.message.localize(context, buffer);
// Suffix with the terminal reset codes
if let Some(style) = self.style {
let suffix = style.render_reset().to_string();
buffer.push_str(&suffix);
fn localize_for(&self, locale: &Locale) -> String {
let list_formatter = ListFormatter::try_new_and(
locale.into(),
ListFormatterOptions::default().with_length(self.length),
)
.unwrap();
fn localize(&self, context: &Context, buffer: &mut String) {
let list_formatter = context.list_formatter(self.length);
let localized_messages = self.messages.iter().map(|message| {
let mut buffer = String::new();
message.localize(context, &mut buffer);
use icu_decimal::options::DecimalFormatterOptions;
use icu_decimal::{DecimalFormatter, DecimalFormatterPreferences};
use icu_experimental::relativetime::{
RelativeTimeFormatter, RelativeTimeFormatterOptions, RelativeTimeFormatterPreferences,
};
use icu_list::options::{ListFormatterOptions, ListLength};
use icu_list::{ListFormatter, ListFormatterPreferences};
fn localize_for(&self, locale: &Locale) -> String;
fn localize(&self, context: &Context, buffer: &mut String);
}
pub struct Context {
pub locale: Locale,
decimal_formatter: OnceLock<DecimalFormatter>,
list_formatters: [OnceLock<ListFormatter>; 3],
plural_rules: [OnceLock<PluralRules>; 2],
relative_time_formatters: [OnceLock<RelativeTimeFormatter>; 7],
}
impl Context {
pub fn new(locale: Locale) -> Self {
Self {
locale,
decimal_formatter: OnceLock::new(),
list_formatters: [const { OnceLock::new() }; 3],
plural_rules: [const { OnceLock::new() }; 2],
relative_time_formatters: [const { OnceLock::new() }; 7],
}
}
pub fn decimal_formatter(&self) -> &DecimalFormatter {
self.decimal_formatter.get_or_init(|| {
DecimalFormatter::try_new(
DecimalFormatterPreferences::from(&self.locale),
DecimalFormatterOptions::default(),
)
.unwrap()
})
}
pub fn list_formatter(&self, length: ListLength) -> &ListFormatter {
let index = match length {
ListLength::Wide => 0,
ListLength::Short => 1,
ListLength::Narrow => 2,
_ => unimplemented!(),
};
self.list_formatters[index].get_or_init(|| {
ListFormatter::try_new_and(
ListFormatterPreferences::from(&self.locale),
ListFormatterOptions::default().with_length(length),
)
.unwrap()
})
}
pub fn plural_rule(&self, rule_type: PluralRuleType) -> Option<&PluralRules> {
let index = match rule_type {
PluralRuleType::Cardinal => 0,
PluralRuleType::Ordinal => 1,
_ => return None,
};
let plural_rules = self.plural_rules[index].get_or_init(|| {
PluralRules::try_new(
PluralRulesPreferences::from(&self.locale),
PluralRulesOptions::default().with_type(rule_type),
)
.unwrap()
});
Some(plural_rules)
}
pub fn relative_time_formatter(&self, unit: jiff::Unit) -> Option<&RelativeTimeFormatter> {
let index = match unit {
jiff::Unit::Year => 0,
jiff::Unit::Month => 1,
jiff::Unit::Week => 2,
jiff::Unit::Day => 3,
jiff::Unit::Hour => 4,
jiff::Unit::Minute => 5,
jiff::Unit::Second => 6,
_ => return None,
};
let formatter = self.relative_time_formatters[index].get_or_init(|| {
let preferences = RelativeTimeFormatterPreferences::from(&self.locale);
const OPTIONS: RelativeTimeFormatterOptions = RelativeTimeFormatterOptions {
numeric: icu_experimental::relativetime::options::Numeric::Auto,
};
match unit {
jiff::Unit::Year => RelativeTimeFormatter::try_new_long_year(preferences, OPTIONS),
jiff::Unit::Month => {
RelativeTimeFormatter::try_new_long_month(preferences, OPTIONS)
}
jiff::Unit::Week => RelativeTimeFormatter::try_new_long_week(preferences, OPTIONS),
jiff::Unit::Day => RelativeTimeFormatter::try_new_long_day(preferences, OPTIONS),
jiff::Unit::Hour => RelativeTimeFormatter::try_new_long_hour(preferences, OPTIONS),
jiff::Unit::Minute => {
RelativeTimeFormatter::try_new_long_minute(preferences, OPTIONS)
}
jiff::Unit::Second => {
RelativeTimeFormatter::try_new_long_second(preferences, OPTIONS)
}
_ => unreachable!(),
}
.unwrap()
});
Some(formatter)
}
fn localize_for(&self, locale: &Locale) -> String {
let localized_items: Vec<String> = self
.messages
.iter()
.map(|item| item.localize_for(locale))
.collect();
fn localize(&self, context: &Context, buffer: &mut String) {
let separator = "\n".repeat(SEPARATOR_COUNT);
for (index, message) in self.messages.iter().enumerate() {
// Add the newlines before every additional line
if index > 0 {
buffer.push_str(&separator)
}
fn localize_for(&self, locale: &Locale) -> String {
let formatter =
DecimalFormatter::try_new(locale.into(), DecimalFormatterOptions::default()).unwrap();
fn localize(&self, context: &Context, buffer: &mut String) {
let formatter = context.decimal_formatter();
println!(
"Current time: {}",
current_timestamp.localize_for(&DEFAULT_LOCALE)
);
println!("Unix epoch: {}", unix_epoch.localize_for(&DEFAULT_LOCALE));
println!(
"Two hours from now: {}",
in_two_hours.localize_for(&DEFAULT_LOCALE)
);
println!(
"Since start of year (UTC): {}",
start_of_year.localize_for(&DEFAULT_LOCALE)
);
let timestamps: [(&str, Timestamp); 4] = [
("Current time", current_timestamp),
("Unix epoch", unix_epoch),
("Two hours from now", in_two_hours),
("Since start of year (UTC)", start_of_year),
];
let context = Context::new(locale!("en-US"));
let mut buffer = String::new();
for (name, timestamp) in timestamps {
buffer.clear();
timestamp.localize(&context, &mut buffer);
println!("{name}: {buffer}");
}
println!(
"Five million: {}",
five_million.localize_for(&DEFAULT_LOCALE)
);
println!(
"Static string: {}",
static_str.localize_for(&DEFAULT_LOCALE)
);
println!("Unix epoch: {}", unix_epoch.localize_for(&DEFAULT_LOCALE));
let styles: [(&str, Box<dyn Localize>); 3] = [
("Bold green", Box::new(bold_green)),
("Italic blue", Box::new(italic_blue)),
("Strikethrough red", Box::new(strikethrough_red)),
];
let context = Context::new(locale!("en-US"));
let mut buffer = String::new();
for (name, style) in styles {
buffer.clear();
style.localize(&context, &mut buffer);
println!("{name}: {buffer}");
}
println!(
"Static string: {}",
static_str.localize_for(&DEFAULT_LOCALE)
);
println!(
"Heap allocated string: {}",
heap_allocated_string.localize_for(&DEFAULT_LOCALE)
);
println!(
"Copy on write string: {}",
copy_on_write_str.localize_for(&DEFAULT_LOCALE)
);
let strings: [(&str, Box<dyn Localize>); 3] = [
("Static string", Box::new(static_str)),
("Heap allocated string", Box::new(heap_allocated_string)),
("Copy on write string", Box::new(copy_on_write_str)),
];
let context = Context::new(locale!("en-US"));
let mut buffer = String::new();
for (name, string) in strings {
buffer.clear();
string.localize(&context, &mut buffer);
println!("{name}: {buffer}");
}
println!(
"Current directory: {}",
current_directory.localize_for(&DEFAULT_LOCALE)
);
println!(
"Current directory (UTF-8): {}",
current_directory_utf8.localize_for(&DEFAULT_LOCALE)
);
let directories: [(&str, Box<dyn Localize>); 2] = [
("Current directory", Box::new(current_directory)),
(
"Current directory (UTF-8)",
Box::new(current_directory_utf8),
),
];
let context = Context::new(locale!("en-US"));
let mut buffer = String::new();
for (name, directory) in directories {
buffer.clear();
directory.localize(&context, &mut buffer);
println!("{name}: {buffer}");
}
println!("Narrow list: {}", narrow_list.localize_for(&DEFAULT_LOCALE));
println!("Short list: {}", short_list.localize_for(&DEFAULT_LOCALE));
println!("Wide list: {}", wide_list.localize_for(&DEFAULT_LOCALE));
let lists: [(&str, Box<dyn Localize>); 3] = [
("Narrow list", Box::new(narrow_list)),
("Short list", Box::new(short_list)),
("Wide list", Box::new(wide_list)),
];
let context = Context::new(locale!("en-US"));
let mut buffer = String::new();
for (name, list) in lists {
buffer.clear();
list.localize(&context, &mut buffer);
println!("{name}: {buffer}");
}
let layouts = [
(
"Single newline",
single_newline.localize_for(&DEFAULT_LOCALE),
),
(
"Double newlines",
double_newlines.localize_for(&DEFAULT_LOCALE),
),
(
"Vertical padding",
vertical_padding.localize_for(&DEFAULT_LOCALE),
),
(
"Horizontal padding",
horizontal_padding.localize_for(&DEFAULT_LOCALE),
),
(
"Combined padding",
combined_padding.localize_for(&DEFAULT_LOCALE),
),
(
"Padded newlines",
padded_newlines.localize_for(&DEFAULT_LOCALE),
),
let layouts: [(&str, Box<dyn Localize>); 6] = [
("Single newline", Box::new(single_newline)),
("Double newlines", Box::new(double_newlines)),
("Vertical padding", Box::new(vertical_padding)),
("Horizontal padding", Box::new(horizontal_padding)),
("Combined padding", Box::new(combined_padding)),
("Padded newlines", Box::new(padded_newlines)),
println!("Zero: {}", zero.localize_for(&DEFAULT_LOCALE));
println!(
"Five million: {}",
five_million.localize_for(&DEFAULT_LOCALE)
);
println!(
"Zero point two: {}",
zero_point_two.localize_for(&DEFAULT_LOCALE)
);
println!(
"Negative two point five: {}",
negative_two_point_five.localize_for(&DEFAULT_LOCALE)
);
let numbers: [(&str, Box<dyn Localize>); 4] = [
("Zero", Box::new(zero)),
("Five million", Box::new(five_million)),
("Zero point two", Box::new(zero_point_two)),
("Negative two point five", Box::new(negative_two_point_five)),
];
for (name, number) in numbers {
buffer.clear();
number.localize(&context, &mut buffer);
println!("{name}: {buffer}");
}
writeable.workspace = true
writeable = "0.6"