///|
priv struct MoPair {
original : Bytes
translation : Bytes
}
///|
fn mo_original_text(entry : PoEntry) -> String {
let contextual = match entry.context {
Some(context) => context + "\u{0004}" + entry.msgid
None => entry.msgid
}
match entry.msgid_plural {
Some(plural) => contextual + "\u{0000}" + plural
None => contextual
}
}
///|
fn mo_translation_text(entry : PoEntry) -> String {
entry.translations.join("\u{0000}")
}
///|
fn entry_has_mo_translation(entry : PoEntry) -> Bool {
if entry.is_header() {
return true
}
for value in entry.translations {
if value != "" {
return true
}
}
false
}
///|
fn collect_mo_pairs(document : PoFile) -> Array[MoPair] raise GettextError {
let pairs : Array[MoPair] = []
let seen : Map[String, Unit] = Map([])
for entry in document.entries {
if entry.obsolete || entry.is_fuzzy() || !entry_has_mo_translation(entry) {
continue
}
let original_text = mo_original_text(entry)
if seen.contains(original_text) {
raise Validation(message="duplicate MO key: \{entry.msgid}")
}
seen[original_text] = ()
pairs.push({
original: @utf8.encode(original_text),
translation: @utf8.encode(mo_translation_text(entry)),
})
}
pairs.sort_by((left, right) => left.original.lexical_compare(right.original))
pairs
}
///|
fn write_u32_at(
buffer : Array[Byte],
offset : Int,
value : Int,
endian : Endian,
) -> Unit {
let byte0 = value.to_byte()
let byte1 = (value >> 8).to_byte()
let byte2 = (value >> 16).to_byte()
let byte3 = (value >> 24).to_byte()
match endian {
Little => {
buffer[offset] = byte0
buffer[offset + 1] = byte1
buffer[offset + 2] = byte2
buffer[offset + 3] = byte3
}
Big => {
buffer[offset] = byte3
buffer[offset + 1] = byte2
buffer[offset + 2] = byte1
buffer[offset + 3] = byte0
}
}
}
///|
fn write_mo_magic(buffer : Array[Byte], endian : Endian) -> Unit {
match endian {
Little => {
buffer[0] = b'\xde'
buffer[1] = b'\x12'
buffer[2] = b'\x04'
buffer[3] = b'\x95'
}
Big => {
buffer[0] = b'\x95'
buffer[1] = b'\x04'
buffer[2] = b'\x12'
buffer[3] = b'\xde'
}
}
}
///|
/// Compile a PO document into GNU MO revision 0 bytes.
///
/// Obsolete, fuzzy, and untranslated non-header entries are deliberately
/// omitted, matching the behavior expected from a production message
/// compiler. Original strings are sorted by their UTF-8 byte representation.
pub fn compile_mo(
document : PoFile,
endian? : Endian = Little,
) -> Bytes raise GettextError {
let pairs = collect_mo_pairs(document)
let count = pairs.length()
if count > (2147483647 - 28) / 16 {
raise Validation(message="catalog is too large for an MO revision 0 file")
}
let original_table = 28
let translation_table = original_table + count * 8
let data_start = translation_table + count * 8
let output = Array::make(data_start, b'\x00')
let original_lengths : Array[Int] = []
let original_offsets : Array[Int] = []
let translation_lengths : Array[Int] = []
let translation_offsets : Array[Int] = []
for pair in pairs {
original_lengths.push(pair.original.length())
original_offsets.push(output.length())
output.append(pair.original.to_array())
output.push(b'\x00')
}
for pair in pairs {
translation_lengths.push(pair.translation.length())
translation_offsets.push(output.length())
output.append(pair.translation.to_array())
output.push(b'\x00')
}
write_mo_magic(output, endian)
write_u32_at(output, 4, 0, endian)
write_u32_at(output, 8, count, endian)
write_u32_at(output, 12, original_table, endian)
write_u32_at(output, 16, translation_table, endian)
write_u32_at(output, 20, 0, endian)
write_u32_at(output, 24, 0, endian)
for index in 0..