use std::collections::HashMap;
use std::fs::File;
use std::hash::Hash;
use std::io::{BufRead, BufReader, Lines, Read};
use std::iter::Iterator;
use std::path::Path;
use lazy_static::lazy_static;
use crate::entry::POEntry;
use crate::errors::{MaybeFilename, SyntaxError};
use crate::file::{pofile::POFile, FileOptions};
#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug)]
pub enum St {
ST, HE, TC, GC, OC, FL, CT, PC, PM, PP, MI, MP, MS, MX, MC, }
include!(concat!(env!("OUT_DIR"), "/poparser-transitions.rs"));
lazy_static! {
static ref TRANSITIONS: Transitions = build_transitions();
static ref KEYWORDS: HashMap<String, &'static St> = {
let mut m = HashMap::new();
m.insert("msgctxt".to_string(), &St::CT);
m.insert("msgid".to_string(), &St::MI);
m.insert("msgstr".to_string(), &St::MS);
m.insert("msgid_plural".to_string(), &St::MP);
m
};
static ref PREV_KEYWORDS: HashMap<String, &'static St> = {
let mut m = HashMap::new();
m.insert("msgid_plural".to_string(), &St::PP);
m.insert("msgid".to_string(), &St::PM);
m.insert("msgctxt".to_string(), &St::PC);
m
};
}
type TransitionFn =
dyn Fn(&mut POFileParser) -> Result<(), SyntaxError>;
type Symbol = St;
type CurrentSt = St;
type Action = St;
type NextSt = St;
pub type Transitions = HashMap<(Symbol, CurrentSt), (Action, NextSt)>;
struct LinesHandler<'a> {
lines: Lines<BufReader<&'a mut dyn Read>>,
}
impl LinesHandler<'_> {
fn new(handler: &mut dyn Read) -> LinesHandler<'_> {
LinesHandler {
lines: BufReader::new(handler).lines(),
}
}
}
impl Iterator for LinesHandler<'_> {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
match self.lines.next() {
Some(Ok(line)) => Some(line),
Some(Err(_)) => None,
None => None,
}
}
}
pub(crate) struct POFileParser {
content_is_path: bool,
pub file: POFile,
current_state: St,
current_token: String,
current_line: usize,
current_entry: POEntry,
msgstr_index: usize,
entry_obsolete: bool,
}
impl POFileParser {
pub fn new(file_options: FileOptions) -> POFileParser {
POFileParser {
content_is_path: Path::new(&file_options.path_or_content)
.is_file(),
file: POFile::new(file_options),
current_state: St::ST,
current_token: String::with_capacity(32),
current_line: 0,
current_entry: POEntry::new(0),
msgstr_index: 0,
entry_obsolete: false,
}
}
fn add_current_entry(&mut self) -> Result<(), SyntaxError> {
let unescaped_entry = self.current_entry.unescaped();
if unescaped_entry.is_err() {
return Err(SyntaxError::BasicCustom {
maybe_filename: MaybeFilename::new(
&self.file.options.path_or_content,
self.content_is_path,
),
message: unescaped_entry.err().unwrap().to_string(),
});
}
self.file.entries.push(unescaped_entry.unwrap());
self.current_entry = POEntry::new(self.current_line);
self.msgstr_index = 0;
Ok(())
}
fn maybe_add_current_entry(&mut self) -> Result<(), SyntaxError> {
if [St::MC, St::MS, St::MX].contains(&self.current_state) {
self.add_current_entry()?;
}
Ok(())
}
fn entry_started(&self) -> bool {
!self.current_entry.msgid.is_empty()
}
fn missing_msgstr_error(&self, line: usize) -> SyntaxError {
SyntaxError::Custom {
maybe_filename: MaybeFilename::new(
&self.file.options.path_or_content,
self.content_is_path,
),
line,
index: 0,
message: "missing 'msgstr' section".to_string(),
}
}
fn missing_plural_msgstr_error(
&self,
line: usize,
) -> SyntaxError {
SyntaxError::Custom {
maybe_filename: MaybeFilename::new(
&self.file.options.path_or_content,
self.content_is_path,
),
line,
index: 0,
message: "missing plural 'msgstr[n]' section".to_string(),
}
}
fn validate_current_entry_complete(
&self,
line: usize,
) -> Result<(), SyntaxError> {
if self.current_entry.msgstr.is_none()
&& self.current_entry.msgid_plural.is_none()
{
return Err(self.missing_msgstr_error(line));
}
if self.current_entry.msgid_plural.is_some()
&& self.current_entry.msgstr_plural.is_empty()
{
return Err(self.missing_plural_msgstr_error(line));
}
Ok(())
}
fn invalid_previous_continuation_error(
&self,
line: usize,
) -> SyntaxError {
SyntaxError::Custom {
maybe_filename: MaybeFilename::new(
&self.file.options.path_or_content,
self.content_is_path,
),
line,
index: 0,
message: "invalid previous continuation line".to_string(),
}
}
fn process(
&mut self,
symbol: &Symbol,
) -> Result<(), SyntaxError> {
let next_transition = (*symbol, self.current_state);
let (action, next_state) =
*TRANSITIONS.get(&next_transition).unwrap();
(transition_fn_factory(action)?)(self)?;
if action != St::MC {
self.current_state = next_state;
}
Ok(())
}
pub fn parse(&mut self) -> Result<(), SyntaxError> {
if self.content_is_path {
self.parse_file()?;
} else {
self.parse_content()?;
}
Ok(())
}
fn parse_file(&mut self) -> Result<(), SyntaxError> {
let mut buf = BufReader::new(
File::open(&self.file.options.path_or_content).unwrap(),
);
let mut handler = LinesHandler::new(&mut buf);
self.parse_with_handler(&mut handler)?;
Ok(())
}
fn parse_content(&mut self) -> Result<(), SyntaxError> {
let content = self.file.options.path_or_content.clone();
let mut buf = BufReader::new(content.as_bytes());
let mut handler = LinesHandler::new(&mut buf);
self.parse_with_handler(&mut handler)?;
Ok(())
}
fn parse_with_handler(
&mut self,
handler: &mut LinesHandler,
) -> Result<(), SyntaxError> {
let first_line = handler.next().unwrap_or("".to_string());
self.parse_line(maybe_lstrip_utf8_bom(&first_line))?;
for line in handler.by_ref() {
self.parse_line(&line)?;
}
if !self.entry_started() {
if let Some(msgstr) = &self.current_entry.msgstr {
if !msgstr.is_empty() {
self.add_current_entry()?;
}
}
} else {
self.validate_current_entry_complete(self.current_line)?;
self.add_current_entry()?;
}
let metadata_entry = self.file.find_by_msgid("");
if let Some(metadata_entry) = metadata_entry {
self.file.metadata_is_fuzzy =
!metadata_entry.flags.is_empty();
self.file.remove(&metadata_entry);
for metadata_line in
metadata_entry.msgstr.unwrap().split('\n')
{
let (key, value) =
match metadata_line.split_once(": ") {
Some((key, value)) => (key, value),
None => continue,
};
if !self.file.metadata.contains_key(key) {
self.file.metadata.insert(
key.to_string(),
value.trim().to_string(),
);
} else {
let mut new_value =
self.file.metadata.remove(key).unwrap();
new_value.push_str(value.trim());
self.file
.metadata
.insert(key.to_string(), new_value);
}
}
}
Ok(())
}
fn tokens_from_line(&self, line: &str) -> Vec<String> {
let mut tokens: Vec<String> = Vec::with_capacity(3);
for token in line.split_ascii_whitespace() {
tokens.push(token.to_string());
if tokens.len() == 3 {
break;
}
}
tokens
}
fn parse_line(&mut self, line: &str) -> Result<(), SyntaxError> {
self.current_line += 1;
let mut line = line.trim();
if line.is_empty() {
return Ok(());
}
let mut tokens = self.tokens_from_line(line);
let mut nb_tokens = tokens.len();
if nb_tokens == 0 || tokens[0] == "#~|" {
return Ok(());
} else if nb_tokens > 1 && tokens[0] == "#~" {
line = line[3..].trim();
tokens = tokens[1..].to_vec();
nb_tokens -= 1;
self.entry_obsolete = true
} else {
self.entry_obsolete = false;
}
if nb_tokens > 1 && KEYWORDS.contains_key(&tokens[0]) {
line = line[tokens[0].len()..].trim_start();
let symbol = *KEYWORDS.get(&tokens[0]).unwrap();
if [St::CT, St::MI].contains(symbol)
&& self.entry_started()
{
self.validate_current_entry_complete(
self.current_line,
)?;
}
maybe_raise_unescaped_double_quote_found_error(
line,
self.current_line,
self.content_is_path,
&self.file.options.path_or_content,
tokens[0].chars().count() + 2,
)?;
self.current_token = line.to_string();
self.process(symbol)?;
return Ok(());
}
self.current_token = line.to_string();
if tokens[0] == "#:" {
if nb_tokens <= 1 {
return Ok(());
}
self.process(&St::OC)?;
} else if line.starts_with('"') {
maybe_raise_unescaped_double_quote_found_error(
line,
self.current_line,
self.content_is_path,
&self.file.options.path_or_content,
1,
)?;
self.process(&St::MC)?;
} else if self.current_token.starts_with("msgstr[") {
let index = self
.current_token
.splitn(2, '[')
.last()
.unwrap()
.split(']')
.next()
.unwrap();
match index.parse::<usize>() {
Ok(index) => {
self.msgstr_index = index;
}
Err(_) => {
return Err(SyntaxError::Custom {
maybe_filename: MaybeFilename::new(
&self.file.options.path_or_content,
self.content_is_path,
),
message: format!(
concat!(
"Invalid msgstr plural index.",
" Expected digit, found '{}'."
),
index,
),
line: self.current_line,
index: 7,
});
}
};
self.process(&St::MX)?;
} else if tokens[0] == "#," {
if nb_tokens < 2 {
return Ok(());
}
self.process(&St::FL)?;
} else if tokens[0] == "#" || tokens[0].starts_with("##") {
self.process(&St::TC)?;
} else if tokens[0] == "#." {
if nb_tokens < 2 {
return Ok(());
}
self.process(&St::GC)?;
} else if tokens[0] == "#|" {
if nb_tokens < 2 {
return Err(SyntaxError::Custom {
maybe_filename: MaybeFilename::new(
&self.file.options.path_or_content,
self.content_is_path,
),
line: self.current_line,
index: 2,
message: "empty previous message found"
.to_string(),
});
}
if tokens[1].starts_with('"') {
if ![St::PM, St::PP, St::PC]
.contains(&self.current_state)
{
return Err(self
.invalid_previous_continuation_error(
self.current_line,
));
}
let quote_index = match line.find('"') {
Some(index) => index,
None => {
return Err(self
.invalid_previous_continuation_error(
self.current_line,
));
}
};
let continuation = &line[quote_index..];
maybe_raise_unescaped_double_quote_found_error(
continuation,
self.current_line,
self.content_is_path,
&self.file.options.path_or_content,
quote_index + 1,
)?;
self.current_token = continuation.to_string();
self.process(&St::MC)?;
return Ok(());
}
if nb_tokens == 2 {
return Err(SyntaxError::Custom {
message: "invalid continuation line".to_string(),
maybe_filename: MaybeFilename::new(
&self.file.options.path_or_content,
self.content_is_path,
),
line: self.current_line,
index: 0,
});
}
if !PREV_KEYWORDS.contains_key(&tokens[1]) {
return Err(SyntaxError::Custom {
message: format!("unknown keyword {}", tokens[1]),
maybe_filename: MaybeFilename::new(
&self.file.options.path_or_content,
self.content_is_path,
),
line: self.current_line,
index: 0,
});
}
self.current_token = line
[tokens[1].len() + tokens[0].len() + 1..]
.trim_start()
.to_string();
self.process(PREV_KEYWORDS.get(&tokens[1]).unwrap())?;
} else {
return Err(SyntaxError::Generic {
maybe_filename: MaybeFilename::new(
&self.file.options.path_or_content,
self.content_is_path,
),
line: self.current_line,
index: 0,
});
}
Ok(())
}
}
fn handle_he(parser: &mut POFileParser) -> Result<(), SyntaxError> {
let h = parser.file.header.clone();
let mut newheader = h.unwrap_or_default().to_string();
if !newheader.is_empty() {
newheader.push('\n');
}
if parser.current_token.len() > 2 {
newheader.push_str(&parser.current_token[2..]);
}
parser.file.header = Some(newheader);
Ok(())
}
fn handle_tc(parser: &mut POFileParser) -> Result<(), SyntaxError> {
parser.maybe_add_current_entry()?;
let optional_tcomment = parser.current_entry.tcomment.as_mut();
let mut tcomment = match optional_tcomment {
Some(tcomment) => {
let t = tcomment;
t.push('\n');
t
}
None => "",
}
.to_string();
let mut toappend =
parser.current_token.trim_start_matches('#').to_string();
if toappend.starts_with(' ') {
toappend = toappend[1..].to_string();
}
tcomment.push_str(&toappend);
parser.current_entry.tcomment =
Some(tcomment.as_str().to_string());
Ok(())
}
fn handle_gc(parser: &mut POFileParser) -> Result<(), SyntaxError> {
parser.maybe_add_current_entry()?;
let optional_comment = parser.current_entry.comment.as_mut();
let mut comment = match optional_comment {
Some(comment) => {
let t = comment;
t.push('\n');
t
}
None => "",
}
.to_string();
if parser.current_token.len() > 3 {
comment.push_str(&parser.current_token[3..]);
}
parser.current_entry.comment = Some(comment);
Ok(())
}
fn handle_oc(parser: &mut POFileParser) -> Result<(), SyntaxError> {
parser.maybe_add_current_entry()?;
for occ in parser.current_token[3..].split_whitespace() {
if !occ.is_empty() {
let (mut fil, mut line) =
occ.split_once(':').unwrap_or((occ, ""));
let mut line_isdigit = true;
for c in line.chars() {
if !c.is_ascii_digit() {
line_isdigit = false;
break;
}
}
if !line_isdigit {
fil = occ;
line = "";
}
parser
.current_entry
.occurrences
.push((fil.to_string(), line.to_string()))
}
}
Ok(())
}
fn handle_fl(parser: &mut POFileParser) -> Result<(), SyntaxError> {
parser.maybe_add_current_entry()?;
if parser.current_token.len() > 3 {
let current_token_split =
parser.current_token[3..].split(',');
for substr in current_token_split {
parser
.current_entry
.flags
.push(substr.trim().to_string());
}
}
Ok(())
}
fn handle_pp(parser: &mut POFileParser) -> Result<(), SyntaxError> {
parser.maybe_add_current_entry()?;
parser.current_entry.previous_msgid_plural = Some(
parser.current_token[1..parser.current_token.len() - 1]
.to_string(),
);
Ok(())
}
fn handle_pm(parser: &mut POFileParser) -> Result<(), SyntaxError> {
parser.maybe_add_current_entry()?;
parser.current_entry.previous_msgid = Some(
parser.current_token[1..parser.current_token.len() - 1]
.to_string(),
);
Ok(())
}
fn handle_pc(parser: &mut POFileParser) -> Result<(), SyntaxError> {
parser.maybe_add_current_entry()?;
parser.current_entry.previous_msgctxt = Some(
parser.current_token[1..parser.current_token.len() - 1]
.to_string(),
);
Ok(())
}
fn handle_ct(parser: &mut POFileParser) -> Result<(), SyntaxError> {
parser.maybe_add_current_entry()?;
parser.current_entry.msgctxt = Some(
parser.current_token[1..parser.current_token.len() - 1]
.to_string(),
);
Ok(())
}
fn handle_mi(parser: &mut POFileParser) -> Result<(), SyntaxError> {
parser.maybe_add_current_entry()?;
parser.current_entry.obsolete = parser.entry_obsolete;
parser.current_entry.msgid = parser.current_token
[1..parser.current_token.len() - 1]
.to_string();
Ok(())
}
fn handle_mp(parser: &mut POFileParser) -> Result<(), SyntaxError> {
parser.current_entry.msgid_plural = Some(
parser.current_token[1..parser.current_token.len() - 1]
.to_string(),
);
Ok(())
}
fn handle_ms(parser: &mut POFileParser) -> Result<(), SyntaxError> {
parser.current_entry.msgstr = Some(
parser.current_token[1..parser.current_token.len() - 1]
.to_string(),
);
Ok(())
}
fn handle_mx(parser: &mut POFileParser) -> Result<(), SyntaxError> {
let value =
&parser.current_token[parser.current_token.find('"').unwrap()
+ 1
..parser.current_token.len() - 1];
let msgstr_plural_length =
parser.current_entry.msgstr_plural.len();
if parser.msgstr_index + 1 > msgstr_plural_length {
for _ in 0..parser.msgstr_index + 1 - msgstr_plural_length {
parser.current_entry.msgstr_plural.push("".to_string());
}
}
parser.current_entry.msgstr_plural[parser.msgstr_index] =
value.to_string();
Ok(())
}
fn handle_mc(parser: &mut POFileParser) -> Result<(), SyntaxError> {
let token =
&parser.current_token[1..&parser.current_token.len() - 1];
if parser.current_state == St::MI {
parser.current_entry.msgid.push_str(token);
} else if parser.current_state == St::MS {
let msgstr = parser.current_entry.msgstr.as_mut().unwrap();
msgstr.push_str(token);
parser.current_entry.msgstr = Some(msgstr.to_string());
} else if parser.current_state == St::CT {
let msgctxt = parser.current_entry.msgctxt.as_mut().unwrap();
msgctxt.push_str(token);
parser.current_entry.msgctxt = Some(msgctxt.to_string());
} else if parser.current_state == St::MP {
let msgid_plural =
parser.current_entry.msgid_plural.as_mut().unwrap();
msgid_plural.push_str(token);
parser.current_entry.msgid_plural =
Some(msgid_plural.to_string());
} else if parser.current_state == St::MX {
parser.current_entry.msgstr_plural[parser.msgstr_index]
.push_str(token);
} else if parser.current_state == St::PP {
let previous_msgid_plural = parser
.current_entry
.previous_msgid_plural
.as_mut()
.unwrap();
previous_msgid_plural.push_str(token);
parser.current_entry.previous_msgid_plural =
Some(previous_msgid_plural.to_string());
} else if parser.current_state == St::PM {
let previous_msgid =
parser.current_entry.previous_msgid.as_mut().unwrap();
previous_msgid.push_str(token);
parser.current_entry.previous_msgid =
Some(previous_msgid.to_string());
} else if parser.current_state == St::PC {
let previous_msgctxt =
parser.current_entry.previous_msgctxt.as_mut().unwrap();
previous_msgctxt.push_str(token);
parser.current_entry.previous_msgctxt =
Some(previous_msgctxt.to_string());
} else {
return Err(SyntaxError::Custom {
maybe_filename: MaybeFilename::new(
&parser.file.options.path_or_content,
parser.content_is_path,
),
message: format!(
"unexpected state {:?}",
parser.current_state
),
line: parser.current_line,
index: 0,
});
}
Ok(())
}
fn transition_fn_factory(
action: Action,
) -> Result<&'static TransitionFn, SyntaxError> {
match action {
St::HE => Ok(&handle_he),
St::TC => Ok(&handle_tc),
St::GC => Ok(&handle_gc),
St::OC => Ok(&handle_oc),
St::FL => Ok(&handle_fl),
St::PP => Ok(&handle_pp),
St::PM => Ok(&handle_pm),
St::PC => Ok(&handle_pc),
St::CT => Ok(&handle_ct),
St::MI => Ok(&handle_mi),
St::MP => Ok(&handle_mp),
St::MS => Ok(&handle_ms),
St::MX => Ok(&handle_mx),
St::MC => Ok(&handle_mc),
_ => Err(SyntaxError::UnknownState {
state: format!("{action:?}"),
}),
}
}
#[inline(always)]
fn maybe_lstrip_utf8_bom(line: &str) -> &str {
line.trim_start_matches('\u{feff}')
}
fn find_unescaped_double_quote_index(line: &str) -> Option<usize> {
let mut escaped = false;
for (i, c) in line.chars().enumerate() {
if c == '"' && !escaped {
return Some(i);
} else if c == '\\' {
escaped = !escaped;
} else {
escaped = false;
}
}
None
}
fn maybe_raise_unescaped_double_quote_found_error(
text: &str,
linenum: usize,
path_or_content_is_path: bool,
path_or_content: &str,
index_offset: usize,
) -> Result<(), SyntaxError> {
let text_chars = text.chars();
if text_chars.last().unwrap_or('\0') != '"' {
return Err(SyntaxError::Custom {
maybe_filename: MaybeFilename::new(
path_or_content,
path_or_content_is_path,
),
line: linenum,
index: text.chars().count() - 1,
message: format!("unterminated string '{text}'"),
});
}
let text_str = text.to_string();
let unescaped_double_quote_i = find_unescaped_double_quote_index(
&text_str[1..text_str.len() - 1],
);
if let Some(double_quote_i) = unescaped_double_quote_i {
return Err(SyntaxError::UnescapedDoubleQuoteFound {
maybe_filename: MaybeFilename::new(
path_or_content,
path_or_content_is_path,
),
line: linenum,
index: double_quote_i + 1 + index_offset,
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn constructor() {
let path = "tests-data/empty.po";
let content: String = fs::read_to_string(path).unwrap();
let parser = POFileParser::new(path.into());
assert_eq!(parser.file.options.path_or_content, path);
assert_eq!(parser.content_is_path, true);
assert_eq!(parser.file.options.wrapwidth, 78);
assert_eq!(parser.current_line, 0);
assert_eq!(parser.current_entry.msgid, "");
assert_eq!(parser.current_entry.linenum, 0);
assert_eq!(parser.msgstr_index, 0);
let parser = POFileParser::new((path, 30).into());
assert_eq!(parser.file.options.path_or_content, path);
assert_eq!(parser.content_is_path, true);
assert_eq!(parser.file.options.wrapwidth, 30);
let parser = POFileParser::new(content.as_str().into());
assert_eq!(parser.file.options.path_or_content, content);
assert_eq!(parser.content_is_path, false);
assert_eq!(parser.file.options.wrapwidth, 78);
}
#[test]
fn parse_empty_file() -> Result<(), SyntaxError> {
let path = "tests-data/empty.po";
let mut parser = POFileParser::new(path.into());
parser.parse()?;
assert_eq!(parser.file.entries.len(), 0);
assert_eq!(parser.file.metadata.len(), 0);
Ok(())
}
#[test]
fn parse_empty_content() -> Result<(), SyntaxError> {
let mut parser = POFileParser::new("".into());
parser.parse()?;
assert_eq!(parser.file.entries.len(), 0);
assert_eq!(parser.file.metadata.len(), 0);
Ok(())
}
#[test]
fn parse_utf8_bom() -> Result<(), SyntaxError> {
let path = "tests-data/utf8-bom.po";
let mut parser = POFileParser::new(path.into());
parser.parse()?;
assert_eq!(
parser.file.header,
Some("This file contains an UTF8 BOM".to_string()),
);
assert_eq!(parser.file.metadata.len(), 0);
assert_eq!(parser.file.entries.len(), 0);
Ok(())
}
#[test]
fn parse_header() -> Result<(), SyntaxError> {
let path = "tests-data/header-no-trailing-newline.po";
let mut parser = POFileParser::new(path.into());
parser.parse()?;
assert_eq!(
parser.file.header,
Some(
concat!(
"This file is distributed under the same license",
"\n\n",
"Translators:",
"\n",
"Foo bar, Year",
"\n\n",
"Baz, YEAR",
)
.to_string()
),
);
let path = "tests-data/header-trailing-newlines.po";
let mut parser = POFileParser::new(path.into());
parser.parse()?;
assert_eq!(
parser.file.header,
Some(
concat!(
"This file is distributed under the same license\n\n",
"Translators:\n",
"Foo bar, Year\n\n",
"Baz, YEAR\n\n\n",
)
.to_string()
),
);
assert_eq!(parser.file.entries.len(), 0);
Ok(())
}
#[test]
fn parse_metadata() -> Result<(), SyntaxError> {
let path = "tests-data/metadata.po";
let mut parser = POFileParser::new(path.into());
parser.parse()?;
assert_eq!(parser.file.metadata.len(), 11);
assert_eq!(parser.file.entries.len(), 0);
let metadata = HashMap::from([
("Plural-Forms", "nplurals=2; plural=(n != 1);"),
("POT-Creation-Date", "2020-05-19 20:23+0200"),
("Content-Transfer-Encoding", "8bit"),
("MIME-Version", "1.0"),
("Report-Msgid-Bugs-To", "mondeja"),
("PO-Revision-Date", "2020-09-28 03:17+0000"),
("Project-Id-Version", "django"),
("Last-Translator", "Foo Bar <foobar@gmail.com>"),
("Content-Type", "text/plain; charset=UTF-8"),
("Language", "es"),
(
"Language-Team",
concat!(
"Spanish",
" (http://www.transifex.com/",
"django/django/language/es/)",
),
),
]);
for (key, value) in metadata.iter() {
assert_eq!(
parser.file.metadata.get(&key as &str).unwrap(),
value
);
}
Ok(())
}
#[test]
fn parse_msgids_msgstrs() -> Result<(), SyntaxError> {
let path = "tests-data/msgids-msgstrs.po";
let mut parser = POFileParser::new(path.into());
parser.parse()?;
assert_eq!(parser.file.entries.len(), 2);
let first_entry = &parser.file.entries[0];
let second_entry = &parser.file.entries[1];
assert_eq!(first_entry.msgid, "msgid 1");
assert_eq!(first_entry.msgstr.as_ref().unwrap(), "msgstr 1");
assert_eq!(first_entry.obsolete, false);
assert_eq!(second_entry.msgid, "msgid 2");
assert_eq!(second_entry.msgstr.as_ref().unwrap(), "msgstr 2");
assert_eq!(second_entry.obsolete, false);
Ok(())
}
#[test]
fn parse_long_message() -> Result<(), SyntaxError> {
let path = "tests-data/long-message.po";
let mut parser = POFileParser::new(path.into());
parser.parse()?;
assert_eq!(parser.file.entries.len(), 1);
let entry = &parser.file.entries[0];
assert_eq!(
entry.msgid,
concat!(
"Enter a valid “slug” consisting of letters, numbers,",
" underscores or hyphens.",
),
);
assert_eq!(
entry.msgstr.as_ref().unwrap(),
concat!(
"Introduzca un 'slug' válido, consistente en letras,",
" nĂşmeros, guiones bajos o medios.",
),
);
assert_eq!(
entry.comment.as_ref().unwrap(),
"This is a generated/extracted comment",
);
Ok(())
}
#[test]
fn parse_long_msgids_msgstrs() -> Result<(), SyntaxError> {
let path = "tests-data/msgid-msgstr-long.po";
let mut parser = POFileParser::new(path.into());
parser.parse()?;
let po_content = fs::read_to_string(path).unwrap();
assert_eq!(parser.file.entries.len(), 2);
assert_eq!(parser.file.metadata.len(), 0);
let first_entry = &parser.file.entries[0];
let second_entry = &parser.file.entries[1];
assert_eq!(
first_entry.msgid,
concat!(
"msgid 1 msgid 1 msgid 1 msgid 1 msgid 1",
" msgid 1 msgid 1 msgid 1 msgid 1 msgid 1",
" msgid 1 msgid 1",
)
);
assert_eq!(
first_entry.msgid.len(),
po_content.lines().nth(0).unwrap().len()
- "msgid ".len()
- 2,
);
let msgstr = first_entry.msgstr.as_ref().unwrap();
assert_eq!(
msgstr,
concat!(
"msgstr 1 msgstr 1 msgstr 1 msgstr 1 msgstr 1",
" msgstr 1 msgstr 1 msgstr 1 msgstr 1 msgstr 1",
" msgstr 1",
)
);
assert_eq!(
msgstr.len(),
po_content.lines().nth(1).unwrap().len()
- "msgstr ".len()
- 2,
);
let expected_msgid_msgstr = concat!(
"\n<p class=\"help\">To install bookmarklets, drag",
" the link to your bookmarks\ntoolbar, or right-click",
" the link and add it to your bookmarks. Now you can\n",
"select the bookmarklet from any page in the site.",
" Note that some of these\nbookmarklets require you to",
" be viewing the site from a computer designated\n",
"as \"internal\" (talk to your system administrator",
" if you aren't sure if\nyour computer is \"internal\").",
"</p>\n",
);
assert_eq!(second_entry.msgid, expected_msgid_msgstr);
assert_eq!(
second_entry.msgstr.as_ref().unwrap(),
expected_msgid_msgstr
);
Ok(())
}
#[test]
fn parse_flags() -> Result<(), SyntaxError> {
let path = "tests-data/flags.po";
let mut parser = POFileParser::new(path.into());
parser.parse()?;
assert_eq!(parser.file.entries.len(), 6);
let entry_1 = &parser.file.entries[0];
let entry_2 = &parser.file.entries[1];
let entry_3 = &parser.file.entries[2];
let entry_4 = &parser.file.entries[3];
let entry_5 = &parser.file.entries[4];
let entry_6 = &parser.file.entries[5];
assert_eq!(entry_1.msgid, "msgid 1");
assert_eq!(entry_1.msgstr.as_ref().unwrap(), "msgstr 1");
assert_eq!(entry_1.obsolete, false);
assert_eq!(entry_1.flags.len(), 2);
assert_eq!(entry_1.flags, vec!["python-format", "fuzzy"]);
assert_eq!(entry_1.fuzzy(), true);
assert_eq!(entry_2.msgid, "msgid 2");
assert_eq!(entry_2.msgstr.as_ref().unwrap(), "msgstr 2");
assert_eq!(entry_2.obsolete, false);
assert_eq!(entry_2.flags.len(), 1);
assert_eq!(entry_2.flags[0], "fuzzy");
assert_eq!(entry_2.fuzzy(), true);
assert_eq!(entry_3.msgid, "msgid 3");
assert_eq!(entry_3.msgstr.as_ref().unwrap(), "msgstr 3");
assert_eq!(entry_3.obsolete, false);
assert_eq!(entry_3.flags.len(), 1);
assert_eq!(entry_3.flags[0], "python-format");
assert_eq!(entry_3.fuzzy(), false);
assert_eq!(entry_4.msgid, "msgid 4");
assert_eq!(entry_4.msgstr.as_ref().unwrap(), "msgstr 4");
assert_eq!(entry_4.obsolete, false);
assert_eq!(entry_4.flags.len(), 7);
assert_eq!(
entry_4.flags,
vec!["1", "2", "3", "4", "5", "6", "7"]
);
assert_eq!(entry_4.fuzzy(), false);
assert_eq!(entry_5.msgid, "msgid 5");
assert_eq!(entry_5.msgstr.as_ref().unwrap(), "msgstr 5");
assert_eq!(entry_5.obsolete, false);
assert_eq!(entry_5.flags.len(), 7);
assert_eq!(
entry_5.flags,
vec!["a", "b", "c", "d", "e", "f", "g"]
);
assert_eq!(entry_5.fuzzy(), false);
assert_eq!(entry_6.msgid, "msgid 6");
assert_eq!(entry_6.msgstr.as_ref().unwrap(), "msgstr 6");
assert_eq!(entry_6.obsolete, false);
assert_eq!(entry_6.flags.len(), 0);
assert_eq!(entry_6.fuzzy(), false);
Ok(())
}
#[test]
fn parse_msgid_plural() -> Result<(), SyntaxError> {
let path = "tests-data/msgid-plural.po";
let mut parser = POFileParser::new(path.into());
parser.parse()?;
assert_eq!(parser.file.entries.len(), 2);
assert_eq!(
parser.file.entries[0].msgid_plural.is_some(),
true
);
assert_eq!(
parser.file.entries[1].msgid_plural.is_some(),
true
);
let entry_1 = &parser.file.entries[0];
let entry_2 = &parser.file.entries[1];
assert_eq!(
entry_1.msgid_plural.as_ref().unwrap(),
concat!(
"A Ensure this value has at least %(limit_value)d",
" characters (it has %(show_value)d).",
)
);
assert_eq!(
entry_2.msgid_plural.as_ref().unwrap(),
concat!(
"B Ensure this value has at least %(limit_value)d",
" characters (it has %(show_value)d).",
)
);
assert_eq!(entry_1.msgstr_plural.len(), 2);
assert_eq!(
entry_1.msgstr_plural[0],
concat!(
"A AsegĂşrese de que este valor tenga al menos",
" %(limit_value)d caracter (tiene %(show_value)d).",
)
);
assert_eq!(
entry_1.msgstr_plural[1],
concat!(
"A AsegĂşrese de que este valor tenga al menos",
" %(limit_value)d carácter(es) (tiene%(show_value)d).",
)
);
Ok(())
}
#[test]
fn parse_msgctxt() -> Result<(), SyntaxError> {
let path = "tests-data/msgctxt.po";
let mut parser = POFileParser::new(path.into());
parser.parse()?;
assert_eq!(parser.file.entries.len(), 3);
let entry_1 = &parser.file.entries[0];
let entry_2 = &parser.file.entries[1];
let entry_3 = &parser.file.entries[2];
assert_eq!(entry_1.msgid, "Jan.");
assert_eq!(entry_1.msgstr.as_ref().unwrap(), "Ene.");
assert_eq!(
entry_1.msgctxt.as_ref().unwrap(),
"abbrev. month"
);
assert_eq!(entry_1.fuzzy(), false);
assert_eq!(entry_2.msgid, "J.");
assert_eq!(entry_2.msgstr.as_ref().unwrap(), "E.");
assert_eq!(
entry_2.msgctxt.as_ref().unwrap(),
"abbrev. month"
);
assert_eq!(entry_2.fuzzy(), true);
assert_eq!(entry_3.msgid, "To date");
assert_eq!(
entry_3.msgstr.as_ref().unwrap(),
"Hasta la fecha"
);
assert_eq!(entry_3.msgctxt.as_ref().unwrap(), "to date");
assert_eq!(entry_3.fuzzy(), false);
Ok(())
}
#[test]
fn parse_previous_msgid_msgctx() -> Result<(), SyntaxError> {
let path = "tests-data/previous-msgid-msgctxt.po";
let mut parser = POFileParser::new(path.into());
parser.parse()?;
assert_eq!(parser.file.entries.len(), 1);
assert_eq!(parser.file.entries[0].msgid, "Some msgid");
assert_eq!(
parser.file.entries[0].previous_msgid.as_ref().unwrap(),
"previous untranslated entry"
);
assert_eq!(
parser.file.entries[0].previous_msgctxt.as_ref().unwrap(),
"@previous_context"
);
Ok(())
}
#[test]
fn parse_empty_occurrences_line() -> Result<(), SyntaxError> {
let path = "tests-data/empty-occurrences-line.po";
let mut parser = POFileParser::new(path.into());
parser.parse()?;
assert_eq!(parser.file.entries.len(), 1);
let expected_occurrences = Vec::from([
("db/models/manipulators.py", "310"),
("contrib/admin/views/main.py", "342"),
("contrib/admin/views/main.py", "344"),
("contrib/admin/views/main.py", "346"),
("core/validators.py", "275"),
]);
assert_eq!(parser.file.entries[0].occurrences.len(), 5);
for (i, (occ_fline, occ_line)) in
expected_occurrences.iter().enumerate()
{
assert_eq!(
parser.file.entries[0].occurrences[i],
(occ_fline.to_string(), occ_line.to_string())
);
}
Ok(())
}
#[test]
fn parse_occurrence_no_linenum() -> Result<(), SyntaxError> {
let path = "tests-data/occurrence-no-linenum.po";
let mut parser = POFileParser::new(path.into());
parser.parse()?;
assert_eq!(parser.file.entries.len(), 1);
assert_eq!(parser.file.entries[0].msgid, "Hello");
assert_eq!(
parser.file.entries[0].msgstr.as_ref().unwrap(),
"Bonjour"
);
assert_eq!(parser.file.entries[0].occurrences.len(), 2);
assert_eq!(
parser.file.entries[0].occurrences[0],
("path/to/file/noocc.rs".to_string(), "".to_string())
);
assert_eq!(
parser.file.entries[0].occurrences[1],
("path/to/file/occ.rs".to_string(), "45".to_string())
);
Ok(())
}
#[test]
fn parse_weird_occurrences() -> Result<(), SyntaxError> {
let path = "tests-data/weird-occurrences.po";
let mut parser = POFileParser::new(path.into());
parser.parse()?;
assert_eq!(parser.file.entries.len(), 3);
let entry_1 = &parser.file.entries[0];
assert_eq!(entry_1.msgid, "Windows path");
assert_eq!(
entry_1.occurrences,
vec![("C:\\foo\\bar.py:12".to_string(), "".to_string())]
);
let entry_2 = &parser.file.entries[1];
assert_eq!(entry_2.msgid, "Override the default prgname");
assert_eq!(
entry_2.occurrences,
vec![("main.c".to_string(), "117".to_string())]
);
let entry_3 = &parser.file.entries[2];
assert_eq!(entry_3.msgid, "choose new graphic");
assert_eq!(entry_3.occurrences, vec![
("Balloon-Fills,BitmapFillStyle>>addFillStyleMenuItems:hand:from:".to_string(), "".to_string())
]);
Ok(())
}
#[test]
fn parse_complete() -> Result<(), SyntaxError> {
let path = "tests-data/django-complete.po";
let content = fs::read_to_string(path).unwrap();
let mut parser = POFileParser::new(content.as_str().into());
parser.parse()?;
assert_eq!(parser.file.entries.len(), 341);
assert_eq!(parser.file.header.unwrap().lines().count(), 30);
let n_msgid_plurals = parser
.file
.entries
.iter()
.filter(|e| e.msgid_plural.is_some())
.count();
assert_eq!(
content.matches("msgid \"").count() - 1,
parser.file.entries.len(),
);
assert_eq!(
content.matches("msgstr").count() - 1 - n_msgid_plurals,
parser.file.entries.len(),
);
assert_eq!(
content.matches("msgctxt \"").count(),
parser
.file
.entries
.iter()
.filter(|e| e.msgctxt.is_some())
.count(),
);
assert_eq!(
content.matches("msgid_plural \"").count(),
n_msgid_plurals
);
assert_eq!(
content.matches("msgstr[0]").count(),
n_msgid_plurals
);
assert_eq!(
content.matches("msgstr[1]").count(),
n_msgid_plurals
);
assert_eq!(
content.matches("#, ").count(),
parser
.file
.entries
.iter()
.filter(|e| e.flags.len() > 0)
.count(),
);
assert_eq!(
content.matches("python-format").count(),
parser
.file
.entries
.iter()
.filter(|e| e.flags.contains(&"python-format".into()))
.count(),
);
Ok(())
}
#[test]
fn parse_fuzzy_header() -> Result<(), SyntaxError> {
let path = "tests-data/fuzzy-header.po";
let mut parser = POFileParser::new(path.into());
parser.parse()?;
let metadata_as_entry = parser.file.metadata_as_entry();
assert_eq!(parser.file.entries.len(), 0);
assert_eq!(parser.file.header.unwrap().lines().count(), 2);
assert_eq!(metadata_as_entry.fuzzy(), true);
Ok(())
}
#[test]
fn parse_indented() -> Result<(), SyntaxError> {
let path = "tests-data/indented.po";
let mut parser = POFileParser::new(path.into());
parser.parse()?;
assert_eq!(parser.file.entries.len(), 2);
assert_eq!(
parser.file.entries[0].tcomment.as_ref().unwrap(),
concat!(
"Added for previous msgid/msgid_plural/msgctxt testing",
"\nTokens are separated by some tabs and a single space.",
),
);
Ok(())
}
#[test]
fn parse_previous_continuation_line() -> Result<(), SyntaxError> {
let path = "tests-data/previous-msgid-continuation.po";
let mut parser = POFileParser::new(path.into());
parser.parse()?;
assert_eq!(parser.file.entries.len(), 2);
assert_eq!(
parser.file.entries[1].previous_msgid.as_deref(),
Some("Bar baz qux"),
);
Ok(())
}
#[test]
fn parse_repeated_metadata() -> Result<(), SyntaxError> {
let path = "tests-data/repeated-metadata-keys.po";
let mut parser = POFileParser::new(path.into());
parser.parse()?;
assert!(parser
.file
.metadata
.contains_key("Content-Transfer-Encoding"));
assert_eq!(
parser
.file
.metadata
.get("Content-Transfer-Encoding")
.unwrap(),
r"8bit4bit",
);
assert!(parser.file.metadata.contains_key("MIME-Version"));
assert_eq!(
parser.file.metadata.get("MIME-Version").unwrap(),
"1.02.2",
);
Ok(())
}
#[test]
fn parse_unescaped_double_quote() {
let path = "tests-data/unescaped-double-quote-msgid.po";
let mut parser = POFileParser::new(path.into());
let result = parser.parse();
assert_eq!(
result,
Err(SyntaxError::UnescapedDoubleQuoteFound {
maybe_filename: MaybeFilename::new(path, true,),
line: 5,
index: 11,
})
);
let path =
"tests-data/unescaped-double-quote-continuation.po";
let mut parser = POFileParser::new(path.into());
let result = parser.parse();
assert_eq!(
result,
Err(SyntaxError::UnescapedDoubleQuoteFound {
maybe_filename: MaybeFilename::new(path, true,),
line: 6,
index: 27,
})
);
}
#[test]
fn parse_obsolete_previous_msgid() -> Result<(), SyntaxError> {
let path = "tests-data/obsolete-previous-msgid.po";
let mut parser = POFileParser::new(path.into());
parser.parse()?;
assert_eq!(parser.file.entries.len(), 2);
let obs_entry = &parser.file.entries[1];
assert_eq!(obs_entry.obsolete, true);
assert!(obs_entry.previous_msgid.is_none());
assert!(obs_entry.fuzzy());
Ok(())
}
#[test]
fn error_when_empty_previous_message_line() {
let content = concat!(
"#\n",
"msgid \"\"\n",
"msgstr \"\"\n",
"\n",
"#|\n",
"msgid \"foo\"\n",
"msgstr \"bar\"\n",
);
let mut parser = POFileParser::new(content.into());
let result = parser.parse();
assert_eq!(
result,
Err(SyntaxError::Custom {
maybe_filename: MaybeFilename::new(content, false,),
line: 5,
index: 2,
message: "empty previous message found".to_string(),
})
);
}
#[test]
fn error_when_invalid_token_found() {
let content = concat!(
"#\n",
"msgid \"\"\n",
"msgstr \"\"\n",
"\n",
"#|msgid \"foo\"\n",
"msgstr \"bar\"\n",
);
let mut parser = POFileParser::new(content.into());
let result = parser.parse();
assert_eq!(
result,
Err(SyntaxError::Generic {
maybe_filename: MaybeFilename::new(content, false,),
line: 5,
index: 0,
})
);
}
#[test]
fn error_when_conflict_marker_found() {
let content = concat!(
"<<<<<<< HEAD\n",
"msgid \"hello\"\n",
"msgstr \"ok\"\n",
"=======\n",
"msgid \"hello\"\n",
"msgstr \"maybe\"\n",
">>>>>>> branch\n",
);
let mut parser = POFileParser::new(content.into());
let result = parser.parse();
assert_eq!(
result,
Err(SyntaxError::Generic {
maybe_filename: MaybeFilename::new(content, false,),
line: 1,
index: 0,
})
);
}
#[test]
fn error_when_conflict_marker_found_in_entry() {
let content = concat!(
"msgid \"hello\"\n",
"<<<<<<< HEAD\n",
"msgstr \"ok\"\n",
"=======\n",
"msgstr \"maybe\"\n",
">>>>>>> branch\n",
);
let mut parser = POFileParser::new(content.into());
let result = parser.parse();
assert_eq!(
result,
Err(SyntaxError::Generic {
maybe_filename: MaybeFilename::new(content, false,),
line: 2,
index: 0,
})
);
}
#[test]
fn error_when_non_digit_msgstr_plural_index() {
let content = concat!(
"#\n",
"msgid \"\"\n",
"msgstr \"\"\n",
"\n",
"msgstr[foo] \"bar\"\n",
);
let mut parser = POFileParser::new(content.into());
let result = parser.parse();
assert_eq!(
result,
Err(SyntaxError::Custom {
maybe_filename: MaybeFilename::new(content, false,),
line: 5,
index: 7,
message: "Invalid msgstr plural index. Expected digit, found 'foo'.".to_string(),
})
);
}
#[test]
fn error_when_previous_msgid_invalid_continuation_line() {
let path =
"tests-data/invalid-previous-msgid-continuation.po";
let mut parser = POFileParser::new(path.into());
let result = parser.parse();
assert_eq!(
result,
Err(SyntaxError::Custom {
maybe_filename: MaybeFilename::new(path, true),
line: 4,
index: 0,
message: "invalid continuation line".to_string(),
})
);
}
#[test]
fn error_when_missing_msgstr_before_next_entry() {
let content = concat!(
"msgctxt \"context:\"\n",
"msgid \"hello\"\n",
"\n",
"msgctxt \"next:\"\n",
"msgid \"foo\"\n",
"msgstr \"bar\"\n",
);
let mut parser = POFileParser::new(content.into());
let result = parser.parse();
assert_eq!(
result,
Err(SyntaxError::Custom {
maybe_filename: MaybeFilename::new(content, false,),
line: 4,
index: 0,
message: "missing 'msgstr' section".to_string(),
})
);
}
#[test]
fn error_when_missing_msgstr_before_next_entry_with_comment() {
let content = concat!(
"msgid \"hello\"\n",
"# comment\n",
"msgid \"next\"\n",
"msgstr \"ok\"\n",
);
let mut parser = POFileParser::new(content.into());
let result = parser.parse();
assert_eq!(
result,
Err(SyntaxError::Custom {
maybe_filename: MaybeFilename::new(content, false,),
line: 3,
index: 0,
message: "missing 'msgstr' section".to_string(),
})
);
}
#[test]
fn error_when_missing_msgstr_before_next_entry_with_generated_comment(
) {
let content = concat!(
"msgid \"hello\"\n",
"#. comment\n",
"msgid \"next\"\n",
"msgstr \"ok\"\n",
);
let mut parser = POFileParser::new(content.into());
let result = parser.parse();
assert_eq!(
result,
Err(SyntaxError::Custom {
maybe_filename: MaybeFilename::new(content, false,),
line: 3,
index: 0,
message: "missing 'msgstr' section".to_string(),
})
);
}
#[test]
fn error_when_missing_msgstr_before_next_entry_with_occurrence_comment(
) {
let content = concat!(
"msgid \"hello\"\n",
"#: path.rs:10\n",
"msgid \"next\"\n",
"msgstr \"ok\"\n",
);
let mut parser = POFileParser::new(content.into());
let result = parser.parse();
assert_eq!(
result,
Err(SyntaxError::Custom {
maybe_filename: MaybeFilename::new(content, false,),
line: 3,
index: 0,
message: "missing 'msgstr' section".to_string(),
})
);
}
#[test]
fn error_when_missing_msgstr_before_next_entry_with_flags_comment(
) {
let content = concat!(
"msgid \"hello\"\n",
"#, fuzzy\n",
"msgid \"next\"\n",
"msgstr \"ok\"\n",
);
let mut parser = POFileParser::new(content.into());
let result = parser.parse();
assert_eq!(
result,
Err(SyntaxError::Custom {
maybe_filename: MaybeFilename::new(content, false,),
line: 3,
index: 0,
message: "missing 'msgstr' section".to_string(),
})
);
}
#[test]
fn error_when_missing_plural_msgstr_before_next_entry() {
let content = concat!(
"msgid \"hello\"\n",
"msgid_plural \"hellos\"\n",
"msgctxt \"next:\"\n",
"msgid \"foo\"\n",
"msgstr \"bar\"\n",
);
let mut parser = POFileParser::new(content.into());
let result = parser.parse();
assert_eq!(
result,
Err(SyntaxError::Custom {
maybe_filename: MaybeFilename::new(content, false,),
line: 3,
index: 0,
message: "missing plural 'msgstr[n]' section"
.to_string(),
})
);
}
#[test]
fn error_when_missing_plural_msgstr_before_next_entry_with_comment(
) {
let content = concat!(
"msgid \"hello\"\n",
"msgid_plural \"hellos\"\n",
"# comment\n",
"msgid \"next\"\n",
"msgstr \"ok\"\n",
);
let mut parser = POFileParser::new(content.into());
let result = parser.parse();
assert_eq!(
result,
Err(SyntaxError::Custom {
maybe_filename: MaybeFilename::new(content, false,),
line: 4,
index: 0,
message: "missing plural 'msgstr[n]' section"
.to_string(),
})
);
}
#[test]
fn error_when_dangling_previous_continuation_line() {
let content = concat!(
"#| \"dangling\"\n",
"msgid \"foo\"\n",
"msgstr \"bar\"\n",
);
let mut parser = POFileParser::new(content.into());
let result = parser.parse();
assert_eq!(
result,
Err(SyntaxError::Custom {
maybe_filename: MaybeFilename::new(content, false,),
line: 1,
index: 0,
message: "invalid previous continuation line"
.to_string(),
})
);
}
#[test]
fn error_when_unclosed_string_delimiter() {
let path = "tests-data/unclosed-string-delimiter.po";
let mut parser = POFileParser::new(path.into());
let result = parser.parse();
assert_eq!(
result,
Err(SyntaxError::Custom {
maybe_filename: MaybeFilename::new(path, true),
line: 5,
index: 12,
message: "unterminated string '\"Foo bar bazá'"
.to_string(),
})
)
}
}