use std::collections::BTreeMap;
use std::fmt;
use std::path::Path;
use crate::ParseError;
use super::plural::PluralProfile;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CatalogOrigin {
pub file: String,
pub line: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ExtractedSingularMessage {
pub msgid: String,
pub msgctxt: Option<String>,
pub comments: Vec<String>,
pub origin: Vec<CatalogOrigin>,
pub placeholders: BTreeMap<String, Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PluralSource {
pub one: Option<String>,
pub other: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ExtractedPluralMessage {
pub msgid: String,
pub msgctxt: Option<String>,
pub source: PluralSource,
pub comments: Vec<String>,
pub origin: Vec<CatalogOrigin>,
pub placeholders: BTreeMap<String, Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExtractedMessage {
Singular(ExtractedSingularMessage),
Plural(ExtractedPluralMessage),
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SourceExtractedMessage {
pub msgid: String,
pub msgctxt: Option<String>,
pub comments: Vec<String>,
pub origin: Vec<CatalogOrigin>,
pub placeholders: BTreeMap<String, Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CatalogUpdateInput {
Structured(Vec<ExtractedMessage>),
SourceFirst(Vec<SourceExtractedMessage>),
}
impl Default for CatalogUpdateInput {
fn default() -> Self {
Self::Structured(Vec::new())
}
}
impl From<Vec<ExtractedMessage>> for CatalogUpdateInput {
fn from(value: Vec<ExtractedMessage>) -> Self {
Self::Structured(value)
}
}
impl From<Vec<SourceExtractedMessage>> for CatalogUpdateInput {
fn from(value: Vec<SourceExtractedMessage>) -> Self {
Self::SourceFirst(value)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TranslationShape {
Singular {
value: String,
},
Plural {
source: PluralSource,
translation: BTreeMap<String, String>,
variable: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EffectiveTranslationRef<'a> {
Singular(&'a str),
Plural(&'a BTreeMap<String, String>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EffectiveTranslation {
Singular(String),
Plural(BTreeMap<String, String>),
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CatalogMessageExtra {
pub translator_comments: Vec<String>,
pub flags: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CatalogMessage {
pub msgid: String,
pub msgctxt: Option<String>,
pub translation: TranslationShape,
pub comments: Vec<String>,
pub origin: Vec<CatalogOrigin>,
pub obsolete: bool,
pub extra: Option<CatalogMessageExtra>,
}
impl CatalogMessage {
#[must_use]
pub fn key(&self) -> CatalogMessageKey {
CatalogMessageKey {
msgid: self.msgid.clone(),
msgctxt: self.msgctxt.clone(),
}
}
#[must_use]
pub fn effective_translation(&self) -> EffectiveTranslationRef<'_> {
match &self.translation {
TranslationShape::Singular { value } => EffectiveTranslationRef::Singular(value),
TranslationShape::Plural { translation, .. } => {
EffectiveTranslationRef::Plural(translation)
}
}
}
pub(super) fn effective_translation_owned(&self) -> EffectiveTranslation {
match &self.translation {
TranslationShape::Singular { value } => EffectiveTranslation::Singular(value.clone()),
TranslationShape::Plural { translation, .. } => {
EffectiveTranslation::Plural(translation.clone())
}
}
}
pub(super) fn source_fallback_translation(&self, locale: Option<&str>) -> EffectiveTranslation {
match &self.translation {
TranslationShape::Singular { value } => {
if value.is_empty() {
EffectiveTranslation::Singular(self.msgid.clone())
} else {
EffectiveTranslation::Singular(value.clone())
}
}
TranslationShape::Plural {
source,
translation,
..
} => {
let profile = PluralProfile::for_locale(locale);
let mut effective = profile.materialize_translation(translation);
for category in profile.categories() {
let should_fill = effective.get(category).is_none_or(String::is_empty);
if should_fill {
effective.insert(
category.clone(),
profile.source_locale_value(category, source),
);
}
}
EffectiveTranslation::Plural(effective)
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CatalogMessageKey {
pub msgid: String,
pub msgctxt: Option<String>,
}
impl CatalogMessageKey {
#[must_use]
pub fn new(msgid: impl Into<String>, msgctxt: Option<String>) -> Self {
Self {
msgid: msgid.into(),
msgctxt,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticSeverity {
Info,
Warning,
Error,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
pub severity: DiagnosticSeverity,
pub code: String,
pub message: String,
pub msgid: Option<String>,
pub msgctxt: Option<String>,
}
impl Diagnostic {
pub(super) fn new(
severity: DiagnosticSeverity,
code: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
severity,
code: code.into(),
message: message.into(),
msgid: None,
msgctxt: None,
}
}
pub(super) fn with_identity(mut self, msgid: &str, msgctxt: Option<&str>) -> Self {
self.msgid = Some(msgid.to_owned());
self.msgctxt = msgctxt.map(str::to_owned);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CatalogStats {
pub total: usize,
pub added: usize,
pub changed: usize,
pub unchanged: usize,
pub obsolete_marked: usize,
pub obsolete_removed: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CatalogUpdateResult {
pub content: String,
pub created: bool,
pub updated: bool,
pub stats: CatalogStats,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CatalogCombineInput<'a> {
pub content: &'a str,
pub label: Option<&'a str>,
}
impl<'a> CatalogCombineInput<'a> {
#[must_use]
pub const fn new(content: &'a str) -> Self {
Self {
content,
label: None,
}
}
#[must_use]
pub const fn labeled(content: &'a str, label: &'a str) -> Self {
Self {
content,
label: Some(label),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CatalogConflictStrategy {
#[default]
UseFirst,
UseLast,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CatalogCombineSelection {
#[default]
All,
MoreThan(usize),
LessThan(usize),
Unique,
}
impl CatalogCombineSelection {
pub(super) const fn includes(self, definitions: usize) -> bool {
match self {
Self::All => true,
Self::MoreThan(limit) => definitions > limit,
Self::LessThan(limit) => definitions < limit,
Self::Unique => definitions < 2,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CatalogCombineStats {
pub inputs: usize,
pub definitions: usize,
pub selected: usize,
pub skipped: usize,
pub conflicts_resolved: usize,
pub total: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CatalogCombineResult {
pub content: String,
pub stats: CatalogCombineStats,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedCatalog {
pub locale: Option<String>,
pub semantics: CatalogSemantics,
pub headers: BTreeMap<String, String>,
pub messages: Vec<CatalogMessage>,
pub diagnostics: Vec<Diagnostic>,
}
impl ParsedCatalog {
pub fn into_normalized_view(self) -> Result<NormalizedParsedCatalog, ApiError> {
NormalizedParsedCatalog::new(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NormalizedParsedCatalog {
pub(super) catalog: ParsedCatalog,
pub(super) key_index: BTreeMap<CatalogMessageKey, usize>,
msgid_index: BTreeMap<String, Vec<usize>>,
}
impl NormalizedParsedCatalog {
pub(super) fn new(catalog: ParsedCatalog) -> Result<Self, ApiError> {
let mut key_index = BTreeMap::new();
let mut msgid_index = BTreeMap::<String, Vec<usize>>::new();
for (index, message) in catalog.messages.iter().enumerate() {
let key = message.key();
if key_index.insert(key.clone(), index).is_some() {
return Err(ApiError::Conflict(format!(
"duplicate parsed catalog message for msgid {:?} and context {:?}",
key.msgid, key.msgctxt
)));
}
msgid_index.entry(key.msgid).or_default().push(index);
}
Ok(Self {
catalog,
key_index,
msgid_index,
})
}
#[must_use]
pub const fn parsed_catalog(&self) -> &ParsedCatalog {
&self.catalog
}
#[must_use]
pub fn into_parsed_catalog(self) -> ParsedCatalog {
self.catalog
}
#[must_use]
pub fn get(&self, key: &CatalogMessageKey) -> Option<&CatalogMessage> {
self.key_index
.get(key)
.map(|index| &self.catalog.messages[*index])
}
#[must_use]
pub fn get_by_parts(&self, msgid: &str, msgctxt: Option<&str>) -> Option<&CatalogMessage> {
self.msgid_index.get(msgid)?.iter().find_map(|index| {
let message = &self.catalog.messages[*index];
(message.msgctxt.as_deref() == msgctxt).then_some(message)
})
}
#[must_use]
pub fn contains_key(&self, key: &CatalogMessageKey) -> bool {
self.key_index.contains_key(key)
}
#[must_use]
pub fn contains_parts(&self, msgid: &str, msgctxt: Option<&str>) -> bool {
self.get_by_parts(msgid, msgctxt).is_some()
}
#[must_use]
pub fn message_count(&self) -> usize {
self.catalog.messages.len()
}
pub fn iter(&self) -> impl Iterator<Item = (&CatalogMessageKey, &CatalogMessage)> + '_ {
self.key_index
.iter()
.map(|(key, index)| (key, &self.catalog.messages[*index]))
}
pub fn effective_translation(
&self,
key: &CatalogMessageKey,
) -> Option<EffectiveTranslationRef<'_>> {
self.get(key).map(CatalogMessage::effective_translation)
}
pub fn effective_translation_by_parts(
&self,
msgid: &str,
msgctxt: Option<&str>,
) -> Option<EffectiveTranslationRef<'_>> {
self.get_by_parts(msgid, msgctxt)
.map(CatalogMessage::effective_translation)
}
#[must_use]
pub fn effective_translation_with_source_fallback(
&self,
key: &CatalogMessageKey,
source_locale: &str,
) -> Option<EffectiveTranslation> {
let message = self.get(key)?;
Some(self.effective_translation_for_message(message, source_locale))
}
#[must_use]
pub fn effective_translation_with_source_fallback_by_parts(
&self,
msgid: &str,
msgctxt: Option<&str>,
source_locale: &str,
) -> Option<EffectiveTranslation> {
let message = self.get_by_parts(msgid, msgctxt)?;
Some(self.effective_translation_for_message(message, source_locale))
}
fn effective_translation_for_message(
&self,
message: &CatalogMessage,
source_locale: &str,
) -> EffectiveTranslation {
if self
.catalog
.locale
.as_deref()
.is_none_or(|locale| locale == source_locale)
{
message.source_fallback_translation(self.catalog.locale.as_deref())
} else {
message.effective_translation_owned()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PluralEncoding {
#[default]
Icu,
Gettext,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CatalogStorageFormat {
#[default]
Po,
Ndjson,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CatalogSemantics {
#[default]
IcuNative,
GettextCompat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ObsoleteStrategy {
#[default]
Mark,
Delete,
Keep,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OrderBy {
#[default]
Msgid,
Origin,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PlaceholderCommentMode {
Disabled,
Enabled {
limit: usize,
},
}
impl Default for PlaceholderCommentMode {
fn default() -> Self {
Self::Enabled { limit: 3 }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpdateCatalogOptions<'a> {
pub locale: Option<&'a str>,
pub source_locale: &'a str,
pub input: CatalogUpdateInput,
pub existing: Option<&'a str>,
pub storage_format: CatalogStorageFormat,
pub semantics: CatalogSemantics,
pub plural_encoding: PluralEncoding,
pub obsolete_strategy: ObsoleteStrategy,
pub overwrite_source_translations: bool,
pub order_by: OrderBy,
pub include_origins: bool,
pub include_line_numbers: bool,
pub print_placeholders_in_comments: PlaceholderCommentMode,
pub custom_header_attributes: Option<&'a BTreeMap<String, String>>,
}
impl Default for UpdateCatalogOptions<'_> {
fn default() -> Self {
Self {
locale: None,
source_locale: "",
input: CatalogUpdateInput::default(),
existing: None,
storage_format: CatalogStorageFormat::Po,
semantics: CatalogSemantics::IcuNative,
plural_encoding: PluralEncoding::Icu,
obsolete_strategy: ObsoleteStrategy::Mark,
overwrite_source_translations: false,
order_by: OrderBy::Msgid,
include_origins: true,
include_line_numbers: true,
print_placeholders_in_comments: PlaceholderCommentMode::Enabled { limit: 3 },
custom_header_attributes: None,
}
}
}
impl<'a> UpdateCatalogOptions<'a> {
#[must_use]
pub fn new(source_locale: &'a str, input: impl Into<CatalogUpdateInput>) -> Self {
Self {
source_locale,
input: input.into(),
..Self::default()
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpdateCatalogFileOptions<'a> {
pub target_path: &'a Path,
pub locale: Option<&'a str>,
pub source_locale: &'a str,
pub input: CatalogUpdateInput,
pub storage_format: CatalogStorageFormat,
pub semantics: CatalogSemantics,
pub plural_encoding: PluralEncoding,
pub obsolete_strategy: ObsoleteStrategy,
pub overwrite_source_translations: bool,
pub order_by: OrderBy,
pub include_origins: bool,
pub include_line_numbers: bool,
pub print_placeholders_in_comments: PlaceholderCommentMode,
pub custom_header_attributes: Option<&'a BTreeMap<String, String>>,
}
impl Default for UpdateCatalogFileOptions<'_> {
fn default() -> Self {
Self {
target_path: Path::new(""),
locale: None,
source_locale: "",
input: CatalogUpdateInput::default(),
storage_format: CatalogStorageFormat::Po,
semantics: CatalogSemantics::IcuNative,
plural_encoding: PluralEncoding::Icu,
obsolete_strategy: ObsoleteStrategy::Mark,
overwrite_source_translations: false,
order_by: OrderBy::Msgid,
include_origins: true,
include_line_numbers: true,
print_placeholders_in_comments: PlaceholderCommentMode::Enabled { limit: 3 },
custom_header_attributes: None,
}
}
}
impl<'a> UpdateCatalogFileOptions<'a> {
#[must_use]
pub fn new(
target_path: &'a Path,
source_locale: &'a str,
input: impl Into<CatalogUpdateInput>,
) -> Self {
Self {
target_path,
source_locale,
input: input.into(),
..Self::default()
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CombineCatalogOptions<'a> {
pub inputs: &'a [CatalogCombineInput<'a>],
pub locale: Option<&'a str>,
pub source_locale: &'a str,
pub storage_format: CatalogStorageFormat,
pub semantics: CatalogSemantics,
pub plural_encoding: PluralEncoding,
pub conflict_strategy: CatalogConflictStrategy,
pub selection: CatalogCombineSelection,
pub order_by: OrderBy,
pub include_origins: bool,
pub include_line_numbers: bool,
pub include_obsolete: bool,
}
impl Default for CombineCatalogOptions<'_> {
fn default() -> Self {
Self {
inputs: &[],
locale: None,
source_locale: "",
storage_format: CatalogStorageFormat::Po,
semantics: CatalogSemantics::IcuNative,
plural_encoding: PluralEncoding::Icu,
conflict_strategy: CatalogConflictStrategy::UseFirst,
selection: CatalogCombineSelection::All,
order_by: OrderBy::Msgid,
include_origins: true,
include_line_numbers: true,
include_obsolete: false,
}
}
}
impl<'a> CombineCatalogOptions<'a> {
#[must_use]
pub fn new(inputs: &'a [CatalogCombineInput<'a>], source_locale: &'a str) -> Self {
Self {
inputs,
source_locale,
..Self::default()
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseCatalogOptions<'a> {
pub content: &'a str,
pub locale: Option<&'a str>,
pub source_locale: &'a str,
pub storage_format: CatalogStorageFormat,
pub semantics: CatalogSemantics,
pub plural_encoding: PluralEncoding,
pub strict: bool,
}
impl Default for ParseCatalogOptions<'_> {
fn default() -> Self {
Self {
content: "",
locale: None,
source_locale: "",
storage_format: CatalogStorageFormat::Po,
semantics: CatalogSemantics::IcuNative,
plural_encoding: PluralEncoding::Icu,
strict: false,
}
}
}
impl<'a> ParseCatalogOptions<'a> {
#[must_use]
pub fn new(content: &'a str, source_locale: &'a str) -> Self {
Self {
content,
source_locale,
..Self::default()
}
}
}
#[derive(Debug)]
pub enum ApiError {
Parse(ParseError),
Io(std::io::Error),
InvalidArguments(String),
Conflict(String),
Unsupported(String),
}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Parse(error) => error.fmt(f),
Self::Io(error) => error.fmt(f),
Self::InvalidArguments(message)
| Self::Conflict(message)
| Self::Unsupported(message) => f.write_str(message),
}
}
}
impl std::error::Error for ApiError {}
impl From<ParseError> for ApiError {
fn from(value: ParseError) -> Self {
Self::Parse(value)
}
}
impl From<std::io::Error> for ApiError {
fn from(value: std::io::Error) -> Self {
Self::Io(value)
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::io;
use std::path::Path;
use super::{
ApiError, CatalogCombineInput, CatalogCombineSelection, CatalogConflictStrategy,
CatalogMessage, CatalogMessageExtra, CatalogMessageKey, CatalogSemantics,
CatalogStorageFormat, CatalogUpdateInput, CombineCatalogOptions, Diagnostic,
DiagnosticSeverity, EffectiveTranslation, EffectiveTranslationRef, NormalizedParsedCatalog,
ObsoleteStrategy, OrderBy, ParseCatalogOptions, ParsedCatalog, PlaceholderCommentMode,
PluralEncoding, PluralSource, TranslationShape, UpdateCatalogFileOptions,
UpdateCatalogOptions,
};
#[test]
fn catalog_update_input_defaults_and_conversions_use_expected_variants() {
assert!(matches!(
CatalogUpdateInput::default(),
CatalogUpdateInput::Structured(messages) if messages.is_empty()
));
assert!(matches!(
CatalogUpdateInput::from(Vec::<super::ExtractedMessage>::new()),
CatalogUpdateInput::Structured(messages) if messages.is_empty()
));
assert!(matches!(
CatalogUpdateInput::from(Vec::<super::SourceExtractedMessage>::new()),
CatalogUpdateInput::SourceFirst(messages) if messages.is_empty()
));
}
#[test]
fn catalog_message_helpers_cover_key_and_fallback_behavior() {
let singular = CatalogMessage {
msgid: "Hello".to_owned(),
msgctxt: Some("button".to_owned()),
translation: TranslationShape::Singular {
value: String::new(),
},
comments: vec!["Shown in toolbar".to_owned()],
origin: Vec::new(),
obsolete: false,
extra: Some(CatalogMessageExtra {
translator_comments: vec!["Imperative".to_owned()],
flags: vec!["fuzzy".to_owned()],
}),
};
assert_eq!(
singular.key(),
CatalogMessageKey::new("Hello", Some("button".to_owned()))
);
assert!(matches!(
singular.effective_translation(),
EffectiveTranslationRef::Singular("")
));
assert_eq!(
singular.source_fallback_translation(Some("en")),
EffectiveTranslation::Singular("Hello".to_owned())
);
let plural = CatalogMessage {
msgid: "{count, plural, one {# file} other {# files}}".to_owned(),
msgctxt: None,
translation: TranslationShape::Plural {
source: PluralSource {
one: Some("{count} file".to_owned()),
other: "{count} files".to_owned(),
},
translation: BTreeMap::from([
("one".to_owned(), String::new()),
("other".to_owned(), "{count} Dateien".to_owned()),
]),
variable: "count".to_owned(),
},
comments: Vec::new(),
origin: Vec::new(),
obsolete: false,
extra: None,
};
assert!(matches!(
plural.effective_translation(),
EffectiveTranslationRef::Plural(values)
if values.get("other") == Some(&"{count} Dateien".to_owned())
));
assert_eq!(
plural.source_fallback_translation(Some("de")),
EffectiveTranslation::Plural(BTreeMap::from([
("one".to_owned(), "{count} file".to_owned()),
("other".to_owned(), "{count} Dateien".to_owned()),
]))
);
}
#[test]
fn normalized_catalog_helpers_expose_lookup_and_source_fallback_views() {
let parsed = ParsedCatalog {
locale: Some("en".to_owned()),
semantics: CatalogSemantics::IcuNative,
headers: BTreeMap::new(),
messages: vec![
CatalogMessage {
msgid: "Hello".to_owned(),
msgctxt: None,
translation: TranslationShape::Singular {
value: String::new(),
},
comments: Vec::new(),
origin: Vec::new(),
obsolete: false,
extra: None,
},
CatalogMessage {
msgid: "Hello".to_owned(),
msgctxt: Some("button".to_owned()),
translation: TranslationShape::Singular {
value: "Howdy".to_owned(),
},
comments: Vec::new(),
origin: Vec::new(),
obsolete: false,
extra: None,
},
],
diagnostics: Vec::new(),
};
let normalized = NormalizedParsedCatalog::new(parsed.clone()).expect("normalized");
let key = CatalogMessageKey::new("Hello", None);
assert_eq!(normalized.message_count(), 2);
assert!(normalized.contains_key(&key));
assert!(normalized.contains_parts("Hello", Some("button")));
assert_eq!(
normalized.parsed_catalog().semantics,
CatalogSemantics::IcuNative
);
assert!(normalized.get(&key).is_some());
assert_eq!(
normalized
.get_by_parts("Hello", Some("button"))
.and_then(|message| match message.effective_translation() {
EffectiveTranslationRef::Singular(value) => Some(value),
EffectiveTranslationRef::Plural(_) => None,
}),
Some("Howdy")
);
assert!(matches!(
normalized.effective_translation_by_parts("Hello", None),
Some(EffectiveTranslationRef::Singular(""))
));
assert_eq!(
normalized.effective_translation_with_source_fallback(&key, "en"),
Some(EffectiveTranslation::Singular("Hello".to_owned()))
);
assert_eq!(
normalized.effective_translation_with_source_fallback_by_parts(
"Hello",
Some("button"),
"en"
),
Some(EffectiveTranslation::Singular("Howdy".to_owned()))
);
assert_eq!(normalized.into_parsed_catalog(), parsed);
}
#[test]
fn option_defaults_reflect_native_po_defaults() {
let update = UpdateCatalogOptions::default();
assert_eq!(update.storage_format, CatalogStorageFormat::Po);
assert_eq!(update.semantics, CatalogSemantics::IcuNative);
assert_eq!(update.plural_encoding, PluralEncoding::Icu);
assert_eq!(update.obsolete_strategy, ObsoleteStrategy::Mark);
assert_eq!(update.order_by, OrderBy::Msgid);
assert!(update.include_origins);
assert!(update.include_line_numbers);
assert_eq!(
update.print_placeholders_in_comments,
PlaceholderCommentMode::Enabled { limit: 3 }
);
let update_new = UpdateCatalogOptions::new("en", Vec::<super::ExtractedMessage>::new());
assert_eq!(update_new.source_locale, "en");
assert!(matches!(
update_new.input,
CatalogUpdateInput::Structured(messages) if messages.is_empty()
));
let update_file = UpdateCatalogFileOptions::default();
assert_eq!(update_file.target_path, Path::new(""));
assert_eq!(update_file.storage_format, CatalogStorageFormat::Po);
assert_eq!(update_file.semantics, CatalogSemantics::IcuNative);
assert_eq!(update_file.plural_encoding, PluralEncoding::Icu);
let update_file_new = UpdateCatalogFileOptions::new(
Path::new("locale/de.po"),
"en",
Vec::<super::SourceExtractedMessage>::new(),
);
assert_eq!(update_file_new.target_path, Path::new("locale/de.po"));
assert_eq!(update_file_new.source_locale, "en");
assert!(matches!(
update_file_new.input,
CatalogUpdateInput::SourceFirst(messages) if messages.is_empty()
));
let parse = ParseCatalogOptions::default();
assert_eq!(parse.storage_format, CatalogStorageFormat::Po);
assert_eq!(parse.semantics, CatalogSemantics::IcuNative);
assert_eq!(parse.plural_encoding, PluralEncoding::Icu);
assert!(!parse.strict);
let parse_new = ParseCatalogOptions::new("msgid \"Hello\"\nmsgstr \"Hallo\"\n", "en");
assert_eq!(parse_new.content, "msgid \"Hello\"\nmsgstr \"Hallo\"\n");
assert_eq!(parse_new.source_locale, "en");
assert_eq!(parse_new.storage_format, CatalogStorageFormat::Po);
let inputs = [CatalogCombineInput::labeled(
"msgid \"Hello\"\nmsgstr \"Hallo\"\n",
"de.po",
)];
let combine = CombineCatalogOptions::new(&inputs, "en");
assert_eq!(combine.inputs, &inputs);
assert_eq!(combine.source_locale, "en");
assert_eq!(combine.conflict_strategy, CatalogConflictStrategy::UseFirst);
assert_eq!(combine.selection, CatalogCombineSelection::All);
assert!(combine.include_origins);
assert!(combine.include_line_numbers);
assert!(!combine.include_obsolete);
}
#[test]
fn diagnostics_and_api_errors_preserve_human_readable_messages() {
let diagnostic = Diagnostic::new(DiagnosticSeverity::Warning, "code", "message")
.with_identity("Hello", Some("button"));
assert_eq!(diagnostic.severity, DiagnosticSeverity::Warning);
assert_eq!(diagnostic.code, "code");
assert_eq!(diagnostic.message, "message");
assert_eq!(diagnostic.msgid.as_deref(), Some("Hello"));
assert_eq!(diagnostic.msgctxt.as_deref(), Some("button"));
let io_error = ApiError::from(io::Error::other("disk"));
assert_eq!(io_error.to_string(), "disk");
assert_eq!(
ApiError::InvalidArguments("bad input".to_owned()).to_string(),
"bad input"
);
assert_eq!(
ApiError::Conflict("duplicate".to_owned()).to_string(),
"duplicate"
);
assert_eq!(
ApiError::Unsupported("unsupported".to_owned()).to_string(),
"unsupported"
);
}
}