From 96d7ef85104665f85a06733cdec141dd76917794 Mon Sep 17 00:00:00 2001 From: Alexandre Leblanc Date: Sat, 10 Feb 2024 00:23:48 -0500 Subject: [PATCH 01/16] fixes #23: Add the merge command front-end. --- src/main.rs | 3 +++ src/merge.rs | 13 +++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 src/merge.rs diff --git a/src/main.rs b/src/main.rs index 0783a91..16a1724 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ 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 +18,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..3163759 --- /dev/null +++ b/src/merge.rs @@ -0,0 +1,13 @@ +use clap::Args; + +#[derive(Args)] +pub struct MergeArgs { + pub input_left: String, + pub input_right: String, + #[arg(short, long)] + pub output_path: Option, +} + +pub fn merge_main(_args: &MergeArgs) -> Result<(), String> { + Err("Merge is not yet implemented.".to_owned()) +} From 4f10851daca0baa986a78c1c98937bda5899724e Mon Sep 17 00:00:00 2001 From: Alexandre Leblanc Date: Sat, 10 Feb 2024 15:20:36 -0500 Subject: [PATCH 02/16] fixes #23: Add missing import. --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index 16a1724..b36c6fa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod extract; +mod merge; mod sort; mod ts; From 137eb5dcb632b5df58f0566deffff8a764682a33 Mon Sep 17 00:00:00 2001 From: Alexandre Leblanc Date: Sat, 10 Feb 2024 15:32:22 -0500 Subject: [PATCH 03/16] fixes #25: Add command line documentation. --- src/extract.rs | 4 ++++ src/merge.rs | 4 ++++ src/sort.rs | 3 +++ 3 files changed, 11 insertions(+) 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/merge.rs b/src/merge.rs index 3163759..7bd03b3 100644 --- a/src/merge.rs +++ b/src/merge.rs @@ -1,9 +1,13 @@ use clap::Args; +/// 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, } diff --git a/src/sort.rs b/src/sort.rs index befb470..fc39043 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, } From 94c840c0bd48b8d963011746ccfc3874a8381242 Mon Sep 17 00:00:00 2001 From: Alexandre Leblanc Date: Sat, 10 Feb 2024 15:38:56 -0500 Subject: [PATCH 04/16] fixes #12: Add feature request template. --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md 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 From 6a32ae3ff20b16fe3cd53ac1b9354e680188fec9 Mon Sep 17 00:00:00 2001 From: Alexandre Leblanc Date: Sat, 10 Feb 2024 15:44:08 -0500 Subject: [PATCH 05/16] fixes #25: Update CHANGELOG.md to include #25. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 922aace..05cd7b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,14 @@ 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 ## Changed +- Improved command line documentation. [#25](https://github.com/mrtryhard/qt-ts-tools/issues/25) + ## Fixed ## [0.2.0] - 2024-01-01 From f04c9afe4684897e691502fa246d7e9936e8bcdc Mon Sep 17 00:00:00 2001 From: Alexandre Leblanc Date: Sat, 10 Feb 2024 15:51:45 -0500 Subject: [PATCH 06/16] fixes #26: Update Clap dependency --- CHANGELOG.md | 7 ++++--- Cargo.lock | 20 ++++++++++---------- Cargo.toml | 2 +- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05cd7b5..d997b08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## Changed -- Improved command line documentation. [#25](https://github.com/mrtryhard/qt-ts-tools/issues/25) +- Improved command line documentation [#25](https://github.com/mrtryhard/qt-ts-tools/issues/25) +- Updated Clap dependencies [#26](https://github.com/mrtryhard/qt-ts-tools/issues/26) ## Fixed @@ -21,7 +22,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 @@ -36,4 +37,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..2c7ae96 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" @@ -167,9 +167,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..e604774 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ edition = "2021" # 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"] } From cf1b8439925013205c9811ef6818b140762046db Mon Sep 17 00:00:00 2001 From: Alexandre Leblanc Date: Sat, 10 Feb 2024 15:57:24 -0500 Subject: [PATCH 07/16] update: Update version to 0.3.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c7ae96..7ad27ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,7 +119,7 @@ dependencies = [ [[package]] name = "qt-ts-tools" -version = "0.2.0" +version = "0.3.0" dependencies = [ "clap", "quick-xml", diff --git a/Cargo.toml b/Cargo.toml index e604774..bec4392 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ 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" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 10490667c1c243d4860211118650cc28d6dc6be4 Mon Sep 17 00:00:00 2001 From: Alexandre Leblanc Date: Thu, 15 Feb 2024 23:48:23 -0500 Subject: [PATCH 08/16] fixes #4: Deserialize and serialize extra tags. It could probably benefit being parsed to allow any kind of `extra-` tags to avoid data loss. For now, simple and straightforward implementation. --- src/ts.rs | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/src/ts.rs b/src/ts.rs index 1f8de06..5b85bb4 100644 --- a/src/ts.rs +++ b/src/ts.rs @@ -7,9 +7,13 @@ use std::io::{BufWriter, Write}; // 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, Eq, Deserialize, Serialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum TranslationType { + #[default] + #[serde(skip)] + Finished, Unfinished, Obsolete, Vanished, @@ -45,6 +49,25 @@ 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)] @@ -74,18 +97,23 @@ pub struct Dependency { pub struct MessageNode { #[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 + 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, + /// Previous content of comment (result of merge) #[serde(skip_serializing_if = "Option::is_none")] 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")] @@ -94,7 +122,25 @@ pub struct MessageNode { 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)] @@ -252,7 +298,6 @@ 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; #[test] @@ -264,7 +309,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"); From 7feb1efeb4f72cfa5cb3f2438101057b44da0b0e Mon Sep 17 00:00:00 2001 From: Alexandre Leblanc Date: Thu, 15 Feb 2024 23:57:27 -0500 Subject: [PATCH 09/16] fixes #27: Added tool description in help message. --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index bec4392..1f0713e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ homepage = "https://github.com/mrtryhard/qt-ts-tools" license = "MIT OR Apache-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 From 0147b42f85f0aa25017cf86f82fc7ad1650c26a8 Mon Sep 17 00:00:00 2001 From: Alexandre Leblanc Date: Fri, 16 Feb 2024 00:01:51 -0500 Subject: [PATCH 10/16] update: Update CHANGELOG.md. - Adds entry for #27 (command line improvement) - Adds entry for #4 (`extra-*` tags support) --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d997b08..8149238 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## Added +- `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) +- 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 From 065f61aceee6ad4ff9b60cfb401d6abf079ada93 Mon Sep 17 00:00:00 2001 From: Alexandre Leblanc Date: Sat, 17 Feb 2024 12:44:59 -0500 Subject: [PATCH 11/16] wip: Merge feature. --- CHANGELOG.md | 1 + src/merge.rs | 27 ++++++++++++++++++++++++++- src/ts.rs | 5 +++-- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8149238..1b8be47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## 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 diff --git a/src/merge.rs b/src/merge.rs index 7bd03b3..b780c3f 100644 --- a/src/merge.rs +++ b/src/merge.rs @@ -1,3 +1,5 @@ +use crate::ts; +use crate::ts::TSNode; use clap::Args; /// Merges two translation file contexts and messages into a single output. @@ -12,6 +14,29 @@ pub struct MergeArgs { pub output_path: Option, } -pub fn merge_main(_args: &MergeArgs) -> Result<(), String> { +pub fn merge_main(args: &MergeArgs) -> Result<(), String> { + let left: Result = quick_xml::Reader::from_file(&args.input_left) + .and_then(|file| quick_xml::de::from_reader(file.into_inner())?); + let right: Result = quick_xml::Reader::from_file(&args.input_right) + .and_then(|file| quick_xml::de::from_reader(file.into_inner())?); + + 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 mut left = left.unwrap(); + let right = right.unwrap(); + + // Priority is always `right` wins. + // Discriminant is `source` + // Get messages that are completely different: different, non-matching source + // Get messages that are common but has different properties + // Add missing message in left from right + // Update left messages from right, that matches on right (common message gets updated) + Err("Merge is not yet implemented.".to_owned()) } diff --git a/src/ts.rs b/src/ts.rs index 5b85bb4..9e08e91 100644 --- a/src/ts.rs +++ b/src/ts.rs @@ -37,8 +37,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")] @@ -95,6 +95,7 @@ pub struct Dependency { #[derive(Debug, Eq, Deserialize, Serialize, PartialEq)] pub struct MessageNode { + /// Original string to translate #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, /// Result of a merge From 48cb834ba71841651a3a2648a1dc6f7813b0cf19 Mon Sep 17 00:00:00 2001 From: Alexandre Leblanc Date: Sun, 14 Apr 2024 18:20:01 +0900 Subject: [PATCH 12/16] wip: Implement merge for contextless message nodes. Progres for issue #24. Remains applying context merges. --- Cargo.lock | 16 +++++ Cargo.toml | 1 + src/merge.rs | 92 +++++++++++++++++++++----- src/ts.rs | 64 +++++++++++------- test_data/example_merge_finished.xml | 23 +++++++ test_data/example_merge_unfinished.xml | 27 ++++++++ 6 files changed, 185 insertions(+), 38 deletions(-) create mode 100644 test_data/example_merge_finished.xml create mode 100644 test_data/example_merge_unfinished.xml diff --git a/Cargo.lock b/Cargo.lock index 7ad27ba..851c996 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" @@ -122,6 +137,7 @@ name = "qt-ts-tools" version = "0.3.0" dependencies = [ "clap", + "itertools", "quick-xml", "serde", ] diff --git a/Cargo.toml b/Cargo.toml index 1f0713e..69adff4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ description = "Small command line utility to manipulate Qt's translation files w 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/merge.rs b/src/merge.rs index b780c3f..64c3661 100644 --- a/src/merge.rs +++ b/src/merge.rs @@ -1,6 +1,8 @@ use crate::ts; -use crate::ts::TSNode; +use crate::ts::{MessageNode, TSNode}; use clap::Args; +use itertools::Itertools; +use std::hash::{Hash, Hasher}; /// Merges two translation file contexts and messages into a single output. #[derive(Args)] @@ -14,29 +16,89 @@ pub struct MergeArgs { pub output_path: Option, } +#[derive(Eq, PartialOrd, Clone)] +struct NodeGroup { + pub node: MessageNode, +} + +impl PartialEq for NodeGroup { + fn eq(&self, other: &Self) -> bool { + self.node.source == other.node.source && self.node.locations == other.node.locations + } +} + +impl Hash for NodeGroup { + 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 works 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: Result = quick_xml::Reader::from_file(&args.input_left) - .and_then(|file| quick_xml::de::from_reader(file.into_inner())?); - let right: Result = quick_xml::Reader::from_file(&args.input_right) - .and_then(|file| quick_xml::de::from_reader(file.into_inner())?); + 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())); + 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())); + return Err(format!( + "Could not process right file '{}'. Error: {}", + &args.input_right, + e.to_string() + )); } + let mut right = right.unwrap(); let mut left = left.unwrap(); - let right = right.unwrap(); - // Priority is always `right` wins. - // Discriminant is `source` - // Get messages that are completely different: different, non-matching source - // Get messages that are common but has different properties - // Add missing message in left from right - // Update left messages from right, that matches on right (common message gets updated) + left.messages = merge_messages(&mut left.messages, &mut right.messages); - Err("Merge is not yet implemented.".to_owned()) + ts::write_to_output(&args.output_path, &left) +} + +/// 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| NodeGroup { node }) + .collect(); + + let unique_messages_right: Vec<_> = right_messages + .drain(0..) + .map(|node| NodeGroup { node }) + .collect(); + + 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()), + } } diff --git a/src/ts.rs b/src/ts.rs index 9e08e91..b9a7114 100644 --- a/src/ts.rs +++ b/src/ts.rs @@ -8,7 +8,7 @@ use std::io::{BufWriter, Write}; // https://doc.qt.io/qt-6/linguist-ts-file-format.html /// If no type is set, a message is "finished". -#[derive(Debug, Default, Eq, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Default, Clone, Eq, Deserialize, Serialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum TranslationType { #[default] @@ -19,7 +19,7 @@ pub enum TranslationType { Vanished, } -#[derive(Debug, Eq, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Eq, Clone, Deserialize, Serialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum YesNo { Yes, @@ -50,19 +50,28 @@ pub struct TSNode { #[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")] + 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")] + #[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")] + #[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, @@ -93,7 +102,7 @@ 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")] @@ -120,23 +129,32 @@ pub struct MessageNode { #[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, /* - 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")] + 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")] + #[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")] + #[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, @@ -144,7 +162,7 @@ pub struct MessageNode { 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. @@ -160,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, @@ -168,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, diff --git a/test_data/example_merge_finished.xml b/test_data/example_merge_finished.xml new file mode 100644 index 0000000..00a5763 --- /dev/null +++ b/test_data/example_merge_finished.xml @@ -0,0 +1,23 @@ + + + + + + + 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 + + diff --git a/test_data/example_merge_unfinished.xml b/test_data/example_merge_unfinished.xml new file mode 100644 index 0000000..c290fe9 --- /dev/null +++ b/test_data/example_merge_unfinished.xml @@ -0,0 +1,27 @@ + + + + + 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 + + + From 7c3ac663ab7d2263993dfbc9834bfe55660049bd Mon Sep 17 00:00:00 2001 From: Alexandre Leblanc Date: Thu, 18 Apr 2024 23:01:00 +0900 Subject: [PATCH 13/16] wip: Implement merge for contexts. Progress for issue #24. Remains applying unit tests. --- src/merge.rs | 16 ++++++++++++++++ src/ts.rs | 7 ++----- test_data/example_merge_finished.xml | 22 ++++++++++++++++++++++ test_data/example_merge_unfinished.xml | 26 ++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 5 deletions(-) diff --git a/src/merge.rs b/src/merge.rs index 64c3661..751ee9c 100644 --- a/src/merge.rs +++ b/src/merge.rs @@ -63,10 +63,26 @@ pub fn merge_main(args: &MergeArgs) -> Result<(), String> { let mut left = left.unwrap(); left.messages = merge_messages(&mut left.messages, &mut right.messages); + merge_contexts(right, &mut left); ts::write_to_output(&args.output_path, &left) } +fn merge_contexts(right: TSNode, left: &mut 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.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, diff --git a/src/ts.rs b/src/ts.rs index b9a7114..5b8ffbf 100644 --- a/src/ts.rs +++ b/src/ts.rs @@ -81,8 +81,7 @@ pub struct TSNode { #[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")] @@ -263,10 +262,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()) } } diff --git a/test_data/example_merge_finished.xml b/test_data/example_merge_finished.xml index 00a5763..9aa09b2 100644 --- a/test_data/example_merge_finished.xml +++ b/test_data/example_merge_finished.xml @@ -1,6 +1,28 @@ + + kernel/in_merge + + 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 + + diff --git a/test_data/example_merge_unfinished.xml b/test_data/example_merge_unfinished.xml index c290fe9..4a1aabb 100644 --- a/test_data/example_merge_unfinished.xml +++ b/test_data/example_merge_unfinished.xml @@ -1,6 +1,32 @@ + + kernel/in_merge + + 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. + + + + + kernel/not_in_merge + + Newsletter + Nyhetsbrev - Left + + Name From e2e4504e6dd121c038869811736b86fae5c108ed Mon Sep 17 00:00:00 2001 From: Alexandre Leblanc Date: Thu, 18 Apr 2024 23:18:53 +0900 Subject: [PATCH 14/16] fixes #24: Complete merge implementation. --- src/merge.rs | 40 +++++++++++++++---- src/sort.rs | 4 +- src/ts.rs | 14 ++++--- ..._unfinished.xml => example_merge_left.xml} | 0 ...e_finished.xml => example_merge_right.xml} | 0 5 files changed, 43 insertions(+), 15 deletions(-) rename test_data/{example_merge_unfinished.xml => example_merge_left.xml} (100%) rename test_data/{example_merge_finished.xml => example_merge_right.xml} (100%) diff --git a/src/merge.rs b/src/merge.rs index 751ee9c..0a09e96 100644 --- a/src/merge.rs +++ b/src/merge.rs @@ -1,8 +1,10 @@ -use crate::ts; -use crate::ts::{MessageNode, TSNode}; +use std::hash::{Hash, Hasher}; + use clap::Args; use itertools::Itertools; -use std::hash::{Hash, Hasher}; + +use crate::ts; +use crate::ts::{MessageNode, TSNode}; /// Merges two translation file contexts and messages into a single output. #[derive(Args)] @@ -59,13 +61,15 @@ pub fn merge_main(args: &MergeArgs) -> Result<(), String> { )); } - let mut right = right.unwrap(); - let mut left = left.unwrap(); + 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(right, &mut left); - - ts::write_to_output(&args.output_path, &left) + left } fn merge_contexts(right: TSNode, left: &mut TSNode) { @@ -76,7 +80,8 @@ fn merge_contexts(right: TSNode, left: &mut TSNode) { .find(|left_context| left_context.name == right_context.name); if let Some(left_context) = left_context_opt { - left_context.messages = merge_messages(&mut left_context.messages, &mut right_context.messages); + left_context.messages = + merge_messages(&mut left_context.messages, &mut right_context.messages); } else { left.contexts.push(right_context); } @@ -118,3 +123,22 @@ fn load_file(path: &String) -> Result { 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 fc39043..97d4161 100644 --- a/src/sort.rs +++ b/src/sort.rs @@ -71,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 5b8ffbf..3e6f854 100644 --- a/src/ts.rs +++ b/src/ts.rs @@ -1,7 +1,8 @@ -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. @@ -313,9 +314,10 @@ pub fn write_to_output(output_path: &Option, node: &TSNode) -> Result<() #[cfg(test)] mod write_file_test { - use super::*; 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"; @@ -338,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() { @@ -353,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]; @@ -408,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_unfinished.xml b/test_data/example_merge_left.xml similarity index 100% rename from test_data/example_merge_unfinished.xml rename to test_data/example_merge_left.xml diff --git a/test_data/example_merge_finished.xml b/test_data/example_merge_right.xml similarity index 100% rename from test_data/example_merge_finished.xml rename to test_data/example_merge_right.xml From 93d96549c27aae5c8eb0b49ab7e75364554c3a3c Mon Sep 17 00:00:00 2001 From: Alexandre Leblanc Date: Thu, 18 Apr 2024 23:19:46 +0900 Subject: [PATCH 15/16] fixes #24: Add missing file. --- test_data/example_merge_result.xml | 82 ++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 test_data/example_merge_result.xml diff --git a/test_data/example_merge_result.xml b/test_data/example_merge_result.xml new file mode 100644 index 0000000..0ed1e01 --- /dev/null +++ b/test_data/example_merge_result.xml @@ -0,0 +1,82 @@ + + + + + kernel/in_merge + + Only In Left Message + + Only in Left translation + + Left message + + + 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/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 From 5aff7cf6aabe1db02999741fc3537185e637d8f8 Mon Sep 17 00:00:00 2001 From: Alexandre Leblanc Date: Fri, 19 Apr 2024 00:45:38 +0900 Subject: [PATCH 16/16] fixes #24: Complete merge with attributes. --- src/merge.rs | 46 ++++++++++++++++++++++++------ src/ts.rs | 10 +++---- test_data/example_merge_left.xml | 6 ++++ test_data/example_merge_result.xml | 9 ++++++ test_data/example_merge_right.xml | 7 ++++- 5 files changed, 63 insertions(+), 15 deletions(-) diff --git a/src/merge.rs b/src/merge.rs index 0a09e96..cd3267c 100644 --- a/src/merge.rs +++ b/src/merge.rs @@ -18,18 +18,26 @@ pub struct MergeArgs { pub output_path: Option, } +/// MessageNode that can be `eq(...)`. #[derive(Eq, PartialOrd, Clone)] -struct NodeGroup { +struct EquatableMessageNode { pub node: MessageNode, } -impl PartialEq for NodeGroup { +/// 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 NodeGroup { +impl Hash for EquatableMessageNode { fn hash(&self, state: &mut H) { self.node.source.hash(state); self.node.locations.iter().for_each(|loc| { @@ -39,7 +47,7 @@ impl Hash for NodeGroup { } } -// This works by depending on cmp looking up only source and location on messages nodes +// 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); @@ -68,11 +76,11 @@ pub fn merge_main(args: &MergeArgs) -> Result<(), String> { fn merge_ts_nodes(mut left: TSNode, mut right: TSNode) -> TSNode { left.messages = merge_messages(&mut left.messages, &mut right.messages); - merge_contexts(right, &mut left); + merge_contexts(&mut left, right); left } -fn merge_contexts(right: TSNode, left: &mut TSNode) { +fn merge_contexts(left: &mut TSNode, right: TSNode) { right.contexts.into_iter().for_each(|mut right_context| { let left_context_opt = left .contexts @@ -80,6 +88,9 @@ fn merge_contexts(right: TSNode, left: &mut TSNode) { .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 { @@ -95,14 +106,31 @@ fn merge_messages( ) -> Vec { let mut unique_messages_left: Vec<_> = left_messages .drain(0..) - .map(|node| NodeGroup { node }) + .map(|node| EquatableMessageNode { node }) .collect(); - let unique_messages_right: Vec<_> = right_messages + let mut unique_messages_right: Vec<_> = right_messages .drain(0..) - .map(|node| NodeGroup { node }) + .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)) diff --git a/src/ts.rs b/src/ts.rs index 3e6f854..c76801e 100644 --- a/src/ts.rs +++ b/src/ts.rs @@ -86,9 +86,9 @@ pub struct ContextNode { #[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)] @@ -109,17 +109,17 @@ pub struct MessageNode { pub source: Option, /// Result of a merge #[serde(skip_serializing_if = "Option::is_none")] - oldsource: Option, + 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, diff --git a/test_data/example_merge_left.xml b/test_data/example_merge_left.xml index 4a1aabb..39bea26 100644 --- a/test_data/example_merge_left.xml +++ b/test_data/example_merge_left.xml @@ -3,6 +3,11 @@ kernel/in_merge + + idBasedLeft + Source string Left should be in oldsource + + Newsletter Nyhetsbrev - Left @@ -18,6 +23,7 @@ %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. diff --git a/test_data/example_merge_result.xml b/test_data/example_merge_result.xml index 0ed1e01..43679db 100644 --- a/test_data/example_merge_result.xml +++ b/test_data/example_merge_result.xml @@ -10,6 +10,14 @@ Left message + + Now Right Source + Source string Left should be in oldsource + + Also, translated. + + idBasedLeft + Newsletter @@ -27,6 +35,7 @@ Coming from right. + This is a old comment from Left. Should be in oldcomment. diff --git a/test_data/example_merge_right.xml b/test_data/example_merge_right.xml index 9aa09b2..87843e2 100644 --- a/test_data/example_merge_right.xml +++ b/test_data/example_merge_right.xml @@ -3,6 +3,11 @@ kernel/in_merge + + idBasedLeft + Now Right Source + Also, translated. + Newsletter Nyhetsbrev - Right @@ -34,7 +39,7 @@ I am a message in which was written in Code - +