From 92c5ac768b4d519de3a949c4ac29bfb25ed99e6c Mon Sep 17 00:00:00 2001 From: Jeff Baskin Date: Mon, 16 Mar 2026 13:30:55 -0400 Subject: [PATCH] Got universal strings to notify for missing translations. --- src/document.rs | 2 +- src/document/field.rs | 152 +++++++++++++++++++++++++++++++++++++++--- src/lib.rs | 3 +- src/mtterror.rs | 2 + 4 files changed, 147 insertions(+), 12 deletions(-) diff --git a/src/document.rs b/src/document.rs index baec32e..5a5c70d 100644 --- a/src/document.rs +++ b/src/document.rs @@ -10,6 +10,6 @@ use record::{InternalRecord, InternalRecords, Oid}; pub use clock::Clock; pub use create::{CreateDoc, IndexType}; pub use definition::{DocDef, DocFuncType}; -pub use field::{Field, FieldType}; +pub use field::{Field, FieldType, MissingTranslation}; pub use record::{Record, Records}; pub use session::Session; diff --git a/src/document/field.rs b/src/document/field.rs index dd8d0a9..77f0203 100644 --- a/src/document/field.rs +++ b/src/document/field.rs @@ -1,3 +1,4 @@ +use crate::{ErrorID, MTTError}; use chrono::prelude::*; use isolang::Language; use std::{ @@ -577,10 +578,96 @@ mod paragraphs { } } +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +struct ParagraphID { + data: Uuid, +} + +impl ParagraphID { + fn new() -> Self { + Self { + data: Uuid::new_v4(), + } + } + + fn nil() -> Self { + Self { data: Uuid::nil() } + } +} + +#[cfg(test)] +mod paragraphids { + use super::*; + + #[test] + fn are_paragraph_ids_unique() { + let id1 = ParagraphID::new(); + let id2 = ParagraphID::new(); + assert_ne!(id1, id2); + } + + #[test] + fn can_create_nil() { + let id = ParagraphID::nil(); + assert_eq!(id.data, Uuid::nil()); + } +} + +#[derive(Clone, Debug)] +pub struct MissingTranslation { + needs: Language, + has: Language, + text: String, +} + +impl MissingTranslation { + fn new(needs: Language, has: Language, text: String) -> Self { + Self { + needs: needs, + has: has, + text: text, + } + } + + fn needs(&self) -> Language { + self.needs.clone() + } + + fn has(&self) -> Language { + self.has.clone() + } + + fn text(&self) -> String { + self.text.clone() + } +} + +#[cfg(test)] +mod missing_translations { + use super::*; + + #[test] + fn can_get_mising_translation_information() { + let langs = [ + Language::from_639_1("en").unwrap(), + Language::from_639_1("ja").unwrap(), + ]; + let text = Uuid::new_v4().to_string(); + let missing = MissingTranslation::new(langs[0].clone(), langs[1].clone(), text.clone()); + assert_eq!(missing.needs(), langs[0]); + assert_eq!(missing.has(), langs[1]); + assert_eq!(missing.text(), text); + let missing2 = MissingTranslation::new(langs[1].clone(), langs[0].clone(), text.clone()); + assert_eq!(missing2.needs(), langs[1]); + assert_eq!(missing2.has(), langs[0]); + assert_eq!(missing2.text(), text); + } +} + #[derive(Clone, Debug)] struct UniversalString { - paragraphs: HashMap, - revisions: Vec>, + paragraphs: HashMap, + revisions: Vec>, } impl UniversalString { @@ -593,20 +680,36 @@ impl UniversalString { output } - fn get(&self, lang: &Language) -> Option { + fn get(&self, lang: &Language) -> Result { let latest = self.revisions.len() - 1; self.get_revision(latest, lang) } - fn get_revision(&self, rev_num: usize, lang: &Language) -> Option { + fn get_revision(&self, rev_num: usize, lang: &Language) -> Result { let mut output = "".to_string(); + let mut missing: Vec = Vec::new(); for id in self.revisions[rev_num].iter() { let paragraph = self.paragraphs.get(id).unwrap(); - let text = paragraph.get(lang).unwrap(); + let text = match paragraph.get(lang) { + Some(data) => data, + None => { + let (ori_lang, text) = paragraph.get_initial(); + missing.push(MissingTranslation::new( + lang.clone(), + ori_lang.clone(), + text.clone(), + )); + "" + } + }; output += text; output += "\u{2029}"; } - Some(output) + if missing.is_empty() { + Ok(output) + } else { + Err(MTTError::new(ErrorID::MissingTranslation(missing))) + } } fn revision_count(&self) -> usize { @@ -617,17 +720,17 @@ impl UniversalString { let mut version = Vec::new(); for paragraph in text.as_str().split("\u{2029}") { if paragraph != "" { - let mut id = Uuid::nil(); + let mut id = ParagraphID::nil(); for (key, value) in self.paragraphs.iter() { if ¶graph == value.get(&lang).unwrap() { id = key.clone(); break; } } - if id == Uuid::nil() { - id = Uuid::new_v4(); + if id == ParagraphID::nil() { + id = ParagraphID::new(); while self.paragraphs.contains_key(&id) { - id = Uuid::new_v4(); + id = ParagraphID::new(); } self.paragraphs.insert( id.clone(), @@ -645,6 +748,7 @@ impl UniversalString { mod universal_strings { use super::*; use rand::random_range; + use std::collections::HashSet; const ENGLISH_DATA: [&str; 5] = ["one", "two", "three", "four", "five"]; const JAPANESE_DATA: [&str; 5] = ["一", "二", "三", "四", "五"]; @@ -764,4 +868,32 @@ mod universal_strings { assert_eq!(ustr.paragraphs.len(), expected_paragraphs, "{:?}", ustr); assert_eq!(ustr.get_revision(0, &lang).unwrap(), initial); } + + #[test] + fn can_translation_be_added() { + let (elang, edata) = TestData::english(); + let (jlang, jdata) = TestData::japanese(); + let initial = TestData::to_input(jdata.clone()); + let mut ustr = UniversalString::new(jlang.clone(), initial.clone()); + assert_eq!(ustr.get(&jlang).unwrap(), initial); + let err = ustr.get(&elang).unwrap_err(); + match err.get_error_ids().iter().last().unwrap() { + ErrorID::MissingTranslation(missing) => { + assert_eq!( + missing.len(), + jdata.len(), + "should return list of translations needed" + ); + let mut holder: HashSet = jdata.iter().cloned().collect(); + for data in missing.iter() { + assert_eq!(data.needs(), elang, "needed language is incorrect"); + assert_eq!(data.has(), jlang, "original language is incorrect"); + assert!(holder.contains(&data.text())); + holder.remove(&data.text()); + } + assert!(holder.is_empty(), "still had {:?}", holder); + } + _ => unreachable!("got {:?}, should have been needs translation", err), + } + } } diff --git a/src/lib.rs b/src/lib.rs index 0a1b3a7..a6263c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,6 @@ mod mtterror; pub mod name; mod queue; -pub use action::*; use document::{Clock, CreateDoc, Session}; use message::{wrapper::Message, MessageAction}; use queue::{ @@ -18,6 +17,8 @@ use std::{ }; use uuid::Uuid; +pub use action::*; +pub use document::MissingTranslation; pub use mtterror::{ErrorID, MTTError}; pub use name::{Name, NameType}; pub use queue::data_director::{Include, Path}; diff --git a/src/mtterror.rs b/src/mtterror.rs index 19443b1..01a9402 100644 --- a/src/mtterror.rs +++ b/src/mtterror.rs @@ -2,6 +2,7 @@ use crate::{ action::{Field, FieldType}, message::MessageAction, name::{Name, NameType}, + MissingTranslation, }; use isolang::Language; use std::{collections::VecDeque, error::Error, fmt}; @@ -21,6 +22,7 @@ pub enum ErrorID { FieldTypeExpected(FieldType), IndexEntryAlreadyExists(Field), InvalidFieldName(Name), + MissingTranslation(Vec), NameAlreadyExists, NameLanguageNotUnique, NameNotFound(NameType),