///|
pub(all) struct LocalizationImport {
document : Localization
issues : Array[LocalizationIssue]
} derive(Eq, Debug)
///|
pub extend LocalizationImport with Eq::{equal, not_equal}
///|
pub extend LocalizationImport with @debug.Debug::{to_repr}
///|
/// Valve token names are PO contexts. Equal source strings remain distinct
/// messages, and translators may reorder entries without changing token identity.
pub fn Localization::to_po(
self : Localization,
translated? : Localization,
) -> POCatalog raise {
localization_validate(self)
let translations : Map[String, String] = Map([])
if translated is Some(target) {
localization_validate(target)
for token in target.tokens {
translations[token.name.to_lower()] = token.text
}
}
let entries : Array[POEntry] = [
{
context: None,
id: "",
plural: None,
translations: [
"Project-Id-Version: MoonValve\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\nLanguage: " +
translated.map(t => t.language).unwrap_or("") +
"\n",
],
comments: ["# MoonValve localization catalog"],
flags: [],
obsolete: false,
},
]
for token in self.tokens {
entries.push({
context: Some(token.name),
id: token.text,
plural: None,
translations: [translations.get(token.name.to_lower()).unwrap_or("")],
comments: [],
flags: [],
obsolete: false,
})
}
{ entries, trailing_comments: [], }
}
///|
/// Update a PO/POT from a new source catalog. Changed text keeps the old
/// translation marked fuzzy; removed messages become obsolete.
pub fn update_po(template : POCatalog, previous : POCatalog) -> POCatalog raise {
po_validate(template)
po_validate(previous)
let old : Map[String, POEntry] = Map([])
for entry in previous.entries {
if !entry.obsolete {
let key = match entry.context {
Some(context) => "c:" + context
None => "i:" + entry.id
}
if old.contains(key) {
raise InvalidDocument("Ambiguous PO update context")
}
old[key] = entry
}
}
let used : Map[String, Bool] = Map([])
let entries : Array[POEntry] = []
for entry in template.entries {
if entry.obsolete {
continue
}
let key = match entry.context {
Some(context) => "c:" + context
None => "i:" + entry.id
}
if used.contains(key) {
raise InvalidDocument("Ambiguous PO template context")
}
used[key] = true
if old.get(key) is Some(prior) {
let changed = entry.id != prior.id || entry.plural != prior.plural
let flags = prior.flags.copy()
if changed && !flags.contains("fuzzy") {
flags.push("fuzzy")
}
let comments = prior.comments.copy()
if changed {
comments.push("#| msgid " + po_quote(prior.id))
}
entries.push({
..entry,
translations: if entry.plural == prior.plural {
prior.translations.copy()
} else {
entry.translations.copy()
},
comments,
flags,
})
} else {
entries.push(entry)
}
}
for entry in previous.entries {
let key = match entry.context {
Some(context) => "c:" + context
None => "i:" + entry.id
}
if entry.obsolete || !used.contains(key) {
entries.push({ ..entry, obsolete: true, })
}
}
let catalog = {
entries,
trailing_comments: previous.trailing_comments.copy(),
}
catalog.stringify() |> ignore
catalog
}
///|
/// Import reviewed translations. Empty, fuzzy, stale, plural or unsafe entries
/// produce issues and retain source text; unknown contexts are also reported.
pub fn Localization::import_po(
self : Localization,
catalog : POCatalog,
language : String,
syntax~ : String = "valve",
allow_fuzzy~ : Bool = false,
) -> LocalizationImport raise {
localization_validate(self)
po_validate(catalog)
let targets : Map[String, POEntry] = Map([])
let issues : Array[LocalizationIssue] = []
let known : Map[String, Bool] = Map([])
for token in self.tokens {
known[token.name.to_lower()] = true
}
for entry in catalog.entries {
if entry.obsolete || (entry.context is None && entry.id.is_empty()) {
continue
}
guard entry.context is Some(context) else {
issues.push({
token: "",
code: "missing_context",
message: "Valve PO messages require a token name as msgctxt",
})
continue
}
let key = context.to_lower()
if targets.contains(key) {
raise InvalidDocument("Duplicate Valve PO token context")
}
targets[key] = entry
if !known.contains(key) {
issues.push({
token: context,
code: "extra_token",
message: "PO context is absent from the source localization",
})
}
}
let tokens = self.tokens.map(token => {
let issue = if targets.get(token.name.to_lower()) is Some(entry) {
if entry.id != token.text {
Some(("stale_source", "PO msgid differs from the current source text"))
} else if entry.plural is Some(_) {
Some(
(
"plural_unsupported", "Valve tokens require a singular PO translation",
),
)
} else if entry.flags.contains("fuzzy") && !allow_fuzzy {
Some(("fuzzy_translation", "PO translation requires review"))
} else if entry.translations[0].is_empty() && !token.text.is_empty() {
Some(("untranslated", "PO msgstr is empty"))
} else {
let valid = try
localization_placeholders(token.text, syntax~) ==
localization_placeholders(entry.translations[0], syntax~)
catch {
_ => false
}
if valid {
return LocalizationToken::{ ..token, text: entry.translations[0], }
} else {
Some(
(
"placeholder_mismatch", "Translation changes or invalidates placeholders",
),
)
}
}
} else {
Some(("missing_token", "No PO translation for this token"))
}
if issue is Some((code, message)) {
issues.push({ token: token.name, code, message, })
}
token
})
let document = { ..self, language, tokens, }
localization_validate(document)
{ document, issues, }
}