#![warn(missing_docs, rustdoc::broken_intra_doc_links)]
#[cfg(feature = "catalog")]
mod api;
mod borrowed;
pub mod diagnostic_codes;
mod line_state;
mod merge;
mod parse;
mod scan;
mod serialize;
mod text;
mod utf8;
#[cfg(feature = "catalog")]
pub use api::{
ApiError, COMPILED_CATALOG_ARTIFACT_SCHEMA_VERSION, CatalogAuditChecks, CatalogAuditDiagnostic,
CatalogAuditIcuOptions, CatalogAuditMessageRef, CatalogAuditOptions, CatalogAuditReport,
CatalogAuditSummary, CatalogCombineInput, CatalogCombineResult, CatalogCombineSelection,
CatalogCombineStats, CatalogConflictStrategy, CatalogCoverageMessage, CatalogCoverageOptions,
CatalogCoverageReport, CatalogFileCombineResult, CatalogFileFormat, CatalogLocaleCoverage,
CatalogLocaleReview, CatalogMachineTranslationMessage, CatalogMachineTranslationReview,
CatalogMachineTranslationStatus, CatalogMessage, CatalogMessageExtra, CatalogMessageKey,
CatalogMessageStatus, CatalogMode, CatalogOrigin, CatalogReviewOptions, CatalogReviewReport,
CatalogReviewSummary, CatalogReviewTranslation, CatalogSemantics, CatalogSourceChange,
CatalogSourceChangeKind, CatalogSourceChangeReport, CatalogStats, CatalogStorageFormat,
CatalogTranslationChange, CatalogTranslationChangeReport, CatalogUpdateInput,
CatalogUpdateResult, CombineCatalogFilesOptions, CombineCatalogOptions,
CompileCatalogArtifactIcuOptions, CompileCatalogArtifactOptions,
CompileCatalogArtifactReportOptions, CompileCatalogArtifactReportSelection,
CompileCatalogOptions, CompileSelectedCatalogArtifactOptions, CompiledCatalog,
CompiledCatalogArtifact, CompiledCatalogArtifactReport, CompiledCatalogDiagnostic,
CompiledCatalogIdDescription, CompiledCatalogIdIndex, CompiledCatalogMissingMessage,
CompiledCatalogProvenanceReport, CompiledCatalogResolution, CompiledCatalogResolutionKind,
CompiledCatalogTranslationKind, CompiledCatalogUnavailableId, CompiledKeyStrategy,
CompiledMessage, CompiledTranslation, DescribeCompiledIdsReport, Diagnostic,
DiagnosticSeverity, EffectiveTranslation, EffectiveTranslationRef, ExtractedMessage,
ExtractedPluralMessage, ExtractedSingularMessage, IcuFormatterSupportPolicy,
IcuPseudolocalizationOptions, IcuSyntaxPolicy, MachineTranslationMetadata, NdjsonCatalogReader,
NdjsonCatalogReaderOptions, NdjsonCatalogWriter, NdjsonCatalogWriterOptions,
NormalizedParsedCatalog, ObsoleteStrategy, OrderBy, ParseCatalogOptions, ParsedCatalog,
PlaceholderCommentMode, PluralEncoding, PluralSource, RenderOptions, SourceExtractedMessage,
TranslationShape, UpdateCatalogFileOptions, UpdateCatalogOptions, audit_catalogs,
audit_catalogs_with_icu_options, catalog_coverage, catalog_review, combine_catalog_files,
combine_catalogs, compile_catalog_artifact, compile_catalog_artifact_report,
compile_catalog_artifact_selected, compile_catalog_artifact_selected_with_icu_options,
compile_catalog_artifact_with_icu_options, compiled_key, machine_translation_hash,
parse_catalog, pseudolocalize_compiled_catalog_artifact,
pseudolocalize_compiled_catalog_artifact_with_syntax_policy, update_catalog,
update_catalog_file,
};
pub use borrowed::{
BorrowedHeader, BorrowedMsgStr, BorrowedPoFile, BorrowedPoItem, parse_po_borrowed,
};
pub use merge::{ExtractedMessage as MergeExtractedMessage, merge_catalog};
pub use parse::{parse_po, parse_po_bytes};
pub use serialize::stringify_po;
pub use text::{escape_string, extract_quoted, extract_quoted_cow, unescape_string};
use core::{fmt, ops::Index};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
pub struct PoFile {
pub comments: Vec<String>,
pub extracted_comments: Vec<String>,
pub headers: Vec<Header>,
pub items: Vec<PoItem>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
pub struct Header {
pub key: String,
pub value: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
pub struct PoItem {
pub msgid: String,
pub msgctxt: Option<String>,
pub references: Vec<String>,
pub msgid_plural: Option<String>,
pub msgstr: MsgStr,
pub comments: Vec<String>,
pub extracted_comments: Vec<String>,
pub flags: Vec<String>,
pub metadata: Vec<(String, String)>,
pub obsolete: bool,
pub nplurals: usize,
}
impl PoItem {
#[must_use]
pub fn new(nplurals: usize) -> Self {
Self {
nplurals,
..Self::default()
}
}
pub(crate) fn clear_for_reuse(&mut self, nplurals: usize) {
self.msgid.clear();
self.msgctxt = None;
self.references.clear();
self.msgid_plural = None;
self.msgstr = MsgStr::None;
self.comments.clear();
self.extracted_comments.clear();
self.flags.clear();
self.metadata.clear();
self.obsolete = false;
self.nplurals = nplurals;
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(
feature = "serde",
serde(tag = "kind", content = "value", rename_all = "snake_case")
)]
pub enum MsgStr {
#[default]
None,
Singular(String),
Plural(Vec<String>),
}
impl MsgStr {
#[must_use]
pub const fn is_empty(&self) -> bool {
matches!(self, Self::None)
}
#[must_use]
pub fn len(&self) -> usize {
match self {
Self::None => 0,
Self::Singular(_) => 1,
Self::Plural(values) => values.len(),
}
}
#[must_use]
pub fn first(&self) -> Option<&String> {
match self {
Self::None => None,
Self::Singular(value) => Some(value),
Self::Plural(values) => values.first(),
}
}
#[must_use]
pub fn first_str(&self) -> Option<&str> {
self.first().map(String::as_str)
}
#[must_use]
pub fn get(&self, index: usize) -> Option<&str> {
match self {
Self::Singular(value) if index == 0 => Some(value.as_str()),
Self::None | Self::Singular(_) => None,
Self::Plural(values) => values.get(index).map(String::as_str),
}
}
#[must_use]
pub fn iter(&self) -> MsgStrIter<'_> {
match self {
Self::None => MsgStrIter::empty(),
Self::Singular(value) => MsgStrIter::single(value),
Self::Plural(values) => MsgStrIter::many(values.iter()),
}
}
#[must_use]
pub fn into_vec(self) -> Vec<String> {
match self {
Self::None => Vec::new(),
Self::Singular(value) => vec![value],
Self::Plural(values) => values,
}
}
}
impl From<String> for MsgStr {
fn from(value: String) -> Self {
Self::Singular(value)
}
}
impl From<Vec<String>> for MsgStr {
fn from(values: Vec<String>) -> Self {
match values.len() {
0 => Self::None,
1 => Self::Singular(values.into_iter().next().expect("single msgstr value")),
_ => Self::Plural(values),
}
}
}
impl<'a> IntoIterator for &'a MsgStr {
type Item = &'a String;
type IntoIter = MsgStrIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl Index<usize> for MsgStr {
type Output = String;
fn index(&self, index: usize) -> &Self::Output {
match self {
Self::None => panic!("msgstr index out of bounds: no translations present"),
Self::Singular(value) if index == 0 => value,
Self::Singular(_) => panic!("msgstr index out of bounds: singular translation"),
Self::Plural(values) => &values[index],
}
}
}
pub struct MsgStrIter<'a> {
inner: MsgStrIterInner<'a>,
}
enum MsgStrIterInner<'a> {
Empty,
Single(Option<&'a String>),
Many(std::slice::Iter<'a, String>),
}
impl<'a> MsgStrIter<'a> {
const fn empty() -> Self {
Self {
inner: MsgStrIterInner::Empty,
}
}
const fn single(value: &'a String) -> Self {
Self {
inner: MsgStrIterInner::Single(Some(value)),
}
}
const fn many(iter: std::slice::Iter<'a, String>) -> Self {
Self {
inner: MsgStrIterInner::Many(iter),
}
}
}
impl<'a> Iterator for MsgStrIter<'a> {
type Item = &'a String;
fn next(&mut self) -> Option<Self::Item> {
match &mut self.inner {
MsgStrIterInner::Empty => None,
MsgStrIterInner::Single(value) => value.take(),
MsgStrIterInner::Many(iter) => iter.next(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SerializeOptions {
pub fold_length: usize,
pub compact_multiline: bool,
}
impl Default for SerializeOptions {
fn default() -> Self {
Self {
fold_length: 80,
compact_multiline: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParsePosition {
offset: usize,
line: usize,
column: usize,
}
impl ParsePosition {
#[must_use]
pub const fn new(offset: usize, line: usize, column: usize) -> Self {
Self {
offset,
line,
column,
}
}
#[must_use]
pub const fn offset(self) -> usize {
self.offset
}
#[must_use]
pub const fn line(self) -> usize {
self.line
}
#[must_use]
pub const fn column(self) -> usize {
self.column
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseError {
message: String,
position: Option<ParsePosition>,
}
impl ParseError {
#[must_use]
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
position: None,
}
}
#[must_use]
pub fn with_position(message: impl Into<String>, position: ParsePosition) -> Self {
Self {
message: message.into(),
position: Some(position),
}
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
#[must_use]
pub const fn position(&self) -> Option<ParsePosition> {
self.position
}
pub(crate) fn with_position_if_missing(mut self, position: ParsePosition) -> Self {
self.position.get_or_insert(position);
self
}
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for ParseError {}
#[cfg(test)]
mod tests {
use super::{MsgStr, ParseError, ParsePosition};
#[cfg(feature = "serde")]
use super::{Header, PoFile, PoItem};
#[test]
fn parse_error_accessors_preserve_message_and_optional_position() {
let error = ParseError::new("invalid PO string");
assert_eq!(error.message(), "invalid PO string");
assert_eq!(error.position(), None);
assert_eq!(error.to_string(), "invalid PO string");
let position = ParsePosition::new(12, 2, 3);
let positioned = ParseError::with_position("invalid PO string", position);
assert_eq!(positioned.message(), "invalid PO string");
assert_eq!(positioned.position(), Some(position));
assert_eq!(positioned.position().map(ParsePosition::offset), Some(12));
assert_eq!(positioned.position().map(ParsePosition::line), Some(2));
assert_eq!(positioned.position().map(ParsePosition::column), Some(3));
assert_eq!(positioned.to_string(), "invalid PO string");
}
#[test]
fn msgstr_get_returns_none_for_empty_values() {
let msgstr = MsgStr::None;
assert_eq!(msgstr.get(0), None);
}
#[test]
fn msgstr_get_returns_singular_value_at_zero() {
let msgstr = MsgStr::from("Hallo".to_owned());
assert_eq!(msgstr.get(0), Some("Hallo"));
assert_eq!(msgstr.get(1), None);
}
#[test]
fn msgstr_get_returns_plural_values_by_index() {
let msgstr = MsgStr::from(vec!["eins".to_owned(), "viele".to_owned()]);
assert_eq!(msgstr.get(0), Some("eins"));
assert_eq!(msgstr.get(1), Some("viele"));
assert_eq!(msgstr.get(2), None);
}
#[test]
fn msgstr_helpers_cover_empty_singular_and_plural_shapes() {
let empty = MsgStr::from(Vec::<String>::new());
assert!(empty.is_empty());
assert_eq!(empty.len(), 0);
assert_eq!(empty.first(), None);
assert_eq!(empty.first_str(), None);
assert_eq!(empty.iter().count(), 0);
assert_eq!(empty.into_vec(), Vec::<String>::new());
let singular = MsgStr::from(vec!["Hallo".to_owned()]);
assert!(!singular.is_empty());
assert_eq!(singular.len(), 1);
assert_eq!(singular.first().map(String::as_str), Some("Hallo"));
assert_eq!(singular.first_str(), Some("Hallo"));
assert_eq!((&singular).into_iter().collect::<Vec<_>>(), vec!["Hallo"]);
assert_eq!(singular[0], "Hallo");
assert_eq!(singular.into_vec(), vec!["Hallo"]);
let plural = MsgStr::from(vec!["eins".to_owned(), "viele".to_owned()]);
assert_eq!(plural.len(), 2);
assert_eq!(plural.first_str(), Some("eins"));
assert_eq!(plural.iter().collect::<Vec<_>>(), vec!["eins", "viele"]);
assert_eq!(plural[1], "viele");
assert_eq!(plural.into_vec(), vec!["eins", "viele"]);
}
#[cfg(feature = "serde")]
#[test]
fn po_file_serde_round_trips_owned_document_shape() {
let file = PoFile {
comments: vec!["translator note".to_owned()],
headers: vec![Header {
key: "Language".to_owned(),
value: "de".to_owned(),
}],
items: vec![PoItem {
msgid: "Hello".to_owned(),
msgstr: MsgStr::from("Hallo".to_owned()),
references: vec!["src/app.rs:10".to_owned()],
nplurals: 1,
..PoItem::default()
}],
..PoFile::default()
};
let json = serde_json::to_value(&file).expect("PO file serialization must succeed");
assert_eq!(json["items"][0]["msgstr"]["kind"], "singular");
assert_eq!(json["items"][0]["msgstr"]["value"], "Hallo");
let roundtrip: PoFile =
serde_json::from_value(json).expect("PO file deserialization must succeed");
assert_eq!(roundtrip, file);
}
}