///|
fn comment_marker(kind : CommentKind, text : String) -> String {
match kind {
Translator => if text == "" { "#" } else { "# " }
Extracted => "#. "
Reference => "#: "
Flag => "#, "
Previous => "#| "
}
}
///|
fn write_po_line(
output : StringBuilder,
line : String,
obsolete : Bool,
) -> Unit {
if obsolete {
output.write_string("#~ ")
}
output.write_string(line)
output.write_char('\n')
}
///|
fn multiline_fragments(value : String) -> Array[String] {
let fragments : Array[String] = []
let current = StringBuilder()
for c in value {
if c == '\n' {
current.write_char('\n')
fragments.push(current.to_string())
current.reset()
} else {
current.write_char(c)
}
}
if !current.is_empty() {
fragments.push(current.to_string())
}
fragments
}
///|
fn write_po_field(
output : StringBuilder,
keyword : String,
value : String,
obsolete : Bool,
) -> Unit {
if !value.contains_char('\n') {
write_po_line(output, "\{keyword} \"\{escape_po_string(value)}\"", obsolete)
return
}
write_po_line(output, "\{keyword} \"\"", obsolete)
for fragment in multiline_fragments(value) {
write_po_line(output, "\"\{escape_po_string(fragment)}\"", obsolete)
}
}
///|
/// Serialize a PO/POT document into deterministic GNU gettext syntax.
///
/// Semantic information is preserved, while whitespace, entry separation, and
/// multiline layout are normalized. Every entry is followed by one blank line.
pub fn write_po(document : PoFile) -> String {
let output = StringBuilder()
for entry in document.entries {
for comment in entry.comments {
write_po_line(
output,
comment_marker(comment.kind, comment.text) + comment.text,
entry.obsolete,
)
}
match entry.context {
Some(context) =>
write_po_field(output, "msgctxt", context, entry.obsolete)
None => ()
}
write_po_field(output, "msgid", entry.msgid, entry.obsolete)
match entry.msgid_plural {
Some(plural) => {
write_po_field(output, "msgid_plural", plural, entry.obsolete)
if entry.translations.is_empty() {
write_po_field(output, "msgstr[0]", "", entry.obsolete)
} else {
for index, translation in entry.translations {
write_po_field(
output,
"msgstr[\{index}]",
translation,
entry.obsolete,
)
}
}
}
None => {
let translation = entry.translations.get(0).unwrap_or("")
write_po_field(output, "msgstr", translation, entry.obsolete)
}
}
output.write_char('\n')
}
output.to_string()
}
///|
/// Return a POT-style copy with non-header translations cleared.
///
/// The metadata header is retained because template generators commonly store
/// MIME and project fields there. Singular entries receive one empty `msgstr`;
/// plural entries retain at least two indexed slots.
pub fn PoFile::as_template(self : PoFile) -> PoFile {
let entries : Array[PoEntry] = []
for entry in self.entries {
let translations = if entry.is_header() {
entry.translations.copy()
} else {
match entry.msgid_plural {
Some(_) =>
Array::make(
if entry.translations.length() > 2 {
entry.translations.length()
} else {
2
},
"",
)
None => [""]
}
}
entries.push({
comments: entry.comments.copy(),
context: entry.context,
msgid: entry.msgid,
msgid_plural: entry.msgid_plural,
translations,
obsolete: entry.obsolete,
})
}
PoFile::new(entries)
}
///|
/// Serialize a document as a translation template.
pub fn write_pot(document : PoFile) -> String {
write_po(document.as_template())
}