#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(missing_docs, rustdoc::broken_intra_doc_links)]
#[cfg(feature = "catalog")]
#[cfg_attr(docsrs, doc(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")]
#[cfg_attr(docsrs, doc(cfg(feature = "catalog")))]
pub use api::{
AiProvenance, 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,
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, CompiledCatalogPseudolocalizationOptions,
CompiledCatalogResolution, CompiledCatalogResolutionKind, CompiledCatalogTranslationKind,
CompiledCatalogUnavailableId, CompiledKeyStrategy, CompiledMessage, CompiledTranslation,
DescribeCompiledIdsReport, Diagnostic, DiagnosticSeverity, EffectiveTranslation,
EffectiveTranslationRef, ExtractedMessage, ExtractedPluralMessage, ExtractedSingularMessage,
IcuFormatterSupportPolicy, IcuPseudolocalizationOptions, IcuSyntaxPolicy, MachineMetadata,
NormalizedParsedCatalog, ObsoleteInfo, ObsoleteStrategy, OrderBy, ParseCatalogOptions,
ParsedCatalog, PlaceholderCommentMode, PluralEncoding, PluralSource, RenderOptions,
SourceExtractedMessage, TranslationShape, UpdateCatalogFileOptions, UpdateCatalogOptions,
audit_catalogs, combine_catalog_files, combine_catalogs, compile_catalog_artifact,
compile_catalog_artifact_report, compile_catalog_artifact_selected, compiled_key,
machine_translation_hash, measure_catalog_coverage, parse_catalog,
pseudolocalize_compiled_catalog_artifact, review_catalogs, update_catalog, update_catalog_file,
};
pub use borrowed::{
BorrowedHeader, BorrowedMsgStr, BorrowedPoFile, BorrowedPoItem, parse_po_borrowed,
};
pub use diagnostic_codes::DiagnosticCode;
pub use merge::{MergeMessageInput, 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,
iter::FusedIterator,
ops::{Deref, DerefMut, Index},
slice,
};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use smallvec::{IntoIter as SmallVecIntoIter, SmallVec};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct PoVec<T>(SmallVec<[T; 1]>);
impl<T> PoVec<T> {
#[must_use]
pub fn new() -> Self {
Self(SmallVec::new())
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self(SmallVec::with_capacity(capacity))
}
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[must_use]
pub fn as_slice(&self) -> &[T] {
self.0.as_slice()
}
#[must_use]
pub fn as_mut_slice(&mut self) -> &mut [T] {
self.0.as_mut_slice()
}
pub fn iter(&self) -> slice::Iter<'_, T> {
self.as_slice().iter()
}
pub fn iter_mut(&mut self) -> slice::IterMut<'_, T> {
self.as_mut_slice().iter_mut()
}
pub fn push(&mut self, value: T) {
self.0.push(value);
}
pub fn clear(&mut self) {
self.0.clear();
}
#[must_use]
pub fn into_vec(self) -> Vec<T> {
self.0.into_vec()
}
}
impl<T> AsRef<[T]> for PoVec<T> {
fn as_ref(&self) -> &[T] {
self.as_slice()
}
}
impl<T> AsMut<[T]> for PoVec<T> {
fn as_mut(&mut self) -> &mut [T] {
self.as_mut_slice()
}
}
impl<T> Deref for PoVec<T> {
type Target = [T];
fn deref(&self) -> &Self::Target {
self.as_slice()
}
}
impl<T> DerefMut for PoVec<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.as_mut_slice()
}
}
impl<T> Extend<T> for PoVec<T> {
fn extend<I>(&mut self, iter: I)
where
I: IntoIterator<Item = T>,
{
self.0.extend(iter);
}
}
impl<T> From<Vec<T>> for PoVec<T> {
fn from(value: Vec<T>) -> Self {
Self(SmallVec::from_vec(value))
}
}
impl<T, const N: usize> From<[T; N]> for PoVec<T> {
fn from(value: [T; N]) -> Self {
Self(value.into_iter().collect())
}
}
impl<T> FromIterator<T> for PoVec<T> {
fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator<Item = T>,
{
Self(iter.into_iter().collect())
}
}
impl<T> From<PoVec<T>> for Vec<T> {
fn from(value: PoVec<T>) -> Self {
value.into_vec()
}
}
impl<T> IntoIterator for PoVec<T> {
type Item = T;
type IntoIter = PoVecIntoIter<T>;
fn into_iter(self) -> Self::IntoIter {
PoVecIntoIter {
inner: self.0.into_iter(),
}
}
}
impl<'a, T> IntoIterator for &'a PoVec<T> {
type Item = &'a T;
type IntoIter = slice::Iter<'a, T>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl<'a, T> IntoIterator for &'a mut PoVec<T> {
type Item = &'a mut T;
type IntoIter = slice::IterMut<'a, T>;
fn into_iter(self) -> Self::IntoIter {
self.iter_mut()
}
}
impl<T, U> PartialEq<[U]> for PoVec<T>
where
T: PartialEq<U>,
{
fn eq(&self, other: &[U]) -> bool {
self.as_slice() == other
}
}
impl<T, U> PartialEq<&[U]> for PoVec<T>
where
T: PartialEq<U>,
{
fn eq(&self, other: &&[U]) -> bool {
self.as_slice() == *other
}
}
impl<T, U> PartialEq<Vec<U>> for PoVec<T>
where
T: PartialEq<U>,
{
fn eq(&self, other: &Vec<U>) -> bool {
self.as_slice() == other.as_slice()
}
}
pub struct PoVecIntoIter<T> {
inner: SmallVecIntoIter<[T; 1]>,
}
impl<T> Iterator for PoVecIntoIter<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<T> DoubleEndedIterator for PoVecIntoIter<T> {
fn next_back(&mut self) -> Option<Self::Item> {
self.inner.next_back()
}
}
impl<T> ExactSizeIterator for PoVecIntoIter<T> {}
impl<T> FusedIterator for PoVecIntoIter<T> {}
#[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: PoVec<String>,
pub msgid_plural: Option<String>,
pub msgstr: MsgStr,
pub comments: PoVec<String>,
pub extracted_comments: PoVec<String>,
pub flags: PoVec<String>,
pub metadata: PoVec<(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 fn plural(values: Vec<String>) -> Self {
Self::Plural(values)
}
#[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<&str> {
match self {
Self::None => None,
Self::Singular(value) => Some(value.as_str()),
Self::Plural(values) => values.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.as_str()),
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 str;
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 str>),
Many(std::slice::Iter<'a, String>),
}
impl<'a> MsgStrIter<'a> {
const fn empty() -> Self {
Self {
inner: MsgStrIterInner::Empty,
}
}
const fn single(value: &'a str) -> 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 str;
fn next(&mut self) -> Option<Self::Item> {
match &mut self.inner {
MsgStrIterInner::Empty => None,
MsgStrIterInner::Single(value) => value.take(),
MsgStrIterInner::Many(iter) => iter.next().map(String::as_str),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
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,
}
}
}
impl SerializeOptions {
#[must_use]
pub fn with_fold_length(mut self, fold_length: usize) -> Self {
self.fold_length = fold_length;
self
}
#[must_use]
pub fn with_compact_multiline(mut self, compact_multiline: bool) -> Self {
self.compact_multiline = compact_multiline;
self
}
}
#[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, PoVec, SerializeOptions};
#[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_plural_constructor_preserves_single_slot_shape() {
let values = vec!["translation".to_owned()];
let plural = MsgStr::plural(values.clone());
assert_eq!(plural, MsgStr::Plural(values.clone()));
assert_ne!(plural, MsgStr::from(values.clone()));
assert_eq!(plural.len(), 1);
assert_eq!(plural.first(), Some("translation"));
assert_eq!(plural.into_vec(), values);
}
#[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.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(), 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(), Some("eins"));
assert_eq!(plural.iter().collect::<Vec<_>>(), vec!["eins", "viele"]);
assert_eq!(plural[1], "viele");
assert_eq!(plural.into_vec(), vec!["eins", "viele"]);
}
#[test]
fn povec_keeps_slice_iteration_and_vec_conversion_ergonomics() {
let mut values = PoVec::new();
assert!(values.is_empty());
values.push("src/app.rs:10".to_owned());
values.extend(["src/app.rs:20".to_owned()]);
assert_eq!(values.len(), 2);
assert_eq!(
values.as_slice(),
["src/app.rs:10".to_owned(), "src/app.rs:20".to_owned()]
);
assert_eq!(
values.iter().map(String::as_str).collect::<Vec<_>>(),
["src/app.rs:10", "src/app.rs:20"]
);
let from_vec = PoVec::from(vec!["fuzzy".to_owned()]);
assert_eq!(from_vec, vec!["fuzzy".to_owned()]);
assert_eq!(Vec::<String>::from(from_vec), vec!["fuzzy".to_owned()]);
}
#[test]
fn povec_trait_views_cover_mutable_slice_and_reference_iteration() {
let mut values = PoVec::with_capacity(2);
values.extend([1, 2]);
assert_eq!(AsRef::<[i32]>::as_ref(&values), [1, 2]);
AsMut::<[i32]>::as_mut(&mut values)[0] = 3;
values.as_mut_slice()[1] = 4;
for value in &mut values {
*value += 1;
}
values.iter_mut().for_each(|value| *value *= 2);
let as_slice: &[i32] = &values;
assert_eq!(as_slice, [8, 10]);
let as_mut_slice: &mut [i32] = &mut values;
as_mut_slice[0] += 1;
let expected = [9, 10];
assert!(values == expected[..]);
assert!(values == expected.as_slice());
}
#[test]
fn povec_owned_iterator_preserves_values_without_exposing_backing_type() {
let values = PoVec::from(["one".to_owned(), "other".to_owned()]);
assert_eq!(
values.into_iter().collect::<Vec<_>>(),
vec!["one".to_owned(), "other".to_owned()]
);
}
#[test]
fn povec_owned_iterator_supports_double_ended_size_hints() {
let mut iter = PoVec::from([1, 2, 3]).into_iter();
assert_eq!(iter.size_hint(), (3, Some(3)));
assert_eq!(iter.next_back(), Some(3));
assert_eq!(iter.size_hint(), (2, Some(2)));
assert_eq!(iter.next(), Some(1));
assert_eq!(iter.next_back(), Some(2));
assert_eq!(iter.next(), None);
assert_eq!(iter.next_back(), None);
}
#[test]
fn serialize_option_builders_set_fields() {
let options = SerializeOptions::default()
.with_fold_length(120)
.with_compact_multiline(false);
assert_eq!(options.fold_length, 120);
assert!(!options.compact_multiline);
}
#[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()].into(),
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);
}
}