///|
fn decode_catalog_entries(
source : String,
) -> Map[String, String] raise MessageError {
let json = @json.parse(source) catch {
error => raise InvalidCatalog(error.to_string())
}
@json.from_json(json) catch {
_ =>
raise InvalidCatalog(
"Catalog must be a JSON object whose values are strings.",
)
}
}
///|
fn copy_entries(entries : Map[String, String]) -> Map[String, String] {
let output : Map[String, String] = Map([])
for key, value in entries {
output[key] = value
}
output
}
///|
/// Load a catalog without rejecting invalid message templates.
///
/// This is intended for validators and editors that need to report every
/// malformed entry in one pass. Applications should normally use `from_json`.
pub fn Catalog::from_json_lenient(
locale : String,
source : String,
) -> Catalog raise MessageError {
{ locale, entries: decode_catalog_entries(source) }
}
///|
pub fn Catalog::length(self : Catalog) -> Int {
self.entries.length()
}
///|
pub fn Catalog::is_empty(self : Catalog) -> Bool {
self.entries.length() == 0
}
///|
pub fn Catalog::contains(self : Catalog, key : String) -> Bool {
self.entries.contains(key)
}
///|
pub fn Catalog::keys(self : Catalog) -> Array[String] {
let keys : Array[String] = []
for key, _ in self.entries {
keys.push(key)
}
keys.sort()
keys
}
///|
pub fn Catalog::values(self : Catalog) -> Array[String] {
let values : Array[String] = []
for key in self.keys() {
values.push(self.entries[key])
}
values
}
///|
pub fn Catalog::entries(self : Catalog) -> Map[String, String] {
copy_entries(self.entries)
}
///|
pub fn Catalog::parsed(self : Catalog, key : String) -> Message? {
match self.entries.get(key) {
Some(template) =>
try parse_message(template) catch {
_ => None
} noraise {
message => Some(message)
}
None => None
}
}
///|
pub fn Catalog::valid_keys(self : Catalog) -> Array[String] {
let keys : Array[String] = []
for key in self.keys() {
if diagnose_message(self.entries[key]) is None {
keys.push(key)
}
}
keys
}
///|
pub fn Catalog::invalid_keys(self : Catalog) -> Array[String] {
let keys : Array[String] = []
for key in self.keys() {
if diagnose_message(self.entries[key]) is Some(_) {
keys.push(key)
}
}
keys
}
///|
pub fn Catalog::entry_analysis(
self : Catalog,
key : String,
) -> CatalogEntryAnalysis? {
match self.entries.get(key) {
None => None
Some(template) =>
match diagnose_message(template) {
Some(diagnostic) =>
Some({
key,
template,
valid: false,
diagnostic: Some(diagnostic),
message: None,
})
None => {
let message = try! parse_message(template)
Some({
key,
template,
valid: true,
diagnostic: None,
message: Some(message.analyze()),
})
}
}
}
}
///|
pub fn Catalog::analyses(self : Catalog) -> Array[CatalogEntryAnalysis] {
let output : Array[CatalogEntryAnalysis] = []
for key in self.keys() {
match self.entry_analysis(key) {
Some(value) => output.push(value)
None => ()
}
}
output
}
///|
pub fn CatalogEntryAnalysis::key(self : CatalogEntryAnalysis) -> String {
self.key
}
///|
pub fn CatalogEntryAnalysis::template(self : CatalogEntryAnalysis) -> String {
self.template
}
///|
pub fn CatalogEntryAnalysis::valid(self : CatalogEntryAnalysis) -> Bool {
self.valid
}
///|
pub fn CatalogEntryAnalysis::diagnostic(
self : CatalogEntryAnalysis,
) -> MessageDiagnostic? {
self.diagnostic
}
///|
pub fn CatalogEntryAnalysis::message(
self : CatalogEntryAnalysis,
) -> MessageAnalysis? {
self.message
}
///|
fn count_catalog_arguments(catalog : Catalog) -> Int {
let arguments : Map[String, Unit] = Map([])
for analysis in catalog.analyses() {
match analysis.message {
Some(message) =>
for argument in message.arguments {
arguments[argument.name] = ()
}
None => ()
}
}
arguments.length()
}
///|
fn count_catalog_choices(catalog : Catalog) -> Int {
let mut count = 0
for analysis in catalog.analyses() {
match analysis.message {
Some(message) => count += message.choice_node_count
None => ()
}
}
count
}
///|
/// Compute aggregate catalog statistics.
pub fn Catalog::stats(self : Catalog) -> CatalogStats {
let invalid = self.invalid_keys().length()
{
locale: self.locale,
key_count: self.entries.length(),
template_count: self.entries.length(),
valid_template_count: self.entries.length() - invalid,
invalid_template_count: invalid,
argument_count: count_catalog_arguments(self),
choice_count: count_catalog_choices(self),
}
}
///|
pub fn CatalogStats::locale(self : CatalogStats) -> String {
self.locale
}
///|
pub fn CatalogStats::key_count(self : CatalogStats) -> Int {
self.key_count
}
///|
pub fn CatalogStats::template_count(self : CatalogStats) -> Int {
self.template_count
}
///|
pub fn CatalogStats::valid_template_count(self : CatalogStats) -> Int {
self.valid_template_count
}
///|
pub fn CatalogStats::invalid_template_count(self : CatalogStats) -> Int {
self.invalid_template_count
}
///|
pub fn CatalogStats::argument_count(self : CatalogStats) -> Int {
self.argument_count
}
///|
pub fn CatalogStats::choice_count(self : CatalogStats) -> Int {
self.choice_count
}
///|
pub fn CatalogStats::summary(self : CatalogStats) -> String {
"\{self.locale}: \{self.key_count} keys, \{self.valid_template_count} valid, \{self.invalid_template_count} invalid, \{self.argument_count} arguments, \{self.choice_count} choices"
}
///|
/// Compare the key sets of two catalogs.
pub fn diff_catalogs(reference : Catalog, translation : Catalog) -> CatalogDiff {
let shared_keys : Array[String] = []
let missing_keys : Array[String] = []
let extra_keys : Array[String] = []
for key in reference.keys() {
if translation.contains(key) {
shared_keys.push(key)
} else {
missing_keys.push(key)
}
}
for key in translation.keys() {
if !reference.contains(key) {
extra_keys.push(key)
}
}
{
reference_locale: reference.locale,
translation_locale: translation.locale,
shared_keys,
missing_keys,
extra_keys,
}
}
///|
pub fn CatalogDiff::reference_locale(self : CatalogDiff) -> String {
self.reference_locale
}
///|
pub fn CatalogDiff::translation_locale(self : CatalogDiff) -> String {
self.translation_locale
}
///|
pub fn CatalogDiff::shared_keys(self : CatalogDiff) -> Array[String] {
self.shared_keys
}
///|
pub fn CatalogDiff::missing_keys(self : CatalogDiff) -> Array[String] {
self.missing_keys
}
///|
pub fn CatalogDiff::extra_keys(self : CatalogDiff) -> Array[String] {
self.extra_keys
}
///|
pub fn CatalogDiff::is_equal(self : CatalogDiff) -> Bool {
self.missing_keys.length() == 0 && self.extra_keys.length() == 0
}
///|
pub fn CatalogDiff::coverage(self : CatalogDiff) -> CatalogCoverage {
let reference_keys = self.shared_keys.length() + self.missing_keys.length()
let translated_keys = self.shared_keys.length()
let coverage_percent = if reference_keys == 0 {
100
} else {
translated_keys * 100 / reference_keys
}
{
reference_keys,
translated_keys,
missing_keys: self.missing_keys.length(),
extra_keys: self.extra_keys.length(),
coverage_percent,
}
}
///|
pub fn CatalogCoverage::reference_keys(self : CatalogCoverage) -> Int {
self.reference_keys
}
///|
pub fn CatalogCoverage::translated_keys(self : CatalogCoverage) -> Int {
self.translated_keys
}
///|
pub fn CatalogCoverage::missing_keys(self : CatalogCoverage) -> Int {
self.missing_keys
}
///|
pub fn CatalogCoverage::extra_keys(self : CatalogCoverage) -> Int {
self.extra_keys
}
///|
pub fn CatalogCoverage::coverage_percent(self : CatalogCoverage) -> Int {
self.coverage_percent
}
///|
pub fn CatalogCoverage::summary(self : CatalogCoverage) -> String {
"\{self.coverage_percent}% (\{self.translated_keys}/\{self.reference_keys}), \{self.missing_keys} missing, \{self.extra_keys} extra"
}
///|
pub fn CatalogBuilder::new(locale : String) -> CatalogBuilder {
{ locale, entries: Map([]) }
}
///|
pub fn CatalogBuilder::from_catalog(catalog : Catalog) -> CatalogBuilder {
{ locale: catalog.locale, entries: copy_entries(catalog.entries) }
}
///|
pub fn CatalogBuilder::locale(self : CatalogBuilder) -> String {
self.locale
}
///|
pub fn CatalogBuilder::length(self : CatalogBuilder) -> Int {
self.entries.length()
}
///|
pub fn CatalogBuilder::contains(self : CatalogBuilder, key : String) -> Bool {
self.entries.contains(key)
}
///|
pub fn CatalogBuilder::set(
self : CatalogBuilder,
key : String,
template : String,
) -> CatalogBuilder {
self.entries[key] = template
self
}
///|
pub fn CatalogBuilder::remove(
self : CatalogBuilder,
key : String,
) -> CatalogBuilder {
ignore(self.entries.remove(key))
self
}
///|
pub fn CatalogBuilder::clear(self : CatalogBuilder) -> CatalogBuilder {
self.entries.clear()
self
}
///|
pub fn CatalogBuilder::build(
self : CatalogBuilder,
validate? : Bool = true,
) -> Catalog raise MessageError {
let catalog : Catalog = {
locale: self.locale,
entries: copy_entries(self.entries),
}
if validate {
for key, template in catalog.entries {
match diagnose_message(template) {
Some(diagnostic) =>
raise InvalidCatalog(
"\{catalog.locale}/\{key}: \{diagnostic.display()}",
)
None => ()
}
}
}
catalog
}
///|
pub fn Catalog::with_entry(
self : Catalog,
key : String,
template : String,
) -> Catalog raise MessageError {
CatalogBuilder::from_catalog(self).set(key, template).build()
}
///|
pub fn Catalog::without_entry(self : Catalog, key : String) -> Catalog {
try! CatalogBuilder::from_catalog(self).remove(key).build(validate=false)
}
///|
pub fn Catalog::subset(self : Catalog, prefix : String) -> Catalog {
let builder = CatalogBuilder::new(self.locale)
for key, template in self.entries {
if key.has_prefix(prefix) {
ignore(builder.set(key, template))
}
}
try! builder.build(validate=false)
}
///|
pub fn Catalog::merge(
self : Catalog,
other : Catalog,
policy? : CatalogMergePolicy = ReplaceExisting,
) -> Catalog raise MessageError {
let builder = CatalogBuilder::from_catalog(self)
for key, template in other.entries {
if builder.contains(key) {
match policy {
KeepExisting => ()
ReplaceExisting => ignore(builder.set(key, template))
RejectDuplicate =>
raise InvalidCatalog("Duplicate catalog key '\{key}'.")
}
} else {
ignore(builder.set(key, template))
}
}
builder.build()
}
///|
fn write_json_string(output : StringBuilder, value : String) -> Unit {
output.write_char('"')
for ch in value {
match ch {
'"' => output.write_string("\\\"")
'\\' => output.write_string("\\\\")
'\n' => output.write_string("\\n")
'\r' => output.write_string("\\r")
'\t' => output.write_string("\\t")
_ => output.write_char(ch)
}
}
output.write_char('"')
}
///|
/// Serialize a catalog deterministically with keys in lexical order.
pub fn Catalog::to_json(self : Catalog, pretty? : Bool = false) -> String {
let output = StringBuilder::new()
let keys = self.keys()
output.write_char('{')
if pretty && keys.length() > 0 {
output.write_char('\n')
}
for index, key in keys {
if index > 0 {
output.write_char(',')
if pretty {
output.write_char('\n')
}
}
if pretty {
output.write_string(" ")
}
write_json_string(output, key)
output.write_char(':')
if pretty {
output.write_char(' ')
}
write_json_string(output, self.entries[key])
}
if pretty && keys.length() > 0 {
output.write_char('\n')
}
output.write_char('}')
output.to_string()
}