diff --git a/Transcelerator/UNSQuestionsDialog.cs b/Transcelerator/UNSQuestionsDialog.cs index e37b2ea..337ad1b 100644 --- a/Transcelerator/UNSQuestionsDialog.cs +++ b/Transcelerator/UNSQuestionsDialog.cs @@ -2667,13 +2667,14 @@ public void LoadTranslations(IProgressMessage splashScreen) Exception e; ParsedQuestions parsedQuestions; + List temporaryAdditionalLookups = null; if (finfoParsedQuestions.Exists && - finfoMasterQuestions.LastWriteTimeUtc < finfoParsedQuestions.LastWriteTimeUtc && - finfoTxlDll.LastWriteTimeUtc < finfoParsedQuestions.LastWriteTimeUtc && - (!finfoKtRules.Exists || finfoKtRules.LastWriteTimeUtc < finfoParsedQuestions.LastWriteTimeUtc) && - (!finfoQuestionWords.Exists || finfoQuestionWords.LastWriteTimeUtc < finfoParsedQuestions.LastWriteTimeUtc) && - m_fileAccessor.ModifiedTime(DataFileAccessor.DataFileId.QuestionCustomizations).ToUniversalTime() < finfoParsedQuestions.LastWriteTimeUtc && - m_fileAccessor.ModifiedTime(DataFileAccessor.DataFileId.PhraseSubstitutions).ToUniversalTime() < finfoParsedQuestions.LastWriteTimeUtc) + finfoMasterQuestions.LastWriteTimeUtc < finfoParsedQuestions.LastWriteTimeUtc && + finfoTxlDll.LastWriteTimeUtc < finfoParsedQuestions.LastWriteTimeUtc && + (!finfoKtRules.Exists || finfoKtRules.LastWriteTimeUtc < finfoParsedQuestions.LastWriteTimeUtc) && + (!finfoQuestionWords.Exists || finfoQuestionWords.LastWriteTimeUtc < finfoParsedQuestions.LastWriteTimeUtc) && + m_fileAccessor.ModifiedTime(DataFileAccessor.DataFileId.QuestionCustomizations).ToUniversalTime() < finfoParsedQuestions.LastWriteTimeUtc && + m_fileAccessor.ModifiedTime(DataFileAccessor.DataFileId.PhraseSubstitutions).ToUniversalTime() < finfoParsedQuestions.LastWriteTimeUtc) { parsedQuestions = XmlSerializationHelper.DeserializeFromFile(m_parsedQuestionsFilename); } @@ -2701,6 +2702,7 @@ public void LoadTranslations(IProgressMessage splashScreen) m_parser = new MasterQuestionParser(m_masterQuestionsFilename, GetQuestionWords(), m_getKeyTerms(), GetKeyTermRules(keyTermRulesFilename), customizations, PhraseSubstitutions); parsedQuestions = m_parser.Result; + temporaryAdditionalLookups = m_parser.TemporaryAdditionalLookups?.ToList(); Directory.CreateDirectory(Path.GetDirectoryName(m_parsedQuestionsFilename)); XmlSerializationHelper.SerializeToFile(m_parsedQuestionsFilename, parsedQuestions); } @@ -2748,6 +2750,29 @@ public void LoadTranslations(IProgressMessage splashScreen) TranslatablePhrase phrase = m_helper.GetPhrase(unsTranslation.Reference, unsTranslation.PhraseKey); if (phrase != null && (!phrase.IsExcluded || phrase.IsUserAdded)) phrase.Translation = unsTranslation.Translation; + else if (temporaryAdditionalLookups != null) + { + // This is the unusual situation where questions that were previously + // modified (and presumably translated) by a user have subsequently + // been added to the official collection of questions or alternatives. + // Because of this, we lose the association and can't hook up the + // user's translation to the question directly. The parser now keeps + // track of those lost associations so we can try to avoid losing the + // translations. + var iLookup = temporaryAdditionalLookups + .IndexOf(m => m.Reference == unsTranslation.Reference && + m.OriginalPhrase == unsTranslation.PhraseKey); + if (iLookup >= 0) + { + phrase = m_helper.GetPhrase(unsTranslation.Reference, + temporaryAdditionalLookups[iLookup].ModifiedPhrase); + if (phrase != null && !phrase.HasUserTranslation) + phrase.Translation = unsTranslation.Translation; + temporaryAdditionalLookups.RemoveAt(iLookup); + if (!temporaryAdditionalLookups.Any()) + temporaryAdditionalLookups = null; + } + } } } } diff --git a/TxlData/IModifiedPhrase.cs b/TxlData/IModifiedPhrase.cs new file mode 100644 index 0000000..0fd1bb7 --- /dev/null +++ b/TxlData/IModifiedPhrase.cs @@ -0,0 +1,36 @@ +// --------------------------------------------------------------------------------------------- +#region // Copyright (c) 2024, SIL International. +// +// Copyright (c) 2024, SIL International. +// +// Distributable under the terms of the MIT License (http://sil.mit-license.org/) +// +#endregion +// --------------------------------------------------------------------------------------------- +namespace SIL.Transcelerator +{ + /// + /// Represents a phrase/question that the user modified (even if that modification was + /// later superseded by a subsequent program change that added the modified version). + /// + /// Technically, we wouldn't need this interface since the only class that + /// implements it is in this same assembly, but it wouldn't have to be implemented like + /// that, and using this interface clarifies the properties we actually care about. + public interface IModifiedPhrase + { + string Reference { get; } + /// -------------------------------------------------------------------------------- + /// + /// Gets the original phrase. + /// + /// -------------------------------------------------------------------------------- + string OriginalPhrase { get; } + + /// -------------------------------------------------------------------------------- + /// + /// Gets the edited/customized phrase. + /// + /// -------------------------------------------------------------------------------- + string ModifiedPhrase { get; } + } +} diff --git a/TxlData/IPhraseTranslationHelper.cs b/TxlData/IPhraseTranslationHelper.cs index 7ff2f95..e19a690 100644 --- a/TxlData/IPhraseTranslationHelper.cs +++ b/TxlData/IPhraseTranslationHelper.cs @@ -9,7 +9,6 @@ // // File: IPhraseTranslationHelper.cs // --------------------------------------------------------------------------------------------- -using SIL.Transcelerator.Localization; using System.Collections.Generic; using System.Globalization; diff --git a/TxlData/MasterQuestionParser.cs b/TxlData/MasterQuestionParser.cs index bb7152d..62eb479 100644 --- a/TxlData/MasterQuestionParser.cs +++ b/TxlData/MasterQuestionParser.cs @@ -1,4 +1,4 @@ -// --------------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------------------------- #region // Copyright (c) 2023, SIL International. // // Copyright (c) 2023, SIL International. @@ -35,6 +35,11 @@ public class MasterQuestionParser private class Customizations // All customizations that share a key (used to match to a question) { + #region Events and Delegates + internal delegate void ModifiedPhraseEventHandler(Customizations sender, IModifiedPhrase phrase); + internal static event ModifiedPhraseEventHandler ModifiedPhraseFoundInExistingQuestions; + #endregion + private bool m_isResolved = true; private List AdditionsAndInsertions { get; } private List Deletions { get; } @@ -92,6 +97,7 @@ private void SetExcludedAndModified(Question question, IReadOnlyCollection a.IsKey)?.Text))) { question.IsExcluded = true; + ModifiedPhraseFoundInExistingQuestions?.Invoke(this, Modification); } else question.ModifiedPhrase = ModifiedPhrase; @@ -343,8 +349,9 @@ public Question PopQuestion(IQuestionKey keyToUseForReference) private readonly IDictionary>> m_questionWordsLookupTable; private readonly IEnumerable m_questionWords; + private List m_temporaryAdditionalLookups; - #endregion + #endregion #region SubPhraseMatch class private class SubPhraseMatch @@ -400,7 +407,7 @@ public MasterQuestionParser(QuestionSections sections, IEnumerable customizations, IEnumerable phraseSubstitutions) { - m_sections = sections; + m_sections = sections; m_questionWords = questionWords; if (questionWords != null) { @@ -416,6 +423,8 @@ public MasterQuestionParser(QuestionSections sections, } if (customizations != null) { + Customizations.ModifiedPhraseFoundInExistingQuestions += Customizations_ModifiedPhraseFoundInExistingQuestions; + m_customizations = new Dictionary>(); foreach (var customization in customizations) { @@ -447,12 +456,19 @@ public MasterQuestionParser(QuestionSections sections, m_partsTable = new SortedDictionary>>(); } - /// ------------------------------------------------------------------------------------ - /// - /// Performs the parsing logic to divide question text into translatable parts and key term parts. - /// - /// ------------------------------------------------------------------------------------ - private void Parse() + private void Customizations_ModifiedPhraseFoundInExistingQuestions(Customizations sender, IModifiedPhrase phrase) + { + if (m_temporaryAdditionalLookups == null) + m_temporaryAdditionalLookups = new List(); + m_temporaryAdditionalLookups.Add(phrase); + } + + /// ------------------------------------------------------------------------------------ + /// + /// Performs the parsing logic to divide question text into translatable parts and key term parts. + /// + /// ------------------------------------------------------------------------------------ + private void Parse() { if (m_partsTable.Any()) throw new InvalidOperationException("Parse called more than once."); @@ -568,7 +584,10 @@ public ParsedQuestions Result return result; } } - #endregion + + public IEnumerable TemporaryAdditionalLookups => m_temporaryAdditionalLookups; + + #endregion #region Private helper methods /// ------------------------------------------------------------------------------------ @@ -582,7 +601,7 @@ private static IEnumerable GetCustomizations(Question q, SortedDictionary customizations, bool processAllAdditionsForRef = false) { - if (TryPopCustomizationForQuestion(customizations, q, sectionRange, out var customizationsForQuestion)) + if (TryPopCustomizationForQuestion(customizations, q, sectionRange, out var customizationsForQuestion)) { customizationsForQuestion.ApplyToQuestion(q, category.Questions); if (q.InsertedQuestionBefore != null && !category.Questions.Any(existing => !existing.IsExcluded && existing.Matches(q.InsertedQuestionBefore))) diff --git a/TxlData/PhraseCustomization.cs b/TxlData/PhraseCustomization.cs index 6b2b739..90f7922 100644 --- a/TxlData/PhraseCustomization.cs +++ b/TxlData/PhraseCustomization.cs @@ -1,7 +1,7 @@ // --------------------------------------------------------------------------------------------- -#region // Copyright (c) 2023, SIL International. -// -// Copyright (c) 2023, SIL International. +#region // Copyright (c) 2024, SIL International. +// +// Copyright (c) 2024, SIL International. // // Distributable under the terms of the MIT License (http://sil.mit-license.org/) // @@ -24,7 +24,7 @@ namespace SIL.Transcelerator /// /// ------------------------------------------------------------------------------------ [XmlType("PhraseCustomization")] - public class PhraseCustomization + public class PhraseCustomization : IModifiedPhrase { private BCVRef m_scrStartReference; private BCVRef m_scrEndReference; diff --git a/TxlData/TxlDataTests/MasterQuestionParserTests.cs b/TxlData/TxlDataTests/MasterQuestionParserTests.cs index 4d2f2df..a8194da 100644 --- a/TxlData/TxlDataTests/MasterQuestionParserTests.cs +++ b/TxlData/TxlDataTests/MasterQuestionParserTests.cs @@ -1987,6 +1987,7 @@ public void GetResult_ModifiedPhrases_ResultIncludesModifications() } Assert.IsNull(pq.KeyTerms); Assert.AreEqual(5, pq.TranslatableParts.Length); + Assert.That(qp.TemporaryAdditionalLookups, Is.Null); } ///-------------------------------------------------------------------------------------- @@ -4602,6 +4603,11 @@ public void GetResult_ModifiedQuestionNowInInMasterList_OriginalExcluded(string Assert.AreEqual(32003010, actQuestion.EndRef); Assert.AreEqual(iQuestion, actCategory.Questions.Count); + + var lookup = qp.TemporaryAdditionalLookups.Single(); + Assert.That(lookup.Reference, Is.EqualTo(pc.Reference)); + Assert.That(lookup.OriginalPhrase, Is.EqualTo(pc.OriginalPhrase)); + Assert.That(lookup.ModifiedPhrase, Is.EqualTo(pc.ModifiedPhrase)); } private List GetJonahC1V3Customizations(PhraseCustomization.CustomizationType firstAdditionType) @@ -4819,6 +4825,8 @@ public void GetResult_CompoundAdditionAndInsertionOfPhrases_PhrasesAreInCorrectO Assert.IsNull(pq.KeyTerms); Assert.AreEqual(11, pq.TranslatableParts.Length); + + Assert.That(qp.TemporaryAdditionalLookups, Is.Null); } ///-------------------------------------------------------------------------------------- @@ -4891,6 +4899,7 @@ public void GetResult_PhraseAddedAfterInsertionBefore_PhrasesAreInCorrectOrder() Assert.IsNull(pq.KeyTerms); Assert.AreEqual(3, pq.TranslatableParts.Length); + Assert.That(qp.TemporaryAdditionalLookups, Is.Null); } ///--------------------------------------------------------------------------------------