use std::{collections::HashMap, io::BufRead};
use crate::{
error::Error,
traits::Parser,
types::{Entry, EntryStatus, Metadata, Resource, Translation},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MultiLanguageCSVRecord {
pub key: String,
pub translations: HashMap<String, String>,
}
impl MultiLanguageCSVRecord {
pub fn new(key: String) -> Self {
Self {
key,
translations: HashMap::new(),
}
}
pub fn add_translation(&mut self, language: String, value: String) {
self.translations.insert(language, value);
}
pub fn get_translation(&self, language: &str) -> Option<&String> {
self.translations.get(language)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Format {
pub records: Vec<MultiLanguageCSVRecord>,
}
impl Format {
pub fn new() -> Self {
Self {
records: Vec::new(),
}
}
pub fn with_records(records: Vec<MultiLanguageCSVRecord>) -> Self {
Self { records }
}
pub fn add_record(&mut self, record: MultiLanguageCSVRecord) {
self.records.push(record);
}
pub fn get_records(&self) -> &[MultiLanguageCSVRecord] {
&self.records
}
pub fn get_records_mut(&mut self) -> &mut [MultiLanguageCSVRecord] {
&mut self.records
}
}
impl Default for Format {
fn default() -> Self {
Self::new()
}
}
impl Parser for Format {
fn from_reader<R: BufRead>(reader: R) -> Result<Self, Error> {
let mut rdr = csv::ReaderBuilder::new()
.has_headers(false)
.from_reader(reader);
let mut records = Vec::new();
let mut lines = rdr.records();
if let Some(first_line) = lines.next() {
let first_line = first_line.map_err(Error::CsvParse)?;
if first_line.len() == 2 {
if first_line[0].trim().eq_ignore_ascii_case("key") {
let language = first_line[1].trim().to_string();
if language.is_empty() {
return Err(Error::DataMismatch(
"Invalid CSV format: missing language in header".to_string(),
));
}
for line in lines {
let line = line.map_err(Error::CsvParse)?;
if line.len() == 2 {
let mut record = MultiLanguageCSVRecord::new(line[0].to_string());
record.add_translation(language.clone(), line[1].to_string());
records.push(record);
}
}
} else {
records.push(MultiLanguageCSVRecord {
key: first_line[0].to_string(),
translations: {
let mut map = HashMap::new();
map.insert("default".to_string(), first_line[1].to_string());
map
},
});
for line in lines {
let line = line.map_err(Error::CsvParse)?;
if line.len() == 2 {
let mut record = MultiLanguageCSVRecord::new(line[0].to_string());
record.add_translation("default".to_string(), line[1].to_string());
records.push(record);
}
}
}
} else if first_line.len() >= 3 {
let languages: Vec<String> =
first_line.iter().skip(1).map(|s| s.to_string()).collect();
for line in lines {
let line = line.map_err(Error::CsvParse)?;
if line.len() >= 2 {
let mut record = MultiLanguageCSVRecord::new(line[0].to_string());
for (i, lang) in languages.iter().enumerate() {
if i + 1 < line.len() {
record.add_translation(lang.clone(), line[i + 1].to_string());
}
}
records.push(record);
}
}
} else {
return Err(Error::DataMismatch(
"Invalid CSV format: insufficient columns".to_string(),
));
}
}
Ok(Format { records })
}
fn to_writer<W: std::io::Write>(&self, writer: W) -> Result<(), Error> {
if self.records.is_empty() {
return Ok(());
}
let mut wtr = csv::WriterBuilder::new().from_writer(writer);
let mut all_languages = std::collections::HashSet::new();
for record in &self.records {
for lang in record.translations.keys() {
all_languages.insert(lang.clone());
}
}
let mut sorted_languages: Vec<String> = all_languages.into_iter().collect();
sorted_languages.sort();
let mut header = vec!["key".to_string()];
header.extend(sorted_languages.clone());
wtr.write_record(&header).map_err(Error::CsvParse)?;
for record in &self.records {
let mut row = vec![record.key.clone()];
let empty_string = String::new();
for lang in &sorted_languages {
let value = record.translations.get(lang).unwrap_or(&empty_string);
row.push(value.clone());
}
wtr.write_record(&row).map_err(Error::CsvParse)?;
}
wtr.flush().map_err(Error::Io)?;
Ok(())
}
}
impl TryFrom<Vec<Resource>> for Format {
type Error = Error;
fn try_from(resources: Vec<Resource>) -> Result<Self, Self::Error> {
if resources.is_empty() {
return Ok(Format::new());
}
let mut all_keys = std::collections::HashSet::new();
for resource in &resources {
for entry in &resource.entries {
all_keys.insert(entry.id.clone());
}
}
let mut records = Vec::new();
for key in all_keys {
let mut record = MultiLanguageCSVRecord::new(key);
for resource in &resources {
if let Some(entry) = resource.entries.iter().find(|e| e.id == record.key) {
let value = match &entry.value {
Translation::Empty => String::new(),
Translation::Singular(v) => v.clone(),
Translation::Plural(_) => String::new(), };
record.add_translation(resource.metadata.language.clone(), value);
}
}
records.push(record);
}
Ok(Format { records })
}
}
impl TryFrom<Format> for Vec<Resource> {
type Error = Error;
fn try_from(format: Format) -> Result<Self, Self::Error> {
if format.records.is_empty() {
return Ok(Vec::new());
}
let mut all_languages = std::collections::HashSet::new();
for record in &format.records {
for lang in record.translations.keys() {
all_languages.insert(lang.clone());
}
}
let mut resources = Vec::new();
let mut custom_metadata = HashMap::new();
let source_language = all_languages
.iter()
.next()
.unwrap_or(&"en".to_string())
.clone();
custom_metadata.insert("source_language".to_string(), source_language);
custom_metadata.insert("version".to_string(), "1.0".to_string());
for language in all_languages {
let mut resource = Resource {
metadata: Metadata {
language: language.clone(),
domain: String::from(""),
custom: custom_metadata.clone(),
},
entries: Vec::new(),
};
for record in &format.records {
if let Some(translation) = record.translations.get(&language) {
resource.entries.push(Entry {
id: record.key.clone(),
value: Translation::Singular(translation.clone()),
comment: None,
status: EntryStatus::Translated,
custom: HashMap::new(),
});
}
}
if !resource.entries.is_empty() {
resources.push(resource);
}
}
Ok(resources)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::Parser;
use crate::types::{Resource, Translation};
use std::io::Cursor;
#[test]
fn test_parse_simple_csv() {
let csv_content = "hello,Hello\nbye,Goodbye\n";
let format = Format::from_reader(Cursor::new(csv_content)).unwrap();
assert_eq!(format.records.len(), 2);
assert_eq!(format.records[0].key, "hello");
assert_eq!(
format.records[0].get_translation("default"),
Some(&"Hello".to_string())
);
assert_eq!(format.records[1].key, "bye");
assert_eq!(
format.records[1].get_translation("default"),
Some(&"Goodbye".to_string())
);
}
#[test]
fn test_round_trip_csv_resource_csv() {
let csv_content = "hello,Hello\nbye,Goodbye\n";
let format = Format::from_reader(Cursor::new(csv_content)).unwrap();
let resources = Vec::<Resource>::try_from(format.clone()).unwrap();
let serialized: Format = TryFrom::try_from(resources).unwrap();
let mut original_records = format.records.clone();
let mut serialized_records = serialized.records.clone();
original_records.sort_by(|a, b| a.key.cmp(&b.key));
serialized_records.sort_by(|a, b| a.key.cmp(&b.key));
assert_eq!(original_records, serialized_records);
}
#[test]
fn test_csv_row_with_empty_value() {
let csv_content = "empty,\nhello,Hello\n";
let format = Format::from_reader(Cursor::new(csv_content)).unwrap();
assert_eq!(format.records.len(), 2);
assert_eq!(format.records[0].key, "empty");
assert_eq!(
format.records[0].get_translation("default"),
Some(&"".to_string())
);
let resources = Vec::<Resource>::try_from(format.clone()).unwrap();
assert_eq!(resources.len(), 1);
let resource = &resources[0];
assert_eq!(resource.entries.len(), 2);
let entry = &resource.entries[0];
assert_eq!(entry.id, "empty");
assert_eq!(
match &entry.value {
Translation::Singular(s) => s,
_ => panic!("Expected singular translation"),
},
""
);
}
#[test]
fn test_parse_multi_language_csv() {
let csv_content = "key,en,cn\nhello,Hello,你好\nbye,Goodbye,再见\n";
let format = Format::from_reader(Cursor::new(csv_content)).unwrap();
assert_eq!(format.records.len(), 2);
assert_eq!(format.records[0].key, "hello");
assert_eq!(
format.records[0].get_translation("en"),
Some(&"Hello".to_string())
);
assert_eq!(
format.records[0].get_translation("cn"),
Some(&"你好".to_string())
);
assert_eq!(format.records[1].key, "bye");
assert_eq!(
format.records[1].get_translation("en"),
Some(&"Goodbye".to_string())
);
assert_eq!(
format.records[1].get_translation("cn"),
Some(&"再见".to_string())
);
}
#[test]
fn test_parse_single_language_csv_as_multi() {
let csv_content = "hello,Hello\nbye,Goodbye\n";
let format = Format::from_reader(Cursor::new(csv_content)).unwrap();
assert_eq!(format.records.len(), 2);
assert_eq!(format.records[0].key, "hello");
assert_eq!(
format.records[0].get_translation("default"),
Some(&"Hello".to_string())
);
assert_eq!(format.records[1].key, "bye");
assert_eq!(
format.records[1].get_translation("default"),
Some(&"Goodbye".to_string())
);
}
#[test]
fn test_parse_single_language_header_csv() {
let csv_content = "key,en\nhello,Hello\nbye,Goodbye\n";
let format = Format::from_reader(Cursor::new(csv_content)).unwrap();
assert_eq!(format.records.len(), 2);
assert_eq!(
format.records[0].get_translation("en"),
Some(&"Hello".to_string())
);
assert_eq!(
format.records[1].get_translation("en"),
Some(&"Goodbye".to_string())
);
let resources = Vec::<Resource>::try_from(format).unwrap();
assert_eq!(resources.len(), 1);
assert_eq!(resources[0].metadata.language, "en");
}
#[test]
fn test_multi_language_csv_to_resources() {
let csv_content = "key,en,cn\nhello,Hello,你好\nbye,Goodbye,再见\n";
let format = Format::from_reader(Cursor::new(csv_content)).unwrap();
let resources = Vec::<Resource>::try_from(format).unwrap();
assert_eq!(resources.len(), 2);
let en_resource = resources
.iter()
.find(|r| r.metadata.language == "en")
.unwrap();
assert_eq!(en_resource.entries.len(), 2);
assert_eq!(en_resource.entries[0].id, "hello");
assert_eq!(en_resource.entries[1].id, "bye");
let cn_resource = resources
.iter()
.find(|r| r.metadata.language == "cn")
.unwrap();
assert_eq!(cn_resource.entries.len(), 2);
assert_eq!(cn_resource.entries[0].id, "hello");
assert_eq!(cn_resource.entries[1].id, "bye");
}
#[test]
fn test_write_multi_language_csv() {
let mut record1 = MultiLanguageCSVRecord::new("hello".to_string());
record1.add_translation("en".to_string(), "Hello".to_string());
record1.add_translation("cn".to_string(), "你好".to_string());
let mut record2 = MultiLanguageCSVRecord::new("bye".to_string());
record2.add_translation("en".to_string(), "Goodbye".to_string());
record2.add_translation("cn".to_string(), "再见".to_string());
let records = vec![record1, record2];
let mut output = Vec::new();
Format::with_records(records)
.to_writer(&mut output)
.unwrap();
let output_str = String::from_utf8(output).unwrap();
let lines: Vec<&str> = output_str.lines().collect();
assert_eq!(lines.len(), 3);
assert!(lines[0].contains("key"));
assert!(lines[0].contains("cn"));
assert!(lines[0].contains("en"));
}
#[test]
fn test_multi_language_csv_record_methods() {
let mut record = MultiLanguageCSVRecord::new("test_key".to_string());
assert_eq!(record.key, "test_key");
assert_eq!(record.translations.len(), 0);
assert_eq!(record.get_translation("en"), None);
record.add_translation("en".to_string(), "Hello".to_string());
record.add_translation("cn".to_string(), "你好".to_string());
record.add_translation("es".to_string(), "Hola".to_string());
assert_eq!(record.get_translation("en"), Some(&"Hello".to_string()));
assert_eq!(record.get_translation("cn"), Some(&"你好".to_string()));
assert_eq!(record.get_translation("es"), Some(&"Hola".to_string()));
assert_eq!(record.get_translation("fr"), None);
record.add_translation("en".to_string(), "Updated Hello".to_string());
assert_eq!(
record.get_translation("en"),
Some(&"Updated Hello".to_string())
);
assert_eq!(record.translations.len(), 3);
}
#[test]
fn test_multi_language_csv_record_clone() {
let mut record1 = MultiLanguageCSVRecord::new("key1".to_string());
record1.add_translation("en".to_string(), "Hello".to_string());
record1.add_translation("cn".to_string(), "你好".to_string());
let record2 = record1.clone();
assert_eq!(record1.key, record2.key);
assert_eq!(record1.translations, record2.translations);
assert_eq!(record1.get_translation("en"), record2.get_translation("en"));
assert_eq!(record1.get_translation("cn"), record2.get_translation("cn"));
}
#[test]
fn test_multi_language_csv_record_debug() {
let mut record = MultiLanguageCSVRecord::new("test_key".to_string());
record.add_translation("en".to_string(), "Hello".to_string());
record.add_translation("cn".to_string(), "你好".to_string());
let debug_str = format!("{:?}", record);
assert!(debug_str.contains("MultiLanguageCSVRecord"));
assert!(debug_str.contains("test_key"));
assert!(debug_str.contains("Hello"));
assert!(debug_str.contains("你好"));
}
#[test]
fn test_multi_language_csv_record_partial_eq() {
let mut record1 = MultiLanguageCSVRecord::new("key1".to_string());
record1.add_translation("en".to_string(), "Hello".to_string());
record1.add_translation("cn".to_string(), "你好".to_string());
let mut record2 = MultiLanguageCSVRecord::new("key1".to_string());
record2.add_translation("en".to_string(), "Hello".to_string());
record2.add_translation("cn".to_string(), "你好".to_string());
let mut record3 = MultiLanguageCSVRecord::new("key2".to_string());
record3.add_translation("en".to_string(), "Hello".to_string());
assert_eq!(record1, record2);
assert_ne!(record1, record3);
assert_ne!(record2, record3);
}
#[test]
fn test_multi_language_csv_record_empty_translations() {
let record = MultiLanguageCSVRecord::new("empty_key".to_string());
assert_eq!(record.key, "empty_key");
assert_eq!(record.translations.len(), 0);
assert_eq!(record.get_translation("en"), None);
assert_eq!(record.get_translation("cn"), None);
}
#[test]
fn test_multi_language_csv_record_unicode_keys() {
let mut record = MultiLanguageCSVRecord::new("测试键".to_string());
record.add_translation("en".to_string(), "Test Key".to_string());
record.add_translation("cn".to_string(), "测试键".to_string());
assert_eq!(record.key, "测试键");
assert_eq!(record.get_translation("en"), Some(&"Test Key".to_string()));
assert_eq!(record.get_translation("cn"), Some(&"测试键".to_string()));
}
#[test]
fn test_csv_language_key_preservation() {
let csv_content =
"key,en,fr,de\nhello,Hello,Bonjour,Hallo\nbye,Goodbye,Au revoir,Auf Wiedersehen\n";
let format = Format::from_reader(Cursor::new(csv_content)).unwrap();
assert_eq!(format.records.len(), 2);
let first_record = &format.records[0];
assert_eq!(first_record.key, "hello");
assert_eq!(
first_record.get_translation("en"),
Some(&"Hello".to_string())
);
assert_eq!(
first_record.get_translation("fr"),
Some(&"Bonjour".to_string())
);
assert_eq!(
first_record.get_translation("de"),
Some(&"Hallo".to_string())
);
let second_record = &format.records[1];
assert_eq!(second_record.key, "bye");
assert_eq!(
second_record.get_translation("en"),
Some(&"Goodbye".to_string())
);
assert_eq!(
second_record.get_translation("fr"),
Some(&"Au revoir".to_string())
);
assert_eq!(
second_record.get_translation("de"),
Some(&"Auf Wiedersehen".to_string())
);
}
#[test]
fn test_csv_to_resources_language_preservation() {
let csv_content =
"key,en,fr,de\nhello,Hello,Bonjour,Hallo\nbye,Goodbye,Au revoir,Auf Wiedersehen\n";
let format = Format::from_reader(Cursor::new(csv_content)).unwrap();
let resources = Vec::<Resource>::try_from(format).unwrap();
assert_eq!(resources.len(), 3);
let en_resource = resources
.iter()
.find(|r| r.metadata.language == "en")
.unwrap();
assert_eq!(en_resource.entries.len(), 2);
assert_eq!(en_resource.entries[0].id, "hello");
assert_eq!(
en_resource.entries[0].value,
Translation::Singular("Hello".to_string())
);
assert_eq!(en_resource.entries[1].id, "bye");
assert_eq!(
en_resource.entries[1].value,
Translation::Singular("Goodbye".to_string())
);
let fr_resource = resources
.iter()
.find(|r| r.metadata.language == "fr")
.unwrap();
assert_eq!(fr_resource.entries.len(), 2);
assert_eq!(fr_resource.entries[0].id, "hello");
assert_eq!(
fr_resource.entries[0].value,
Translation::Singular("Bonjour".to_string())
);
assert_eq!(fr_resource.entries[1].id, "bye");
assert_eq!(
fr_resource.entries[1].value,
Translation::Singular("Au revoir".to_string())
);
let de_resource = resources
.iter()
.find(|r| r.metadata.language == "de")
.unwrap();
assert_eq!(de_resource.entries.len(), 2);
assert_eq!(de_resource.entries[0].id, "hello");
assert_eq!(
de_resource.entries[0].value,
Translation::Singular("Hallo".to_string())
);
assert_eq!(de_resource.entries[1].id, "bye");
assert_eq!(
de_resource.entries[1].value,
Translation::Singular("Auf Wiedersehen".to_string())
);
}
#[test]
fn test_csv_round_trip_language_preservation() {
let csv_content =
"key,en,fr,de\nhello,Hello,Bonjour,Hallo\nbye,Goodbye,Au revoir,Auf Wiedersehen\n";
let original_format = Format::from_reader(Cursor::new(csv_content)).unwrap();
let resources = Vec::<Resource>::try_from(original_format.clone()).unwrap();
let round_trip_format = Format::try_from(resources).unwrap();
assert_eq!(
original_format.records.len(),
round_trip_format.records.len()
);
let mut original_records = original_format.records.clone();
let mut round_trip_records = round_trip_format.records.clone();
original_records.sort_by(|a, b| a.key.cmp(&b.key));
round_trip_records.sort_by(|a, b| a.key.cmp(&b.key));
for (original, round_trip) in original_records.iter().zip(round_trip_records.iter()) {
assert_eq!(original.key, round_trip.key);
assert_eq!(original.translations, round_trip.translations);
}
}
#[test]
fn test_multi_language_csv_record_special_characters() {
let mut record = MultiLanguageCSVRecord::new("key_with_special_chars".to_string());
record.add_translation("en".to_string(), "Hello, World!".to_string());
record.add_translation("cn".to_string(), "你好,世界!".to_string());
record.add_translation("es".to_string(), "¡Hola, mundo!".to_string());
assert_eq!(
record.get_translation("en"),
Some(&"Hello, World!".to_string())
);
assert_eq!(
record.get_translation("cn"),
Some(&"你好,世界!".to_string())
);
assert_eq!(
record.get_translation("es"),
Some(&"¡Hola, mundo!".to_string())
);
}
#[test]
fn test_multi_language_csv_record_overwrite_translation() {
let mut record = MultiLanguageCSVRecord::new("overwrite_test".to_string());
record.add_translation("en".to_string(), "Original".to_string());
assert_eq!(record.get_translation("en"), Some(&"Original".to_string()));
record.add_translation("en".to_string(), "Updated".to_string());
assert_eq!(record.get_translation("en"), Some(&"Updated".to_string()));
assert_eq!(record.translations.len(), 1); }
#[test]
fn test_multi_language_csv_record_multiple_languages() {
let mut record = MultiLanguageCSVRecord::new("multilingual".to_string());
let languages = vec![
("en", "English"),
("cn", "中文"),
("es", "Español"),
("fr", "Français"),
("de", "Deutsch"),
("ja", "日本語"),
("ko", "한국어"),
("ru", "Русский"),
];
for (code, translation) in &languages {
record.add_translation(code.to_string(), translation.to_string());
}
assert_eq!(record.translations.len(), 8);
for (code, translation) in languages {
assert_eq!(record.get_translation(code), Some(&translation.to_string()));
}
}
}