diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index fe03472..9b20767 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,7 +1,7 @@ --- name: Bug report about: Create a report to help us improve -title: '' +title: 'Bug: ' labels: 'bug' assignees: '' diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..48e31a6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Create a feature or enhancement request +title: 'Feature request: ' +labels: 'enhancement' +assignees: '' + +--- + +**Enhancement's description:** +_Please enter a short description of what is the enhancement (provide as much detail as can be necessary)._ + +**Expected behavior / output:** +_Please provide some examples of input and expected output. You may include faulty input as an example of error handling as well. ._ + +## Sample translation file +_Only include this section if you can provide a translation file if it is relevant to the feature request_ + +## Sample expected / correct output file +_If possible, include an expected output file in order to make it easier for contributors to match. It is not required._ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 922aace..1b8be47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,18 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +[Third milestone](https://github.com/mrtryhard/qt-ts-tools/milestone/3). This introduces the `merge` command and improved documentation. ## Added +- Merge mechanism to merge two translation files [#24](https://github.com/mrtryhard/qt-ts-tools/issues/24) +- `extra-*` fields support in `TS` and `message` nodes [#4](https://github.com/mrtryhard/qt-ts-tools/issues/4) + ## Changed +- Improved command line documentation [#25](https://github.com/mrtryhard/qt-ts-tools/issues/25), [#27](https://github.com/mrtryhard/qt-ts-tools/issues/27) +- Updated Clap dependencies [#26](https://github.com/mrtryhard/qt-ts-tools/issues/26) + ## Fixed ## [0.2.0] - 2024-01-01 @@ -18,7 +25,7 @@ Completion of the [second milestone](https://github.com/mrtryhard/qt-ts-tools/mi ### Added -- Extraction mechanism to extract only relevant translation types. [#16](https://github.com/mrtryhard/qt-ts-tools/issues/16) +- Extraction mechanism to extract only relevant translation types [#16](https://github.com/mrtryhard/qt-ts-tools/issues/16) ### Changed @@ -33,4 +40,4 @@ Introduction of `qt-ts-tools`. This completes the first [milestone](https://gith ### Added -- Sort mechanism to sort translation files by location and contexts. [#3](https://github.com/mrtryhard/qt-ts-tools/issues/3) +- Sort mechanism to sort translation files by location and contexts [#3](https://github.com/mrtryhard/qt-ts-tools/issues/3) diff --git a/Cargo.lock b/Cargo.lock index b102704..851c996 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,9 +52,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.18" +version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" +checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f" dependencies = [ "clap_builder", "clap_derive", @@ -62,9 +62,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.18" +version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99" dependencies = [ "anstream", "anstyle", @@ -74,9 +74,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.4.7" +version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" dependencies = [ "heck", "proc-macro2", @@ -86,9 +86,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "colorchoice" @@ -96,12 +96,27 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "memchr" version = "2.7.1" @@ -119,9 +134,10 @@ dependencies = [ [[package]] name = "qt-ts-tools" -version = "0.2.0" +version = "0.3.0" dependencies = [ "clap", + "itertools", "quick-xml", "serde", ] @@ -167,9 +183,9 @@ dependencies = [ [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" [[package]] name = "syn" diff --git a/Cargo.toml b/Cargo.toml index 14472a9..69adff4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,15 +5,17 @@ repository = "https://github.com/mrtryhard/qt-ts-tools" keywords = ["qt", "translation"] homepage = "https://github.com/mrtryhard/qt-ts-tools" license = "MIT OR Apache-2.0" -version = "0.2.0" +version = "0.3.0" edition = "2021" +description = "Small command line utility to manipulate Qt's translation files with diverse operations." # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = { version = "4.4.18", features = ["derive"] } +clap = { version = "4.5.0", features = ["derive"] } quick-xml = { version = "0.31.0", features = ["serialize"] } serde = { version = "1.0.196", features = ["derive"] } +itertools = "0.12.1" [profile.release] strip = true diff --git a/src/extract.rs b/src/extract.rs index 2c441ad..2574835 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -2,11 +2,15 @@ use crate::ts; use crate::ts::{TSNode, TranslationType}; use clap::Args; +/// Extracts a translation type messages and contexts from the input translation file. #[derive(Args)] pub struct ExtractArgs { + /// File path to extract translations from. pub input_path: String, + /// Translation type list to extract into a single, valid translation output. #[arg(short('t'), long, value_enum, num_args = 1..)] pub translation_type: Vec, + /// If specified, will produce output in a file at designated location instead of stdout. #[arg(short, long)] pub output_path: Option, } diff --git a/src/main.rs b/src/main.rs index 0783a91..b36c6fa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,10 @@ mod extract; +mod merge; mod sort; mod ts; use crate::extract::{extract_main, ExtractArgs}; +use crate::merge::{merge_main, MergeArgs}; use crate::sort::{sort_main, SortArgs}; use clap::{Parser, Subcommand}; @@ -17,12 +19,14 @@ struct Cli { enum Commands { Sort(SortArgs), Extract(ExtractArgs), + Merge(MergeArgs), } fn get_cli_result(cli: Cli) -> Result<(), String> { match &cli.command { Commands::Sort(args) => sort_main(&args), Commands::Extract(args) => extract_main(&args), + Commands::Merge(args) => merge_main(&args), } } diff --git a/src/merge.rs b/src/merge.rs new file mode 100644 index 0000000..cd3267c --- /dev/null +++ b/src/merge.rs @@ -0,0 +1,172 @@ +use std::hash::{Hash, Hasher}; + +use clap::Args; +use itertools::Itertools; + +use crate::ts; +use crate::ts::{MessageNode, TSNode}; + +/// Merges two translation file contexts and messages into a single output. +#[derive(Args)] +pub struct MergeArgs { + /// File to receive the merge + pub input_left: String, + /// File to include changes from + pub input_right: String, + /// If specified, will produce output in a file at designated location instead of stdout. + #[arg(short, long)] + pub output_path: Option, +} + +/// MessageNode that can be `eq(...)`. +#[derive(Eq, PartialOrd, Clone)] +struct EquatableMessageNode { + pub node: MessageNode, +} + +/// The rule for equality is if the message id or source match. +impl PartialEq for EquatableMessageNode { + fn eq(&self, other: &Self) -> bool { + if let Some(this_id) = &self.node.id { + if let Some(other_id) = &other.node.id { + return this_id == other_id; + } + } + + self.node.source == other.node.source && self.node.locations == other.node.locations + } +} + +impl Hash for EquatableMessageNode { + fn hash(&self, state: &mut H) { + self.node.source.hash(state); + self.node.locations.iter().for_each(|loc| { + loc.line.hash(state); + loc.filename.hash(state); + }); + } +} + +// This wortks by depending on cmp looking up only source and location on messages nodes +// and on context by comparing the names only +pub fn merge_main(args: &MergeArgs) -> Result<(), String> { + let left = load_file(&args.input_left); + let right = load_file(&args.input_right); + + if let Err(e) = left { + return Err(format!( + "Could not process left file '{}'. Error: {}", + &args.input_left, + e.to_string() + )); + } + + if let Err(e) = right { + return Err(format!( + "Could not process right file '{}'. Error: {}", + &args.input_right, + e.to_string() + )); + } + + let result = merge_ts_nodes(left.unwrap(), right.unwrap()); + + ts::write_to_output(&args.output_path, &result) +} + +fn merge_ts_nodes(mut left: TSNode, mut right: TSNode) -> TSNode { + left.messages = merge_messages(&mut left.messages, &mut right.messages); + merge_contexts(&mut left, right); + left +} + +fn merge_contexts(left: &mut TSNode, right: TSNode) { + right.contexts.into_iter().for_each(|mut right_context| { + let left_context_opt = left + .contexts + .iter_mut() + .find(|left_context| left_context.name == right_context.name); + + if let Some(left_context) = left_context_opt { + left_context.comment = right_context.comment; + left_context.encoding = right_context.encoding; + + left_context.messages = + merge_messages(&mut left_context.messages, &mut right_context.messages); + } else { + left.contexts.push(right_context); + } + }); +} + +/// Merges two messages collections +fn merge_messages( + left_messages: &mut Vec, + right_messages: &mut Vec, +) -> Vec { + let mut unique_messages_left: Vec<_> = left_messages + .drain(0..) + .map(|node| EquatableMessageNode { node }) + .collect(); + + let mut unique_messages_right: Vec<_> = right_messages + .drain(0..) + .map(|node| EquatableMessageNode { node }) + .collect(); + + // Update oldcomment, oldsource. + unique_messages_right.iter_mut().for_each(|right_message| { + let left_message = unique_messages_left + .iter() + .find(|&msg| msg == right_message); + + if let Some(left_message) = left_message { + if right_message.node.source != left_message.node.source { + right_message.node.oldsource = left_message.node.source.clone(); + } + + if right_message.node.comment != left_message.node.comment { + right_message.node.oldcomment = left_message.node.comment.clone(); + } + } + }); + + unique_messages_left + .drain(0..) + .filter(|a| !unique_messages_right.contains(&a)) + .merge(unique_messages_right.iter().cloned()) + .map(|node| node.node) + .collect() +} + +fn load_file(path: &String) -> Result { + match quick_xml::Reader::from_file(&path) { + Ok(reader) => { + let nodes: Result = quick_xml::de::from_reader(reader.into_inner()); + match nodes { + Ok(nodes) => Ok(nodes), + Err(err) => Err(err.to_string()), + } + } + Err(err) => Err(err.to_string()), + } +} + +#[cfg(test)] +mod merge_test { + use super::*; + + #[test] + fn test_merge_two_files() { + let left = load_file(&"./test_data/example_merge_left.xml".to_string()) + .expect("Test data could not be loaded for left file."); + let right = load_file(&"./test_data/example_merge_right.xml".to_string()) + .expect("Test data could not be loaded for right file."); + let expected_result = load_file(&"./test_data/example_merge_result.xml".to_string()) + .expect("Test data could not be loaded for right file."); + + let result = merge_ts_nodes(left, right); + + assert_eq!(result, expected_result); + } +} diff --git a/src/sort.rs b/src/sort.rs index befb470..97d4161 100644 --- a/src/sort.rs +++ b/src/sort.rs @@ -2,9 +2,12 @@ use crate::ts; use crate::ts::TSNode; use clap::Args; +/// Sorts the input translation file by context, then by messages. #[derive(Args)] pub struct SortArgs { + /// File path to sort translations from. pub input_path: String, + /// If specified, will produce output in a file at designated location instead of stdout. #[arg(short, long)] pub output_path: Option, } @@ -68,8 +71,8 @@ mod sort_test { sort_ts_node(&mut data_nosort); // Validate context ordering - assert_eq!(data_nosort.contexts[0].name, Some("CodeContext".to_owned())); - assert_eq!(data_nosort.contexts[1].name, Some("UiContext".to_owned())); + assert_eq!(data_nosort.contexts[0].name, "CodeContext".to_owned()); + assert_eq!(data_nosort.contexts[1].name, "UiContext".to_owned()); // Validate message ordering let messages = &data_nosort.contexts[1].messages; diff --git a/src/ts.rs b/src/ts.rs index 1f8de06..c76801e 100644 --- a/src/ts.rs +++ b/src/ts.rs @@ -1,21 +1,26 @@ -use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use std::io::{BufWriter, Write}; +use serde::{Deserialize, Serialize}; + // This file defines the schema matching (or trying to match?) Qt's XSD // Eventually when a proper Rust code generator exists it would be great to use that instead. // For now they can't handle Qt's semi-weird XSD. // https://doc.qt.io/qt-6/linguist-ts-file-format.html -#[derive(Debug, Eq, Deserialize, Serialize, PartialEq)] +/// If no type is set, a message is "finished". +#[derive(Debug, Default, Clone, Eq, Deserialize, Serialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum TranslationType { + #[default] + #[serde(skip)] + Finished, Unfinished, Obsolete, Vanished, } -#[derive(Debug, Eq, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Eq, Clone, Deserialize, Serialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum YesNo { Yes, @@ -33,8 +38,8 @@ pub struct TSNode { language: Option, #[serde(rename = "context", skip_serializing_if = "Vec::is_empty", default)] pub contexts: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - messages: Option>, + #[serde(rename = "message", skip_serializing_if = "Vec::is_empty", default)] + pub messages: Vec, #[serde(skip_serializing_if = "Option::is_none")] dependencies: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -45,18 +50,45 @@ pub struct TSNode { extracomment: Option, #[serde(skip_serializing_if = "Option::is_none")] translatorcomment: Option, + /* + Following section corresponds to `extra-something` in Qt's XSD. From documentation: + > extra elements may appear in TS and message elements. Each element may appear + > only once within each scope. The contents are preserved verbatim; any + > attributes are dropped. + */ + #[serde( + rename = "extra-po-msgid_plural", + skip_serializing_if = "Option::is_none" + )] + pub po_msg_id_plural: Option, + #[serde( + rename = "extra-po-old_msgid_plural", + skip_serializing_if = "Option::is_none" + )] + pub po_old_msg_id_plural: Option, + /// Comma separated list + #[serde(rename = "extra-po-flags", skip_serializing_if = "Option::is_none")] + pub loc_flags: Option, + #[serde( + rename = "extra-loc-layout_id", + skip_serializing_if = "Option::is_none" + )] + pub loc_layout_id: Option, + #[serde(rename = "extra-loc-feature", skip_serializing_if = "Option::is_none")] + pub loc_feature: Option, + #[serde(rename = "extra-loc-blank", skip_serializing_if = "Option::is_none")] + pub loc_blank: Option, } #[derive(Debug, Eq, Deserialize, Serialize, PartialEq)] pub struct ContextNode { - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, + pub name: String, #[serde(rename = "message")] pub messages: Vec, #[serde(skip_serializing_if = "Option::is_none")] - comment: Option, + pub comment: Option, #[serde(rename = "@encoding", skip_serializing_if = "Option::is_none")] - encoding: Option, + pub encoding: Option, } #[derive(Debug, Deserialize, Serialize, PartialEq)] @@ -70,34 +102,67 @@ pub struct Dependency { catalog: String, } -#[derive(Debug, Eq, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Eq, Clone, Deserialize, Serialize, PartialEq)] pub struct MessageNode { + /// Original string to translate #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, + /// Result of a merge #[serde(skip_serializing_if = "Option::is_none")] - oldsource: Option, // Result of merge + pub oldsource: Option, #[serde(skip_serializing_if = "Option::is_none")] pub translation: Option, #[serde(skip_serializing_if = "Vec::is_empty", rename = "location", default)] pub locations: Vec, + /// This is "disambiguation" in the (new) API, or "msgctxt" in gettext speak #[serde(skip_serializing_if = "Option::is_none")] - comment: Option, + pub comment: Option, + /// Previous content of comment (result of merge) #[serde(skip_serializing_if = "Option::is_none")] - oldcomment: Option, + pub oldcomment: Option, + /// The real comment (added by developer/designer) #[serde(skip_serializing_if = "Option::is_none")] extracomment: Option, + /// Comment added by translator #[serde(skip_serializing_if = "Option::is_none")] translatorcomment: Option, #[serde(rename = "@numerus", skip_serializing_if = "Option::is_none")] numerus: Option, #[serde(skip_serializing_if = "Option::is_none")] - id: Option, + pub id: Option, #[serde(skip_serializing_if = "Option::is_none")] userdata: Option, - // todo: extra-something + /* + Following section corresponds to `extra-something` in Qt's XSD. From documentation: + > extra elements may appear in TS and message elements. Each element may appear + > only once within each scope. The contents are preserved verbatim; any + > attributes are dropped. + */ + #[serde( + rename = "extra-po-msgid_plural", + skip_serializing_if = "Option::is_none" + )] + pub po_msg_id_plural: Option, + #[serde( + rename = "extra-po-old_msgid_plural", + skip_serializing_if = "Option::is_none" + )] + pub po_old_msg_id_plural: Option, + /// Comma separated list + #[serde(rename = "extra-po-flags", skip_serializing_if = "Option::is_none")] + pub loc_flags: Option, + #[serde( + rename = "extra-loc-layout_id", + skip_serializing_if = "Option::is_none" + )] + pub loc_layout_id: Option, + #[serde(rename = "extra-loc-feature", skip_serializing_if = "Option::is_none")] + pub loc_feature: Option, + #[serde(rename = "extra-loc-blank", skip_serializing_if = "Option::is_none")] + pub loc_blank: Option, } -#[derive(Debug, Eq, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Eq, Clone, Deserialize, Serialize, PartialEq)] pub struct TranslationNode { // Did not find a way to make it an enum // Therefore: either you have a `translation_simple` or a `numerus_forms`, but not both. @@ -113,7 +178,7 @@ pub struct TranslationNode { userdata: Option, // deprecated } -#[derive(Debug, Eq, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Eq, Clone, Deserialize, Serialize, PartialEq)] pub struct LocationNode { #[serde(rename = "@filename", skip_serializing_if = "Option::is_none")] pub filename: Option, @@ -121,7 +186,7 @@ pub struct LocationNode { pub line: Option, } -#[derive(Debug, Eq, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Eq, Clone, Deserialize, Serialize, PartialEq)] pub struct NumerusFormNode { #[serde(default, rename = "$value", skip_serializing_if = "String::is_empty")] text: String, @@ -198,10 +263,8 @@ impl PartialOrd for ContextNode { fn partial_cmp(&self, other: &Self) -> Option { // Contexts are generally module or classes names; let's assume they don't need any special collation treatment. self.name - .as_ref() - .unwrap_or(&"".to_owned()) .to_lowercase() - .partial_cmp(&other.name.as_ref().unwrap_or(&"".to_owned()).to_lowercase()) + .partial_cmp(&other.name.to_lowercase()) } } @@ -251,10 +314,10 @@ pub fn write_to_output(output_path: &Option, node: &TSNode) -> Result<() #[cfg(test)] mod write_file_test { - use super::*; - use crate::ts; use quick_xml; + use super::*; + #[test] fn test_write_to_output_file() { const OUTPUT_TEST_FILE: &str = "./test_data/test_result_write_to_ts.xml"; @@ -264,7 +327,7 @@ mod write_file_test { let data: TSNode = quick_xml::de::from_reader(reader.into_inner()).expect("Parsable"); - ts::write_to_output(&Some(OUTPUT_TEST_FILE.to_owned()), &data).expect("Output"); + write_to_output(&Some(OUTPUT_TEST_FILE.to_owned()), &data).expect("Output"); let f = quick_xml::Reader::from_file(OUTPUT_TEST_FILE).expect("Couldn't open output test file"); @@ -277,8 +340,10 @@ mod write_file_test { #[cfg(test)] mod test { - use super::*; use quick_xml; + + use super::*; + // TODO: Data set. https://github.com/qt/qttranslations/ #[test] fn test_parse_with_numerus_forms() { @@ -292,7 +357,7 @@ mod test { assert_eq!(data.language.unwrap(), "sv"); let context1 = &data.contexts[0]; - assert_eq!(context1.name.as_ref().unwrap(), "kernel/navigationpart"); + assert_eq!(context1.name, "kernel/navigationpart"); assert_eq!(context1.messages.len(), 3); let message_c1_2 = &context1.messages[1]; @@ -347,7 +412,7 @@ mod test { assert_eq!(data.language.unwrap(), "de"); let context1 = &data.contexts[0]; - assert_eq!(context1.name.as_ref().unwrap(), "tst_QKeySequence"); + assert_eq!(context1.name, "tst_QKeySequence"); assert_eq!(context1.messages.len(), 11); let message_c1_2 = &context1.messages[2]; let locations = &message_c1_2.locations; diff --git a/test_data/example_merge_left.xml b/test_data/example_merge_left.xml new file mode 100644 index 0000000..39bea26 --- /dev/null +++ b/test_data/example_merge_left.xml @@ -0,0 +1,59 @@ + + + + + kernel/in_merge + + idBasedLeft + Source string Left should be in oldsource + + + + Newsletter + Nyhetsbrev - Left + + + Only In Left Message + Left message + Only in Left translation + + + %1 takes at most %n argument(s). %2 is therefore invalid. + + %1 prend au maximum %n argument. %2 est donc invalide. + %1 prend au maximum %n arguments. %2 est donc invalide. + + This is a old comment from Left. Should be in oldcomment. + + + + kernel/not_in_merge + + Newsletter + Nyhetsbrev - Left + + + + Name + + + + + An example entry for Name + + + + + + This is just a Sample + Dies ist nur ein Beispiel + + + Practice more + + + + I am a message in which was written in Code + + + diff --git a/test_data/example_merge_result.xml b/test_data/example_merge_result.xml new file mode 100644 index 0000000..43679db --- /dev/null +++ b/test_data/example_merge_result.xml @@ -0,0 +1,91 @@ + + + + + kernel/in_merge + + Only In Left Message + + Only in Left translation + + Left message + + + Now Right Source + Source string Left should be in oldsource + + Also, translated. + + idBasedLeft + + + Newsletter + + Nyhetsbrev - Right + + + + %1 takes at most %n argument(s). %2 is therefore invalid. + + + %1 prend au maximum %n argument. %2 est donc invalide. + + + %1 prend au maximum %n arguments. %2 est donc invalide. + + + Coming from right. + This is a old comment from Left. Should be in oldcomment. + + + + kernel/not_in_merge + + Newsletter + + Nyhetsbrev - Left + + + + + kernel/in_merge_from_right + + Extra Context + + Context Extra Right + + + + + This is just a Sample + + Dies ist nur ein Beispiel + + + + + + Practice more + + + I am a message in which was written in Code + + + + Name + + + + + + An example entry for Name + + + This is a new string + + Ceci est une nouvelle chaîne + + + + + \ No newline at end of file diff --git a/test_data/example_merge_right.xml b/test_data/example_merge_right.xml new file mode 100644 index 0000000..87843e2 --- /dev/null +++ b/test_data/example_merge_right.xml @@ -0,0 +1,50 @@ + + + + + kernel/in_merge + + idBasedLeft + Now Right Source + Also, translated. + + + Newsletter + Nyhetsbrev - Right + + + %1 takes at most %n argument(s). %2 is therefore invalid. + + %1 prend au maximum %n argument. %2 est donc invalide. + %1 prend au maximum %n arguments. %2 est donc invalide. + + Coming from right. + + + + kernel/in_merge_from_right + + Extra Context + Context Extra Right + + + + + + This is just a Sample + Dies ist nur ein Beispiel + + + Practice more + + + I am a message in which was written in Code + + + + + + This is a new string + Ceci est une nouvelle chaîne + +