///|
/// A runtime value supplied to a localized message.
pub(all) enum MessageValue {
StringValue(String)
IntValue(Int)
DecimalValue(Decimal)
BoolValue(Bool)
} derive(Eq, Debug)
///|
pub fn MessageValue::from_string(value : String) -> MessageValue {
StringValue(value)
}
///|
pub fn MessageValue::from_int(value : Int) -> MessageValue {
IntValue(value)
}
///|
pub fn MessageValue::from_decimal(value : Decimal) -> MessageValue {
DecimalValue(value)
}
///|
pub fn MessageValue::from_bool(value : Bool) -> MessageValue {
BoolValue(value)
}
///|
/// A named value passed to `MessageTemplate::format`.
pub struct MessageArgument {
name : String
value : MessageValue
} derive(Eq, Debug)
///|
pub fn MessageArgument::new(
name : String,
value : MessageValue,
) -> MessageArgument {
{ name, value }
}
///|
pub fn string_argument(name : String, value : String) -> MessageArgument {
{ name, value: StringValue(value) }
}
///|
pub fn int_argument(name : String, value : Int) -> MessageArgument {
{ name, value: IntValue(value) }
}
///|
pub fn decimal_argument(
name : String,
coefficient : Int,
scale : Int,
) -> MessageArgument {
{ name, value: DecimalValue(Decimal::new(coefficient, scale)) }
}
///|
pub fn bool_argument(name : String, value : Bool) -> MessageArgument {
{ name, value: BoolValue(value) }
}
///|
pub fn MessageArgument::name(self : MessageArgument) -> String {
self.name
}
///|
pub fn MessageArgument::value(self : MessageArgument) -> MessageValue {
self.value
}
///|
/// Errors produced while rendering an otherwise valid message template.
pub(all) enum MessageFormatError {
MissingArgument(String)
DuplicateArgument(String)
ExpectedNumber(String)
ExpectedSelector(String)
NoMatchingCase(String)
} derive(Eq, Debug)
///|
pub fn MessageFormatError::message(self : MessageFormatError) -> String {
match self {
MissingArgument(name) => "missing message argument: \{name}"
DuplicateArgument(name) => "duplicate message argument: \{name}"
ExpectedNumber(name) => "message argument '\{name}' must be an integer"
ExpectedSelector(name) =>
"message argument '\{name}' cannot be used as a select key"
NoMatchingCase(name) =>
"message argument '\{name}' has no matching or other case"
}
}
///|
priv struct MessageRenderContext {
locale : Locale
arguments : Array[MessageArgument]
number_formatter : NumberFormatter
}
///|
fn validate_message_arguments(
arguments : Array[MessageArgument],
) -> Result[Unit, MessageFormatError] {
for index, argument in arguments {
for previous = 0; previous < index; previous = previous + 1 {
if arguments[previous].name == argument.name {
return Err(DuplicateArgument(argument.name))
}
}
}
Ok(())
}
///|
fn find_message_value(
arguments : Array[MessageArgument],
name : String,
) -> MessageValue? {
for argument in arguments {
if argument.name == name {
return Some(argument.value)
}
}
None
}
///|
fn MessageRenderContext::require(
self : MessageRenderContext,
name : String,
) -> Result[MessageValue, MessageFormatError] {
match find_message_value(self.arguments, name) {
Some(value) => Ok(value)
None => Err(MissingArgument(name))
}
}
///|
fn render_plain_value(
context : MessageRenderContext,
value : MessageValue,
) -> String {
match value {
StringValue(text) => text
IntValue(number) => context.number_formatter.format_int(number)
DecimalValue(decimal) =>
DecimalFormatter::new(context.locale).format(decimal)
BoolValue(true) => "true"
BoolValue(false) => "false"
}
}
///|
fn select_key(
name : String,
value : MessageValue,
) -> Result[String, MessageFormatError] {
match value {
StringValue(text) => Ok(text)
IntValue(number) => Ok(number.to_string())
BoolValue(true) => Ok("true")
BoolValue(false) => Ok("false")
DecimalValue(_) => Err(ExpectedSelector(name))
}
}
///|
fn find_message_branch(
cases : Array[MessageCase],
selector : String,
) -> MessageCase? {
for branch in cases {
if branch.selector == selector {
return Some(branch)
}
}
None
}
///|
fn select_message_branch(
cases : Array[MessageCase],
selector : String,
) -> MessageCase? {
match find_message_branch(cases, selector) {
Some(branch) => Some(branch)
None => find_message_branch(cases, "other")
}
}
///|
fn replace_plural_marker(text : String, replacement : String) -> String {
text.replace_all(old="#", new=replacement)
}
///|
fn render_message_nodes(
context : MessageRenderContext,
nodes : Array[MessageNode],
plural_number : Int?,
) -> Result[String, MessageFormatError] {
let output = StringBuilder::new()
for node in nodes {
match node {
Text(text) =>
match plural_number {
Some(number) =>
output.write_string(
replace_plural_marker(
text,
context.number_formatter.format_int(number),
),
)
None => output.write_string(text)
}
Argument(name) =>
match context.require(name) {
Ok(value) => output.write_string(render_plain_value(context, value))
Err(error) => return Err(error)
}
Select(name, cases) => {
let value = match context.require(name) {
Ok(found) => found
Err(error) => return Err(error)
}
let key = match select_key(name, value) {
Ok(found) => found
Err(error) => return Err(error)
}
let branch = match select_message_branch(cases, key) {
Some(found) => found
None => return Err(NoMatchingCase(name))
}
match render_message_nodes(context, branch.nodes, plural_number) {
Ok(rendered) => output.write_string(rendered)
Err(error) => return Err(error)
}
}
Plural(name, offset, kind, cases) => {
let value = match context.require(name) {
Ok(IntValue(number)) => number
Ok(_) => return Err(ExpectedNumber(name))
Err(error) => return Err(error)
}
let exact_selector = "=\{value}"
let branch = match find_message_branch(cases, exact_selector) {
Some(found) => Some(found)
None => {
let category = PluralRules::new(context.locale, kind~)
.select(value - offset)
.name()
select_message_branch(cases, category)
}
}
match branch {
None => return Err(NoMatchingCase(name))
Some(found) =>
match
render_message_nodes(context, found.nodes, Some(value - offset)) {
Ok(rendered) => output.write_string(rendered)
Err(error) => return Err(error)
}
}
}
}
}
Ok(output.to_string())
}
///|
/// Renders this template using locale-aware plural rules and numbers.
pub fn MessageTemplate::format(
self : MessageTemplate,
locale : Locale,
arguments : Array[MessageArgument],
) -> Result[String, MessageFormatError] {
match validate_message_arguments(arguments) {
Err(error) => return Err(error)
Ok(_) => ()
}
let context : MessageRenderContext = {
locale,
arguments,
number_formatter: NumberFormatter::new(locale),
}
render_message_nodes(context, self.nodes, None)
}
///|
/// Parses and formats a one-off template.
///
/// Applications that reuse a message should parse it once and retain the
/// `MessageTemplate` instead.
pub fn format_message(
locale : Locale,
source : String,
arguments : Array[MessageArgument],
) -> Result[String, String] {
match MessageTemplate::parse(source) {
Err(error) => Err(error.message())
Ok(template) =>
match template.format(locale, arguments) {
Ok(value) => Ok(value)
Err(error) => Err(error.message())
}
}
}