diff options
Diffstat (limited to 'server/common')
| -rw-r--r-- | server/common/src/grit.rs | 377 | ||||
| -rw-r--r-- | server/common/src/testdata/grit/translations/base_en_gb.xlf | 48 | ||||
| -rw-r--r-- | server/common/src/testdata/grit/translations/base_my-Zawgyi.xlf | 29 | ||||
| -rw-r--r-- | server/common/src/testdata/grit/translations/base_my.xlf | 29 | ||||
| -rw-r--r-- | server/common/src/testdata/grit/translations/base_sv.xlf | 29 | ||||
| -rw-r--r-- | server/common/src/tests.rs | 170 |
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(),), + ], + }, + ], + }, + ) +} |
