///|
/// An uncompiled message resource supplied by an application.
///
/// Catalog compilation parses every source string up front, so syntax errors
/// are reported during application initialization rather than during a request.
pub struct MessageResource {
key : String
source : String
} derive(Eq, Debug)
///|
pub fn MessageResource::new(key : String, source : String) -> MessageResource {
{ key, source }
}
///|
pub fn message_resource(key : String, source : String) -> MessageResource {
{ key, source }
}
///|
pub fn MessageResource::key(self : MessageResource) -> String {
self.key
}
///|
pub fn MessageResource::source(self : MessageResource) -> String {
self.source
}
///|
/// A parsed message associated with its stable application key.
pub struct CatalogMessage {
key : String
template : MessageTemplate
} derive(Eq, Debug)
///|
pub fn CatalogMessage::key(self : CatalogMessage) -> String {
self.key
}
///|
pub fn CatalogMessage::template(self : CatalogMessage) -> MessageTemplate {
self.template
}
///|
/// Errors found while compiling one locale's message resources.
pub(all) enum CatalogCompileError {
EmptyMessageKey(Int)
DuplicateMessageKey(String)
InvalidMessage(String, MessageParseError)
} derive(Eq, Debug)
///|
pub fn CatalogCompileError::message(self : CatalogCompileError) -> String {
match self {
EmptyMessageKey(index) => "message key at index \{index} must not be empty"
DuplicateMessageKey(key) => "duplicate message key: \{key}"
InvalidMessage(key, error) => "invalid message '\{key}': \{error.message()}"
}
}
///|
/// An immutable collection of compiled messages for one locale.
pub struct MessageCatalog {
locale : Locale
messages : Array[CatalogMessage]
} derive(Eq, Debug)
///|
fn catalog_has_key(messages : Array[CatalogMessage], key : String) -> Bool {
for message in messages {
if message.key == key {
return true
}
}
false
}
///|
/// Parses and validates all messages for a locale.
pub fn MessageCatalog::compile(
locale : Locale,
resources : Array[MessageResource],
) -> Result[MessageCatalog, CatalogCompileError] {
let messages : Array[CatalogMessage] = []
for index, resource in resources {
if resource.key.length() == 0 {
return Err(EmptyMessageKey(index))
}
if catalog_has_key(messages, resource.key) {
return Err(DuplicateMessageKey(resource.key))
}
let template = match MessageTemplate::parse(resource.source) {
Ok(value) => value
Err(error) => return Err(InvalidMessage(resource.key, error))
}
messages.push({ key: resource.key, template })
}
Ok({ locale, messages })
}
///|
pub fn MessageCatalog::locale(self : MessageCatalog) -> Locale {
self.locale
}
///|
pub fn MessageCatalog::length(self : MessageCatalog) -> Int {
self.messages.length()
}
///|
pub fn MessageCatalog::is_empty(self : MessageCatalog) -> Bool {
self.messages.is_empty()
}
///|
pub fn MessageCatalog::keys(self : MessageCatalog) -> Array[String] {
self.messages.map(fn(message) { message.key })
}
///|
pub fn MessageCatalog::contains(self : MessageCatalog, key : String) -> Bool {
catalog_has_key(self.messages, key)
}
///|
pub fn MessageCatalog::get(
self : MessageCatalog,
key : String,
) -> MessageTemplate? {
for message in self.messages {
if message.key == key {
return Some(message.template)
}
}
None
}
///|
/// Errors found while combining locale catalogs into one bundle.
pub(all) enum BundleBuildError {
EmptyBundle
DuplicateCatalog(String)
MissingDefaultCatalog(String)
} derive(Eq, Debug)
///|
pub fn BundleBuildError::message(self : BundleBuildError) -> String {
match self {
EmptyBundle => "a message bundle must contain at least one catalog"
DuplicateCatalog(tag) => "duplicate locale catalog: \{tag}"
MissingDefaultCatalog(tag) => "default locale catalog is missing: \{tag}"
}
}
///|
/// A message together with the locale that ultimately supplied it.
pub struct ResolvedMessage {
requested : Locale
resolved : Locale
key : String
template : MessageTemplate
} derive(Eq, Debug)
///|
pub fn ResolvedMessage::requested_locale(self : ResolvedMessage) -> Locale {
self.requested
}
///|
pub fn ResolvedMessage::resolved_locale(self : ResolvedMessage) -> Locale {
self.resolved
}
///|
pub fn ResolvedMessage::key(self : ResolvedMessage) -> String {
self.key
}
///|
pub fn ResolvedMessage::template(self : ResolvedMessage) -> MessageTemplate {
self.template
}
///|
/// Failures produced while resolving and formatting a bundled message.
pub(all) enum BundleFormatError {
MissingMessage(String, Array[String])
MessageFormattingFailed(String, String, MessageFormatError)
} derive(Eq, Debug)
///|
pub fn BundleFormatError::message(self : BundleFormatError) -> String {
match self {
MissingMessage(key, attempted) =>
"message '\{key}' was not found; tried: \{attempted.join(", ")}"
MessageFormattingFailed(key, tag, error) =>
"failed to format message '\{key}' for \{tag}: \{error.message()}"
}
}
///|
/// A set of locale catalogs with deterministic per-message fallback.
///
/// Resolution tries the request's exact fallback chain first, then the closest
/// catalog in the same language, and finally the default locale. If a catalog
/// exists but lacks the requested key, resolution continues to the next
/// candidate. This allows regional catalogs to override only selected strings.
pub struct MessageBundle {
default_locale : Locale
catalogs : Array[MessageCatalog]
} derive(Eq, Debug)
///|
fn find_catalog(
catalogs : Array[MessageCatalog],
locale : Locale,
) -> MessageCatalog? {
for catalog in catalogs {
if catalog.locale == locale {
return Some(catalog)
}
}
None
}
///|
fn locale_was_tried(tried : Array[Locale], locale : Locale) -> Bool {
for current in tried {
if current == locale {
return true
}
}
false
}
///|
fn push_locale_once(locales : Array[Locale], locale : Locale) -> Unit {
if !locale_was_tried(locales, locale) {
locales.push(locale)
}
}
///|
pub fn MessageBundle::new(
default_locale : Locale,
catalogs : Array[MessageCatalog],
) -> Result[MessageBundle, BundleBuildError] {
if catalogs.is_empty() {
return Err(EmptyBundle)
}
for index, catalog in catalogs {
for previous = 0; previous < index; previous = previous + 1 {
if catalogs[previous].locale == catalog.locale {
return Err(DuplicateCatalog(catalog.locale.tag()))
}
}
}
match find_catalog(catalogs, default_locale) {
None => Err(MissingDefaultCatalog(default_locale.tag()))
Some(_) => Ok({ default_locale, catalogs })
}
}
///|
pub fn MessageBundle::default_locale(self : MessageBundle) -> Locale {
self.default_locale
}
///|
pub fn MessageBundle::supported_locales(self : MessageBundle) -> Array[Locale] {
self.catalogs.map(fn(catalog) { catalog.locale })
}
///|
pub fn MessageBundle::catalog_count(self : MessageBundle) -> Int {
self.catalogs.length()
}
///|
fn MessageBundle::resolution_order(
self : MessageBundle,
requested : Locale,
) -> Array[Locale] {
let order : Array[Locale] = []
for locale in requested.fallback_chain() {
match find_catalog(self.catalogs, locale) {
Some(_) => push_locale_once(order, locale)
None => ()
}
}
match best_locale(requested, self.supported_locales()) {
Some(locale) => push_locale_once(order, locale)
None => ()
}
for locale in self.default_locale.fallback_chain() {
match find_catalog(self.catalogs, locale) {
Some(_) => push_locale_once(order, locale)
None => ()
}
}
push_locale_once(order, self.default_locale)
order
}
///|
/// Resolves a key and reports which locale supplied the message.
pub fn MessageBundle::resolve(
self : MessageBundle,
requested : Locale,
key : String,
) -> Result[ResolvedMessage, BundleFormatError] {
let attempted : Array[String] = []
for locale in self.resolution_order(requested) {
attempted.push(locale.tag())
match find_catalog(self.catalogs, locale) {
Some(catalog) =>
match catalog.get(key) {
Some(template) =>
return Ok({ requested, resolved: locale, key, template })
None => ()
}
None => ()
}
}
Err(MissingMessage(key, attempted))
}
///|
/// Resolves and formats one localized message.
pub fn MessageBundle::format(
self : MessageBundle,
requested : Locale,
key : String,
arguments : Array[MessageArgument],
) -> Result[String, BundleFormatError] {
let resolved = match self.resolve(requested, key) {
Ok(value) => value
Err(error) => return Err(error)
}
match resolved.template.format(resolved.resolved, arguments) {
Ok(value) => Ok(value)
Err(error) =>
Err(MessageFormattingFailed(key, resolved.resolved.tag(), error))
}
}