summaryrefslogtreecommitdiff
path: root/server/common
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2025-06-07 00:38:22 +0200
committerJoel Klinghed <the_jk@spawned.biz>2025-06-07 00:39:28 +0200
commita60c0f58470f78545544d01d525eae481511abec (patch)
treef06d8ace50fcf7f773db6e03d142081c3a9ac62b /server/common
parentad893a4fb44244132d710d7f94fc99a7d83f1b87 (diff)
grit: Add basic parsing of xlf
This doesn't read the full xliff format, it only reads the translation target string and the id for the translation unit.
Diffstat (limited to 'server/common')
-rw-r--r--server/common/src/grit.rs377
-rw-r--r--server/common/src/testdata/grit/translations/base_en_gb.xlf48
-rw-r--r--server/common/src/testdata/grit/translations/base_my-Zawgyi.xlf29
-rw-r--r--server/common/src/testdata/grit/translations/base_my.xlf29
-rw-r--r--server/common/src/testdata/grit/translations/base_sv.xlf29
-rw-r--r--server/common/src/tests.rs170
6 files changed, 681 insertions, 1 deletions
diff --git a/server/common/src/grit.rs b/server/common/src/grit.rs
index a510724..c0351c4 100644
--- a/server/common/src/grit.rs
+++ b/server/common/src/grit.rs
@@ -1,6 +1,7 @@
#![allow(dead_code)]
use anyhow::Error;
+use std::collections::VecDeque;
use std::fs;
use std::io::{BufReader, Read};
use std::path::Path;
@@ -123,6 +124,20 @@ pub struct GritPart {
pub messages: Vec<IfMessage>,
}
+#[derive(Debug, PartialEq)]
+pub struct TranslationFile {
+ pub target_language: String,
+
+ pub units: Vec<TranslationUnit>,
+}
+
+#[derive(Debug, PartialEq)]
+pub struct TranslationUnit {
+ pub id: i64,
+
+ pub target: Vec<TextPlaceholder>,
+}
+
fn get_opt_attribute<'a>(attributes: &'a Vec<OwnedAttribute>, name: &str) -> Option<&'a str> {
for attribute in attributes {
if attribute.name.local_name == name {
@@ -1189,3 +1204,365 @@ pub fn get_message_id(message: &Message) -> i64 {
// Avoid returning negative ids
message_id & 0x7fffffffffffffff
}
+
+fn parse_translation_unit_target_element<R: Read>(
+ _attributes: &Vec<OwnedAttribute>,
+ reader: &mut EventReader<R>,
+) -> anyhow::Result<Vec<TextPlaceholder>> {
+ let mut content = Vec::<TextPlaceholder>::new();
+ let mut first = true;
+
+ loop {
+ let event = reader.next()?;
+ match event {
+ XmlEvent::StartElement {
+ name,
+ attributes,
+ namespace: _,
+ } => match name.local_name.as_str() {
+ "ph" => {
+ first = false;
+ content.push(parse_translation_placeholder_element(&attributes, reader)?);
+ }
+ _ => {
+ return Err(Error::msg(format!(
+ "Unexpected {0} in file",
+ name.local_name
+ )));
+ }
+ },
+ XmlEvent::EndElement { name } => {
+ assert!(name.local_name == "target");
+ break;
+ }
+ XmlEvent::Characters(data) => content.push(TextPlaceholder::Text(if first {
+ first = false;
+ data.trim_start().to_string()
+ } else {
+ data
+ })),
+
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::EndDocument => panic!("Unexpected EOD"),
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+
+ if !first {
+ match content.last_mut().unwrap() {
+ TextPlaceholder::Text(data) => {
+ data.truncate(data.trim_end().len());
+ if data.is_empty() {
+ content.pop();
+ }
+ }
+ TextPlaceholder::Placeholder {
+ name: _,
+ content: _,
+ example: _,
+ } => {}
+ }
+ }
+
+ Ok(content)
+}
+
+fn parse_translation_placeholder_element<R: Read>(
+ attributes: &Vec<OwnedAttribute>,
+ reader: &mut EventReader<R>,
+) -> anyhow::Result<TextPlaceholder> {
+ let id = get_attribute(attributes, "id")?;
+
+ loop {
+ let event = reader.next()?;
+ match event {
+ XmlEvent::StartElement {
+ name,
+ attributes: _,
+ namespace: _,
+ } => {
+ return Err(Error::msg(format!("Unexpected {0} in ph", name.local_name)));
+ }
+ XmlEvent::EndElement { name } => {
+ assert!(name.local_name == "ph");
+ break;
+ }
+ XmlEvent::Characters(_) => (),
+
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::EndDocument => panic!("Unexpected EOD"),
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+
+ Ok(TextPlaceholder::Placeholder {
+ name: id.to_string(),
+ content: String::new(),
+ example: None,
+ })
+}
+
+fn parse_translation_unit_element<R: Read>(
+ attributes: &Vec<OwnedAttribute>,
+ reader: &mut EventReader<R>,
+) -> anyhow::Result<TranslationUnit> {
+ let id = get_attribute(attributes, "id")?.parse::<i64>()?;
+
+ let mut target: Option<Vec<TextPlaceholder>> = None;
+
+ loop {
+ let event = reader.next()?;
+ match event {
+ XmlEvent::StartElement {
+ name,
+ attributes,
+ namespace: _,
+ } => match name.local_name.as_str() {
+ "target" => {
+ if target.is_some() {
+ return Err(Error::msg("Two target in trans-unit"));
+ }
+ target = Some(parse_translation_unit_target_element(&attributes, reader)?);
+ }
+ _ => {
+ reader.skip()?;
+ }
+ },
+ XmlEvent::EndElement { name } => {
+ assert!(name.local_name == "trans-unit");
+ break;
+ }
+ XmlEvent::Characters(_) => (),
+
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::EndDocument => panic!("Unexpected EOD"),
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+
+ Ok(TranslationUnit {
+ id,
+ target: target.expect("No target in trans-unit"),
+ })
+}
+
+fn parse_translation_body_element<R: Read>(
+ _attributes: &Vec<OwnedAttribute>,
+ reader: &mut EventReader<R>,
+) -> anyhow::Result<Vec<TranslationUnit>> {
+ let mut units = Vec::<TranslationUnit>::new();
+
+ loop {
+ let event = reader.next()?;
+ match event {
+ XmlEvent::StartElement {
+ name,
+ attributes,
+ namespace: _,
+ } => match name.local_name.as_str() {
+ "trans-unit" => {
+ units.push(parse_translation_unit_element(&attributes, reader)?);
+ }
+ _ => {
+ reader.skip()?;
+ }
+ },
+ XmlEvent::EndElement { name } => {
+ assert!(name.local_name == "body");
+ break;
+ }
+ XmlEvent::Characters(_) => (),
+
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::EndDocument => panic!("Unexpected EOD"),
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+
+ Ok(units)
+}
+
+fn parse_translation_file_element<R: Read>(
+ attributes: &Vec<OwnedAttribute>,
+ reader: &mut EventReader<R>,
+) -> anyhow::Result<TranslationFile> {
+ let target_language = get_attribute(attributes, "target-language")?;
+
+ let mut units: Option<Vec<TranslationUnit>> = None;
+
+ loop {
+ let event = reader.next()?;
+ match event {
+ XmlEvent::StartElement {
+ name,
+ attributes,
+ namespace: _,
+ } => match name.local_name.as_str() {
+ "body" => {
+ if units.is_some() {
+ return Err(Error::msg("More than one body in file"));
+ }
+ units = Some(parse_translation_body_element(&attributes, reader)?);
+ }
+ _ => {
+ reader.skip()?;
+ }
+ },
+ XmlEvent::EndElement { name } => {
+ assert!(name.local_name == "file");
+ break;
+ }
+ XmlEvent::Characters(_) => (),
+
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::EndDocument => panic!("Unexpected EOD"),
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+
+ Ok(TranslationFile {
+ target_language: target_language.to_string(),
+ units: units.expect("body element in file"),
+ })
+}
+
+fn parse_xliff_element<R: Read>(
+ _attributes: &Vec<OwnedAttribute>,
+ reader: &mut EventReader<R>,
+) -> anyhow::Result<TranslationFile> {
+ let mut file = VecDeque::<TranslationFile>::new();
+
+ loop {
+ let event = reader.next()?;
+ match event {
+ XmlEvent::StartElement {
+ name,
+ attributes,
+ namespace: _,
+ } => match name.local_name.as_str() {
+ "file" => {
+ file.push_back(parse_translation_file_element(&attributes, reader)?);
+ }
+ _ => (),
+ },
+ XmlEvent::EndElement { name } => {
+ assert!(name.local_name == "xliff");
+ break;
+ }
+ XmlEvent::Characters(_) => (),
+
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::EndDocument => panic!("Unexpected EOD"),
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+
+ if file.is_empty() {
+ Err(Error::msg("No file in xliff"))
+ } else if file.len() == 1 {
+ Ok(file.pop_front().unwrap())
+ } else {
+ let mut ret = file.pop_front().unwrap();
+ while !file.is_empty() {
+ let other = file.pop_front().unwrap();
+ if other.target_language == ret.target_language {
+ let end = ret.units.len();
+ ret.units.splice(end..end, other.units);
+ } else {
+ return Err(Error::msg(
+ "Multiple translations in the same file, not supported yet",
+ ));
+ }
+ }
+
+ Ok(ret)
+ }
+}
+
+pub async fn parse_xlf(path: impl AsRef<Path>) -> anyhow::Result<TranslationFile> {
+ let path = path.as_ref().to_path_buf();
+ spawn_blocking(move || {
+ let file = fs::File::open(path)?;
+ let reader = BufReader::new(file);
+ let mut ereader = ParserConfig::new()
+ .ignore_comments(true)
+ .whitespace_to_characters(true)
+ .cdata_to_characters(true)
+ .create_reader(reader);
+ let mut ret: Option<TranslationFile> = None;
+ loop {
+ let event = ereader.next()?;
+ match event {
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::StartElement {
+ name,
+ attributes,
+ namespace: _,
+ } => {
+ if name.local_name == "xliff" {
+ ret = Some(parse_xliff_element(&attributes, &mut ereader)?);
+ } else {
+ return Err(Error::msg("Document root != xliff"));
+ }
+ }
+ XmlEvent::EndDocument => break,
+ XmlEvent::EndElement { name: _ } => panic!("Unexpected EoE"),
+ XmlEvent::Characters(_) => (),
+
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+ Ok(ret.unwrap())
+ })
+ .await
+ .unwrap()
+}
diff --git a/server/common/src/testdata/grit/translations/base_en_gb.xlf b/server/common/src/testdata/grit/translations/base_en_gb.xlf
new file mode 100644
index 0000000..d77bb3a
--- /dev/null
+++ b/server/common/src/testdata/grit/translations/base_en_gb.xlf
@@ -0,0 +1,48 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+<file datatype="xml" source-language="en-US" original="base.grd" target-language="en-gb">
+<body>
+<trans-unit id="8820817407110198400">
+ <source>Bookmarks</source>
+ <target>Bookmarks</target>
+ <note>IDS_BOOKMARKS_FRAGMENT_TITLE
+ Title which is shown on the main bookmarks view.
+ </note>
+</trans-unit>
+<trans-unit id="8443102241046796905">
+ <source>Welcome to <ph id="STRING"/></source>
+ <target>Welcome to <ph id="STRING"/></target>
+ <note>IDS_GENERIC_WELCOME
+ Generic welcome string.
+ Example STRING: Opera
+ </note>
+</trans-unit>
+<trans-unit id="2466140279568640908">
+ <source>By using this application you are agreeing to Opera's <ph id="TOS_BEGIN"/>Terms of Service<ph id="TOS_END"/>. Also, you can learn how Opera handles and protects your data in our <ph id="PRIVACY_BEGIN"/>Privacy Statement<ph id="PRIVACY_END"/>.</source>
+ <target>By using this application you are agreeing to Opera's <ph id="TOS_BEGIN"/>Terms of Service<ph id="TOS_END"/>. Also, you can learn how Opera handles and protects your data in our <ph id="PRIVACY_BEGIN"/>Privacy Statement<ph id="PRIVACY_END"/>.</target>
+ <note>IDS_START_TERMS
+ First startup information about the license and privacy terms.
+ </note>
+</trans-unit>
+<trans-unit id="7770247413830876286" xml:space="preserve">
+ <source>{BOOKMARKS, plural,
+ one {<ph id="COUNT"/> folder deleted}
+ few {<ph id="COUNT"/> folders deleted}
+ many {<ph id="COUNT"/> folders deleted}
+ other {<ph id="COUNT"/> folders deleted}}</source>
+ <target>{BOOKMARKS, plural,
+ one {<ph id="COUNT"/> folder deleted}
+ few {<ph id="COUNT"/> folders deleted}
+ many {<ph id="COUNT"/> folders deleted}
+ other {<ph id="COUNT"/> folders deleted}}</target>
+ <note>IDS_BOOKMARKS_FOLDERS_DELETED
+ Message which is shown when one or more folders have been deleted from the bookmark list.
+ Example-one COUNT: 1
+ Example-few COUNT: 15
+ Example-many COUNT: 100
+ Example-other COUNT: 42
+ </note>
+</trans-unit>
+</body>
+</file>
+</xliff>
diff --git a/server/common/src/testdata/grit/translations/base_my-Zawgyi.xlf b/server/common/src/testdata/grit/translations/base_my-Zawgyi.xlf
new file mode 100644
index 0000000..33c2a31
--- /dev/null
+++ b/server/common/src/testdata/grit/translations/base_my-Zawgyi.xlf
@@ -0,0 +1,29 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+<file datatype="xml" source-language="en-US" original="base.grd" target-language="my-zg">
+<body>
+<trans-unit id="8820817407110198400">
+ <source>Bookmarks</source>
+ <target>ဝက္ဘ္လိပ္စာ မွတ္ထားမွုမ်ား</target>
+ <note>IDS_BOOKMARKS_FRAGMENT_TITLE
+ Title which is shown on the main bookmarks view.
+ </note>
+</trans-unit>
+<trans-unit id="8443102241046796905">
+ <source>Welcome to <ph id="STRING"/></source>
+ <target><ph id="STRING"/> မွ ႀကိဳဆိုပါသည္</target>
+ <note>IDS_GENERIC_WELCOME
+ Generic welcome string.
+ Example STRING: Opera
+ </note>
+</trans-unit>
+<trans-unit id="2466140279568640908">
+ <source>By using this application you are agreeing to Opera's <ph id="TOS_BEGIN"/>Terms of Service<ph id="TOS_END"/>. Also, you can learn how Opera handles and protects your data in our <ph id="PRIVACY_BEGIN"/>Privacy Statement<ph id="PRIVACY_END"/>.</source>
+ <target>ဤအပလီေကးရွင္းကို အသုံးျပဳျခင္းျဖင့္ သင္သည္ Opera ၏ <ph id="TOS_BEGIN"/>ဝန္ေဆာင္မွုစည္းမ်ဥ္းမ်ား<ph id="TOS_END"/> ကို သေဘာတူရာ ေရာက္ပါသည္။ ထို႔အျပင္ ကၽြန္ုပ္တို႔၏<ph id="PRIVACY_BEGIN"/>ကိုယ္ေရးလုံျခဳံမွု ထုတ္ျပန္ခ်က္<ph id="PRIVACY_END"/> ထဲတြင္ သင့္ေဒတာမ်ားကို Opera ၏ ကိုင္တြယ္ပုံႏွင့္ ကာကြယ္ပုံတို႔ကိုလည္း ေလ့လာနိုင္သည္။</target>
+ <note>IDS_START_TERMS
+ First startup information about the license and privacy terms.
+ </note>
+</trans-unit>
+</body>
+</file>
+</xliff>
diff --git a/server/common/src/testdata/grit/translations/base_my.xlf b/server/common/src/testdata/grit/translations/base_my.xlf
new file mode 100644
index 0000000..eea3dc8
--- /dev/null
+++ b/server/common/src/testdata/grit/translations/base_my.xlf
@@ -0,0 +1,29 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+<file datatype="xml" source-language="en-US" original="base.grd" target-language="my">
+<body>
+<trans-unit id="8820817407110198400">
+ <source>Bookmarks</source>
+ <target>ဝက်ဘ်လိပ်စာ မှတ်ထားမှုများ</target>
+ <note>IDS_BOOKMARKS_FRAGMENT_TITLE
+ Title which is shown on the main bookmarks view.
+ </note>
+</trans-unit>
+<trans-unit id="8443102241046796905">
+ <source>Welcome to <ph id="STRING"/></source>
+ <target><ph id="STRING"/> မှ ကြိုဆိုပါသည်</target>
+ <note>IDS_GENERIC_WELCOME
+ Generic welcome string.
+ Example STRING: Opera
+ </note>
+</trans-unit>
+<trans-unit id="2466140279568640908">
+ <source>By using this application you are agreeing to Opera's <ph id="TOS_BEGIN"/>Terms of Service<ph id="TOS_END"/>. Also, you can learn how Opera handles and protects your data in our <ph id="PRIVACY_BEGIN"/>Privacy Statement<ph id="PRIVACY_END"/>.</source>
+ <target>ဤအပလီကေးရှင်းကို အသုံးပြုခြင်းဖြင့် သင်သည် Opera ၏ <ph id="TOS_BEGIN"/>ဝန်ဆောင်မှုစည်းမျဉ်းများ<ph id="TOS_END"/> ကို သဘောတူရာ ရောက်ပါသည်။ ထို့အပြင် ကျွန်ုပ်တို့၏<ph id="PRIVACY_BEGIN"/>ကိုယ်ရေးလုံခြုံမှု ထုတ်ပြန်ချက်<ph id="PRIVACY_END"/> ထဲတွင် သင့်ဒေတာများကို Opera ၏ ကိုင်တွယ်ပုံနှင့် ကာကွယ်ပုံတို့ကိုလည်း လေ့လာနိုင်သည်။</target>
+ <note>IDS_START_TERMS
+ First startup information about the license and privacy terms.
+ </note>
+</trans-unit>
+</body>
+</file>
+</xliff>
diff --git a/server/common/src/testdata/grit/translations/base_sv.xlf b/server/common/src/testdata/grit/translations/base_sv.xlf
new file mode 100644
index 0000000..e33a385
--- /dev/null
+++ b/server/common/src/testdata/grit/translations/base_sv.xlf
@@ -0,0 +1,29 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+<file datatype="xml" source-language="en-US" original="base.grd" target-language="sv">
+<body>
+<trans-unit id="8820817407110198400">
+ <source>Bookmarks</source>
+ <target>Bokmärken</target>
+ <note>IDS_BOOKMARKS_FRAGMENT_TITLE
+ Title which is shown on the main bookmarks view.
+ </note>
+</trans-unit>
+<trans-unit id="8443102241046796905">
+ <source>Welcome to <ph id="STRING"/></source>
+ <target>Välkommen till <ph id="STRING"/></target>
+ <note>IDS_GENERIC_WELCOME
+ Generic welcome string.
+ Example STRING: Opera
+ </note>
+</trans-unit>
+<trans-unit id="2466140279568640908">
+ <source>By using this application you are agreeing to Opera's <ph id="TOS_BEGIN"/>Terms of Service<ph id="TOS_END"/>. Also, you can learn how Opera handles and protects your data in our <ph id="PRIVACY_BEGIN"/>Privacy Statement<ph id="PRIVACY_END"/>.</source>
+ <target>I och med din användning av det här programmet samtycker du till Operas <ph id="TOS_BEGIN"/>Licensvillkor<ph id="TOS_END"/>. Du kan också läsa om hur Opera hanterar och skyddar dina data i vårt <ph id="PRIVACY_BEGIN"/>Sekretessmeddelande<ph id="PRIVACY_END"/>.</target>
+ <note>IDS_START_TERMS
+ First startup information about the license and privacy terms.
+ </note>
+</trans-unit>
+</body>
+</file>
+</xliff>
diff --git a/server/common/src/tests.rs b/server/common/src/tests.rs
index 9ce44c8..29ee6d6 100644
--- a/server/common/src/tests.rs
+++ b/server/common/src/tests.rs
@@ -1,4 +1,4 @@
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
use testdir::testdir;
use tokio::fs;
use tokio::sync::OnceCell;
@@ -808,3 +808,171 @@ fn test_grit_get_message_id() {
7770247413830876286,
)
}
+
+#[tokio::test]
+async fn test_grit_parse_xlf() {
+ let basepath = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/testdata/grit/translations");
+ let en_gb = grit::parse_xlf(basepath.join(Path::new("base_en_gb.xlf")))
+ .await
+ .unwrap();
+ assert_eq!(
+ en_gb,
+ grit::TranslationFile {
+ target_language: "en-gb".to_string(),
+ units: vec![
+ grit::TranslationUnit {
+ id: 8820817407110198400,
+ target: vec![grit::TextPlaceholder::Text("Bookmarks".to_string()),],
+ },
+ grit::TranslationUnit {
+ id: 8443102241046796905,
+ target: vec![
+ grit::TextPlaceholder::Text("Welcome to ".to_string()),
+ grit::TextPlaceholder::Placeholder {
+ name: "STRING".to_string(),
+ content: String::new(),
+ example: None,
+ },
+ ],
+ },
+ grit::TranslationUnit {
+ id: 2466140279568640908,
+ target: vec![
+ grit::TextPlaceholder::Text(
+ "By using this application you are agreeing to Opera's ".to_string(),
+ ),
+ grit::TextPlaceholder::Placeholder {
+ name: "TOS_BEGIN".to_string(),
+ content: String::new(),
+ example: None,
+ },
+ grit::TextPlaceholder::Text("Terms of Service".to_string(),),
+ grit::TextPlaceholder::Placeholder {
+ name: "TOS_END".to_string(),
+ content: String::new(),
+ example: None,
+ },
+ grit::TextPlaceholder::Text(
+ ". Also, you can learn how Opera handles and protects your data in our "
+ .to_string(),
+ ),
+ grit::TextPlaceholder::Placeholder {
+ name: "PRIVACY_BEGIN".to_string(),
+ content: String::new(),
+ example: None,
+ },
+ grit::TextPlaceholder::Text("Privacy Statement".to_string(),),
+ grit::TextPlaceholder::Placeholder {
+ name: "PRIVACY_END".to_string(),
+ content: String::new(),
+ example: None,
+ },
+ grit::TextPlaceholder::Text(".".to_string(),),
+ ],
+ },
+ grit::TranslationUnit {
+ id: 7770247413830876286,
+ target: vec![
+ grit::TextPlaceholder::Text(
+ "{BOOKMARKS, plural,\n one {".to_string(),
+ ),
+ grit::TextPlaceholder::Placeholder {
+ name: "COUNT".to_string(),
+ content: String::new(),
+ example: None,
+ },
+ grit::TextPlaceholder::Text(
+ " folder deleted}\n few {".to_string(),
+ ),
+ grit::TextPlaceholder::Placeholder {
+ name: "COUNT".to_string(),
+ content: String::new(),
+ example: None,
+ },
+ grit::TextPlaceholder::Text(
+ " folders deleted}\n many {".to_string(),
+ ),
+ grit::TextPlaceholder::Placeholder {
+ name: "COUNT".to_string(),
+ content: String::new(),
+ example: None,
+ },
+ grit::TextPlaceholder::Text(
+ " folders deleted}\n other {".to_string(),
+ ),
+ grit::TextPlaceholder::Placeholder {
+ name: "COUNT".to_string(),
+ content: String::new(),
+ example: None,
+ },
+ grit::TextPlaceholder::Text(" folders deleted}}".to_string(),),
+ ],
+ },
+ ],
+ },
+ );
+
+ let sv = grit::parse_xlf(basepath.join(Path::new("base_sv.xlf")))
+ .await
+ .unwrap();
+ assert_eq!(
+ sv,
+ grit::TranslationFile {
+ target_language: "sv".to_string(),
+ units: vec![
+ grit::TranslationUnit {
+ id: 8820817407110198400,
+ target: vec![
+ grit::TextPlaceholder::Text("Bokmärken".to_string()),
+ ],
+ },
+ grit::TranslationUnit {
+ id: 8443102241046796905,
+ target: vec![
+ grit::TextPlaceholder::Text("Välkommen till ".to_string()),
+ grit::TextPlaceholder::Placeholder {
+ name: "STRING".to_string(),
+ content: String::new(),
+ example: None,
+ },
+ ],
+ },
+ grit::TranslationUnit {
+ id: 2466140279568640908,
+ target: vec![
+ grit::TextPlaceholder::Text(
+ "I och med din användning av det här programmet samtycker du till Operas ".to_string(),
+ ),
+ grit::TextPlaceholder::Placeholder {
+ name: "TOS_BEGIN".to_string(),
+ content: String::new(),
+ example: None,
+ },
+ grit::TextPlaceholder::Text("Licensvillkor".to_string(),),
+ grit::TextPlaceholder::Placeholder {
+ name: "TOS_END".to_string(),
+ content: String::new(),
+ example: None,
+ },
+ grit::TextPlaceholder::Text(
+ ". Du kan också läsa om hur Opera hanterar och skyddar dina data i vårt "
+ .to_string(),
+ ),
+ grit::TextPlaceholder::Placeholder {
+ name: "PRIVACY_BEGIN".to_string(),
+ content: String::new(),
+ example: None,
+ },
+ grit::TextPlaceholder::Text("Sekretessmeddelande".to_string(),),
+ grit::TextPlaceholder::Placeholder {
+ name: "PRIVACY_END".to_string(),
+ content: String::new(),
+ example: None,
+ },
+ grit::TextPlaceholder::Text(".".to_string(),),
+ ],
+ },
+ ],
+ },
+ )
+}