use std::collections::BTreeMap;
use std::fs;
use std::mem;
use super::catalog::{
OpaqueCapture, apply_header_defaults, parse_catalog_to_internal, sort_messages,
};
use super::export::export_catalog_content;
use super::file_io::atomic_write;
use super::{
ApiError, CatalogConvertResult, CatalogFileConvertResult, CatalogFileFormat, CatalogMode,
CatalogStorageFormat, CatalogUpdateInput, ConvertCatalogFileOptions, ConvertCatalogOptions,
PlaceholderCommentMode, RenderOptions, UpdateCatalogOptions,
};
pub fn convert_catalog(
options: ConvertCatalogOptions<'_>,
) -> Result<CatalogConvertResult, ApiError> {
super::validate_source_locale(options.source_locale)?;
validate_locale(options.locale)?;
validate_conversion_modes(options.source_mode, options.target_mode)?;
let catalog = parse_catalog_to_internal(
options.content,
None,
options.source_locale,
options.source_mode.semantics(),
options.source_mode.plural_encoding(),
false,
options.source_mode.storage_format(),
OpaqueCapture::Keep,
)?;
finish_catalog_conversion(options, catalog)
}
fn finish_catalog_conversion(
options: ConvertCatalogOptions<'_>,
mut catalog: super::catalog::Catalog,
) -> Result<CatalogConvertResult, ApiError> {
let locale = resolve_locale(options.locale, catalog.locale.as_deref())?;
catalog.locale.clone_from(&locale);
let message_count = catalog.messages.len();
let mut diagnostics = mem::take(&mut catalog.diagnostics);
materialize_all_placeholder_comments(&mut catalog);
match options.target_mode.storage_format() {
CatalogStorageFormat::Po => {
apply_header_defaults(
&mut catalog.headers,
locale.as_deref(),
options.target_mode.semantics(),
&mut diagnostics,
&BTreeMap::new(),
);
sort_messages(&mut catalog.messages, options.order_by);
}
CatalogStorageFormat::Fcl => {
catalog.headers.clear();
catalog.file_comments.clear();
catalog.file_extracted_comments.clear();
}
}
let render = RenderOptions {
order_by: options.order_by,
include_origins: true,
print_placeholders_in_comments: PlaceholderCommentMode::Disabled,
custom_header_attributes: None,
po_serialize: options.po_serialize,
};
let mut export_options =
UpdateCatalogOptions::new(options.source_locale, CatalogUpdateInput::default())
.with_mode(options.target_mode)
.with_render(render);
if let Some(locale) = locale.as_deref() {
export_options = export_options.with_locale(locale);
}
let content = export_catalog_content(
&catalog,
&export_options,
locale.as_deref(),
&mut diagnostics,
)?;
Ok(CatalogConvertResult {
content,
locale,
source_mode: options.source_mode,
target_mode: options.target_mode,
message_count,
diagnostics,
})
}
pub fn convert_catalog_file(
options: ConvertCatalogFileOptions<'_>,
) -> Result<CatalogFileConvertResult, ApiError> {
super::validate_source_locale(options.source_locale)?;
validate_locale(options.locale)?;
if options.input_path.as_os_str().is_empty() {
return Err(ApiError::InvalidArguments(
"input_path must not be empty".to_owned(),
));
}
if options.output_path.as_os_str().is_empty() {
return Err(ApiError::InvalidArguments(
"output_path must not be empty".to_owned(),
));
}
let source_format = match options.source_format {
Some(format) => format,
None => CatalogFileFormat::infer_from_path(options.input_path)?,
};
let target_format = match options.target_format {
Some(format) => format,
None => CatalogFileFormat::infer_from_path(options.output_path)?,
};
let source_mode = mode_for_format("source", source_format, options.source_mode)?;
let target_mode = mode_for_format("target", target_format, options.target_mode)?;
validate_conversion_modes(source_mode, target_mode)?;
let content = fs::read_to_string(options.input_path)
.map_err(|error| ApiError::io_with_path(options.input_path, error))?;
let converted = convert_catalog(
ConvertCatalogOptions::new(&content, options.source_locale, source_mode, target_mode)
.with_optional_locale(options.locale)
.with_order_by(options.order_by)
.with_po_serialize_options(options.po_serialize),
)?;
atomic_write(options.output_path, &converted.content)?;
Ok(CatalogFileConvertResult {
output_path: options.output_path.to_path_buf(),
source_format,
target_format,
source_mode,
target_mode,
locale: converted.locale,
message_count: converted.message_count,
diagnostics: converted.diagnostics,
})
}
impl<'a> ConvertCatalogOptions<'a> {
fn with_optional_locale(mut self, locale: Option<&'a str>) -> Self {
self.locale = locale;
self
}
}
fn validate_locale(locale: Option<&str>) -> Result<(), ApiError> {
if locale.is_some_and(|locale| locale.trim().is_empty()) {
return Err(ApiError::InvalidArguments(
"locale must not be empty".to_owned(),
));
}
Ok(())
}
fn materialize_all_placeholder_comments(catalog: &mut super::catalog::Catalog) {
for message in &mut catalog.messages {
let mut comment = String::new();
let mut generated = Vec::new();
for (name, values) in &message.placeholders {
for value in values {
comment.clear();
super::export::write_placeholder_comment(&mut comment, name, value);
if !message.comments.contains(&comment) && !generated.contains(&comment) {
generated.push(comment.clone());
}
}
}
message.comments.extend(generated);
message.placeholders.clear();
}
}
fn resolve_locale(
expected: Option<&str>,
declared: Option<&str>,
) -> Result<Option<String>, ApiError> {
validate_locale(declared)?;
if let (Some(expected), Some(declared)) = (expected, declared)
&& expected != declared
{
return Err(ApiError::InvalidArguments(format!(
"catalog locale {declared:?} did not match expected locale {expected:?}"
)));
}
Ok(expected.or(declared).map(str::to_owned))
}
fn validate_conversion_modes(
source_mode: CatalogMode,
target_mode: CatalogMode,
) -> Result<(), ApiError> {
if source_mode.semantics() != target_mode.semantics() {
return Err(ApiError::Unsupported(format!(
"catalog conversion from {source_mode:?} to {target_mode:?} changes semantic mode"
)));
}
Ok(())
}
fn mode_for_format(
side: &str,
format: CatalogFileFormat,
mode: Option<CatalogMode>,
) -> Result<CatalogMode, ApiError> {
let mode = mode.unwrap_or_else(|| format.default_mode());
if mode.storage_format() != format.default_mode().storage_format() {
return Err(ApiError::InvalidArguments(format!(
"{side} mode {mode:?} does not match {side} format {format:?}"
)));
}
Ok(mode)
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use crate::PoVec;
use super::super::PluralSource;
use super::super::catalog::{CanonicalMessage, CanonicalTranslation, Catalog};
use super::*;
#[test]
fn conversion_propagates_target_export_errors() {
let catalog = Catalog {
messages: vec![CanonicalMessage {
msgid: "item".to_owned(),
msgctxt: None,
translation: CanonicalTranslation::Plural {
source: PluralSource {
one: Some("item".to_owned()),
other: "items".to_owned(),
},
translation_by_category: BTreeMap::from([(
"one".to_owned(),
"Artikel".to_owned(),
)]),
variable: "count".to_owned(),
},
comments: Vec::new(),
opaque: None,
origins: PoVec::new(),
placeholders: BTreeMap::new(),
obsolete: None,
machine: None,
}],
..Catalog::default()
};
let error = finish_catalog_conversion(
ConvertCatalogOptions::new("", "en", CatalogMode::GettextPo, CatalogMode::GettextPo)
.with_locale("de"),
catalog,
)
.expect_err("missing other category");
assert!(matches!(
error,
ApiError::Unsupported(message) if message.contains("other")
));
}
}