|
| 1 | +use langcodec::traits::Parser; |
| 2 | +use langcodec::types::{Entry, EntryStatus, Metadata, Resource, Translation}; |
| 3 | +use langcodec::{Codec, convert_auto, formats::XcstringsFormat}; |
| 4 | +use std::collections::HashMap; |
| 5 | +use std::path::{Path, PathBuf}; |
| 6 | + |
| 7 | +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] |
| 8 | +enum MatrixFormat { |
| 9 | + Strings, |
| 10 | + AndroidXml, |
| 11 | + Xcstrings, |
| 12 | + Csv, |
| 13 | + Tsv, |
| 14 | +} |
| 15 | + |
| 16 | +impl MatrixFormat { |
| 17 | + fn id(self) -> &'static str { |
| 18 | + match self { |
| 19 | + MatrixFormat::Strings => "strings", |
| 20 | + MatrixFormat::AndroidXml => "android_xml", |
| 21 | + MatrixFormat::Xcstrings => "xcstrings", |
| 22 | + MatrixFormat::Csv => "csv", |
| 23 | + MatrixFormat::Tsv => "tsv", |
| 24 | + } |
| 25 | + } |
| 26 | + |
| 27 | + fn output_file_name(self, source: MatrixFormat) -> String { |
| 28 | + match self { |
| 29 | + MatrixFormat::Strings => format!("{}_to_strings.strings", source.id()), |
| 30 | + MatrixFormat::AndroidXml => format!("{}_to_strings.xml", source.id()), |
| 31 | + MatrixFormat::Xcstrings => format!("{}_to_localizable.xcstrings", source.id()), |
| 32 | + MatrixFormat::Csv => format!("{}_to_translations.csv", source.id()), |
| 33 | + MatrixFormat::Tsv => format!("{}_to_translations.tsv", source.id()), |
| 34 | + } |
| 35 | + } |
| 36 | +} |
| 37 | + |
| 38 | +fn build_seed_resource() -> Resource { |
| 39 | + let mut custom = HashMap::new(); |
| 40 | + custom.insert("source_language".to_string(), "en".to_string()); |
| 41 | + custom.insert("version".to_string(), "1.0".to_string()); |
| 42 | + |
| 43 | + Resource { |
| 44 | + metadata: Metadata { |
| 45 | + language: "en".to_string(), |
| 46 | + domain: "Localizable".to_string(), |
| 47 | + custom, |
| 48 | + }, |
| 49 | + entries: vec![ |
| 50 | + Entry { |
| 51 | + id: "hello".to_string(), |
| 52 | + value: Translation::Singular("Hello".to_string()), |
| 53 | + comment: None, |
| 54 | + status: EntryStatus::Translated, |
| 55 | + custom: HashMap::new(), |
| 56 | + }, |
| 57 | + Entry { |
| 58 | + id: "bye".to_string(), |
| 59 | + value: Translation::Singular("Goodbye".to_string()), |
| 60 | + comment: None, |
| 61 | + status: EntryStatus::Translated, |
| 62 | + custom: HashMap::new(), |
| 63 | + }, |
| 64 | + ], |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +fn write_seed_files(root: &Path) -> HashMap<MatrixFormat, PathBuf> { |
| 69 | + let strings_dir = root.join("seed").join("en.lproj"); |
| 70 | + let android_dir = root.join("seed").join("values-en"); |
| 71 | + let generic_dir = root.join("seed"); |
| 72 | + |
| 73 | + std::fs::create_dir_all(&strings_dir).expect("create strings seed dir"); |
| 74 | + std::fs::create_dir_all(&android_dir).expect("create android seed dir"); |
| 75 | + std::fs::create_dir_all(&generic_dir).expect("create generic seed dir"); |
| 76 | + |
| 77 | + let strings_path = strings_dir.join("Localizable.strings"); |
| 78 | + let android_path = android_dir.join("strings.xml"); |
| 79 | + let xcstrings_path = generic_dir.join("Localizable.xcstrings"); |
| 80 | + let csv_path = generic_dir.join("translations.csv"); |
| 81 | + let tsv_path = generic_dir.join("translations.tsv"); |
| 82 | + |
| 83 | + std::fs::write( |
| 84 | + &strings_path, |
| 85 | + "\"hello\" = \"Hello\";\n\"bye\" = \"Goodbye\";\n", |
| 86 | + ) |
| 87 | + .expect("write strings seed"); |
| 88 | + std::fs::write( |
| 89 | + &android_path, |
| 90 | + "<resources>\n <string name=\"hello\">Hello</string>\n <string name=\"bye\">Goodbye</string>\n</resources>\n", |
| 91 | + ) |
| 92 | + .expect("write android seed"); |
| 93 | + std::fs::write(&csv_path, "key,en\nhello,Hello\nbye,Goodbye\n").expect("write csv seed"); |
| 94 | + std::fs::write(&tsv_path, "key\ten\nhello\tHello\nbye\tGoodbye\n").expect("write tsv seed"); |
| 95 | + |
| 96 | + let xcstrings = XcstringsFormat::try_from(vec![build_seed_resource()]).expect("xcstrings seed"); |
| 97 | + xcstrings |
| 98 | + .write_to(&xcstrings_path) |
| 99 | + .expect("write xcstrings seed"); |
| 100 | + |
| 101 | + HashMap::from([ |
| 102 | + (MatrixFormat::Strings, strings_path), |
| 103 | + (MatrixFormat::AndroidXml, android_path), |
| 104 | + (MatrixFormat::Xcstrings, xcstrings_path), |
| 105 | + (MatrixFormat::Csv, csv_path), |
| 106 | + (MatrixFormat::Tsv, tsv_path), |
| 107 | + ]) |
| 108 | +} |
| 109 | + |
| 110 | +fn assert_en_entries(path: &Path, case_name: &str) { |
| 111 | + let mut codec = Codec::new(); |
| 112 | + codec |
| 113 | + .read_file_by_extension(path, Some("en".to_string())) |
| 114 | + .unwrap_or_else(|e| { |
| 115 | + panic!( |
| 116 | + "{case_name}: failed to read output {}: {}", |
| 117 | + path.display(), |
| 118 | + e |
| 119 | + ) |
| 120 | + }); |
| 121 | + |
| 122 | + let en_resource = codec |
| 123 | + .get_by_language("en") |
| 124 | + .unwrap_or_else(|| panic!("{case_name}: expected an 'en' resource")); |
| 125 | + assert!( |
| 126 | + en_resource.entries.len() >= 2, |
| 127 | + "{case_name}: expected at least 2 entries" |
| 128 | + ); |
| 129 | + |
| 130 | + let hello = codec |
| 131 | + .find_entry("hello", "en") |
| 132 | + .unwrap_or_else(|| panic!("{case_name}: missing key 'hello'")); |
| 133 | + match &hello.value { |
| 134 | + Translation::Singular(value) => assert_eq!(value, "Hello", "{case_name}: bad hello value"), |
| 135 | + other => panic!("{case_name}: expected singular hello, got {:?}", other), |
| 136 | + } |
| 137 | + |
| 138 | + let bye = codec |
| 139 | + .find_entry("bye", "en") |
| 140 | + .unwrap_or_else(|| panic!("{case_name}: missing key 'bye'")); |
| 141 | + match &bye.value { |
| 142 | + Translation::Singular(value) => assert_eq!(value, "Goodbye", "{case_name}: bad bye value"), |
| 143 | + other => panic!("{case_name}: expected singular bye, got {:?}", other), |
| 144 | + } |
| 145 | +} |
| 146 | + |
| 147 | +#[test] |
| 148 | +fn conversion_matrix_common_paths_preserve_values() { |
| 149 | + let temp = tempfile::tempdir().expect("create temp dir"); |
| 150 | + let seed_paths = write_seed_files(temp.path()); |
| 151 | + |
| 152 | + let formats = [ |
| 153 | + MatrixFormat::Strings, |
| 154 | + MatrixFormat::AndroidXml, |
| 155 | + MatrixFormat::Xcstrings, |
| 156 | + MatrixFormat::Csv, |
| 157 | + MatrixFormat::Tsv, |
| 158 | + ]; |
| 159 | + |
| 160 | + let output_root = temp.path().join("matrix_output"); |
| 161 | + std::fs::create_dir_all(&output_root).expect("create matrix output dir"); |
| 162 | + |
| 163 | + for source in formats { |
| 164 | + for target in formats { |
| 165 | + if source == target { |
| 166 | + continue; |
| 167 | + } |
| 168 | + |
| 169 | + let source_path = seed_paths |
| 170 | + .get(&source) |
| 171 | + .unwrap_or_else(|| panic!("missing seed path for {:?}", source)); |
| 172 | + let target_path = output_root.join(target.output_file_name(source)); |
| 173 | + |
| 174 | + let case_name = format!("{} -> {}", source.id(), target.id()); |
| 175 | + convert_auto(source_path, &target_path) |
| 176 | + .unwrap_or_else(|e| panic!("{case_name}: conversion failed: {}", e)); |
| 177 | + assert_en_entries(&target_path, &case_name); |
| 178 | + } |
| 179 | + } |
| 180 | +} |
0 commit comments