///|
pub fn Catalog::from_json(
  locale : String,
  source : String,
) -> Catalog raise MessageError {
  let json = @json.parse(source) catch {
    error => raise InvalidCatalog(error.to_string())
  }
  let entries : Map[String, String] = @json.from_json(json) catch {
    _ =>
      raise InvalidCatalog(
        "Catalog must be a JSON object whose values are strings.",
      )
  }
  for key, template in entries {
    let _ = parse_message(template) catch {
      _ => raise InvalidCatalog("\{locale}/\{key}: invalid message template")
    }
  }
  { locale, entries }
}

///|
pub fn Catalog::locale(self : Catalog) -> String {
  self.locale
}

///|
pub fn Catalog::get(self : Catalog, key : String) -> String? {
  self.entries.get(key)
}

///|
pub fn Translator::new(
  default_locale : String,
  fallback_locale : String,
) -> Translator {
  {
    default_locale: canonicalize_locale_lossy(default_locale),
    fallback_locale: canonicalize_locale_lossy(fallback_locale),
    catalogs: Map([]),
  }
}

///|
pub fn Translator::add_catalog(self : Translator, catalog : Catalog) -> Unit {
  self.catalogs[canonicalize_locale_lossy(catalog.locale)] = catalog
}

///|
fn append_locale_chain(
  output : Array[String],
  seen : Map[String, Unit],
  locale : String,
) -> Unit {
  for candidate in locale_fallback_chain(locale) {
    if !seen.contains(candidate) {
      seen[candidate] = ()
      output.push(candidate)
    }
  }
}

///|
fn Translator::lookup_chain(
  self : Translator,
  locale : String,
) -> Array[String] {
  let output : Array[String] = []
  let seen : Map[String, Unit] = Map([])
  append_locale_chain(output, seen, locale)
  append_locale_chain(output, seen, self.default_locale)
  append_locale_chain(output, seen, self.fallback_locale)
  output
}

///|
/// Resolve a key and preserve the locale of the catalog that supplied it.
pub fn Translator::resolve(
  self : Translator,
  key : String,
  locale? : String,
) -> ResolvedMessage raise MessageError {
  let requested = canonicalize_locale_lossy(
    match locale {
      Some(value) => value
      None => self.default_locale
    },
  )
  for candidate in self.lookup_chain(requested) {
    match self.catalogs.get(candidate) {
      Some(catalog) =>
        match catalog.get(key) {
          Some(template) =>
            return {
              key,
              requested_locale: requested,
              resolved_locale: canonicalize_locale_lossy(catalog.locale),
              template,
              used_fallback: requested !=
              canonicalize_locale_lossy(catalog.locale),
            }
          None => ()
        }
      None => ()
    }
  }
  raise MissingChoice("message key: \{key}")
}

///|
/// Look up and format a key, trying requested, default, then fallback locale.
pub fn Translator::translate(
  self : Translator,
  key : String,
  arguments : Map[String, Argument],
  locale? : String,
) -> String raise MessageError {
  let requested = match locale {
    Some(value) => value
    None => self.default_locale
  }
  let resolved = self.resolve(key, locale=requested)
  format(parse_message(resolved.template), arguments, resolved.resolved_locale)
}

///|
/// Resolve, format, and return locale provenance for a translation.
pub fn Translator::translate_detailed(
  self : Translator,
  key : String,
  arguments : Map[String, Argument],
  locale? : String,
) -> TranslationResult raise MessageError {
  let resolved = match locale {
    Some(value) => self.resolve(key, locale=value)
    None => self.resolve(key)
  }
  let value = format(
    parse_message(resolved.template),
    arguments,
    resolved.resolved_locale,
  )
  {
    key,
    requested_locale: resolved.requested_locale,
    resolved_locale: resolved.resolved_locale,
    value,
    used_fallback: resolved.used_fallback,
  }
}

///|
pub fn Translator::default_locale(self : Translator) -> String {
  self.default_locale
}

///|
pub fn Translator::fallback_locale(self : Translator) -> String {
  self.fallback_locale
}

///|
pub fn Translator::has_catalog(self : Translator, locale : String) -> Bool {
  self.catalogs.contains(canonicalize_locale_lossy(locale))
}

///|
pub fn Translator::catalog_count(self : Translator) -> Int {
  self.catalogs.length()
}

///|
pub fn Translator::available_locales(self : Translator) -> Array[String] {
  let locales : Array[String] = []
  for locale, _ in self.catalogs {
    locales.push(locale)
  }
  locales.sort()
  locales
}

///|
pub fn ResolvedMessage::key(self : ResolvedMessage) -> String {
  self.key
}

///|
pub fn ResolvedMessage::requested_locale(self : ResolvedMessage) -> String {
  self.requested_locale
}

///|
pub fn ResolvedMessage::resolved_locale(self : ResolvedMessage) -> String {
  self.resolved_locale
}

///|
pub fn ResolvedMessage::template(self : ResolvedMessage) -> String {
  self.template
}

///|
pub fn ResolvedMessage::used_fallback(self : ResolvedMessage) -> Bool {
  self.used_fallback
}

///|
pub fn TranslationResult::key(self : TranslationResult) -> String {
  self.key
}

///|
pub fn TranslationResult::requested_locale(self : TranslationResult) -> String {
  self.requested_locale
}

///|
pub fn TranslationResult::resolved_locale(self : TranslationResult) -> String {
  self.resolved_locale
}

///|
pub fn TranslationResult::value(self : TranslationResult) -> String {
  self.value
}

///|
pub fn TranslationResult::used_fallback(self : TranslationResult) -> Bool {
  self.used_fallback
}