From 558172f206cc87cd709df41f4afd472a3a517057 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Mon, 8 Jun 2026 13:25:29 -0400 Subject: [PATCH 1/8] Fix NRE in XLiffBody.AddTransUnit when tu.Source is null (#149) --- src/L10NSharp/XLiffUtils/XLiffBody.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/L10NSharp/XLiffUtils/XLiffBody.cs b/src/L10NSharp/XLiffUtils/XLiffBody.cs index 695b1f3..515dc2c 100644 --- a/src/L10NSharp/XLiffUtils/XLiffBody.cs +++ b/src/L10NSharp/XLiffUtils/XLiffBody.cs @@ -176,7 +176,7 @@ public bool AddTransUnit(XLiffTransUnit tu) // the source value there. if (tu.Target != null && tu.Target.Value != null) TranslationsById[tu.Id] = tu.Target.Value; - else + else if (tu.Source != null) TranslationsById[tu.Id] = tu.Source.Value; return true; } @@ -246,7 +246,7 @@ internal int NumberTranslated { ++_translatedCount; } - else if (tu.Target.Value != tu.Source.Value && + else if (tu.Source != null && tu.Target.Value != tu.Source.Value && tu.Target.TargetState == XLiffTransUnitVariant.TranslationState.Undefined) { ++_translatedCount; From 227cfd0d34c2566d90a129256aa5aaf602af079b Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Mon, 8 Jun 2026 13:31:17 -0400 Subject: [PATCH 2/8] Add test and changelog for NRE fix in XLiffBody.AddTransUnit (#149) --- CHANGELOG.md | 1 + src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 895773a..3c5cc92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - [L10NSharp.Windows.Forms] Restored project-local Resources support for `FallbackLanguagesDlgBase` button images (`Move`, `Move_up`, and `Move_down`). - [L10NSharp.Windows.Forms] Corrected resource manager base name to `L10NSharp.Windows.Forms.Properties.Resources`. - [L10NSharp.Windows.Forms.Tests] Corrected resource manager base name to `L10NSharp.Windows.Forms.Tests.Properties.Resources`. +- [L10NSharp] Fixed `NullReferenceException` in `XLiffBody.AddTransUnit` when a trans-unit has no source variant (e.g. from a malformed XLIFF file). (#149) ### Removed diff --git a/src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs b/src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs index 72a02f0..c8264a0 100644 --- a/src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs +++ b/src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs @@ -239,6 +239,16 @@ public void MergeXliffDocuments_WorksAsExpected() }, true); } + [Test] + public void AddTransUnit_NullSourceNullTarget_DoesNotThrow() + { + var body = new XLiffBody(); + // Target is null (default), so the else branch in AddTransUnit fires. + // Before the fix: tu.Source.Value throws NRE when Source is also null. + var tu = new XLiffTransUnit { Id = "some-id", Source = null }; + Assert.DoesNotThrow(() => body.AddTransUnit(tu)); + } + private void CheckMergedTransUnit(XLiffTransUnit tu, string sourceText, string[] notes, bool isDynamic) { Assert.IsNotNull(tu); From 1f9ff828093a3912fbbdca7c1aee5d02f523e4f2 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Mon, 8 Jun 2026 13:42:27 -0400 Subject: [PATCH 3/8] Simplify AddTransUnit_NullSourceNullTarget_DoesNotThrow test --- src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs b/src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs index c8264a0..8b73894 100644 --- a/src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs +++ b/src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs @@ -243,9 +243,7 @@ public void MergeXliffDocuments_WorksAsExpected() public void AddTransUnit_NullSourceNullTarget_DoesNotThrow() { var body = new XLiffBody(); - // Target is null (default), so the else branch in AddTransUnit fires. - // Before the fix: tu.Source.Value throws NRE when Source is also null. - var tu = new XLiffTransUnit { Id = "some-id", Source = null }; + var tu = new XLiffTransUnit { Id = "some-id", Source = null, Target = null }; Assert.DoesNotThrow(() => body.AddTransUnit(tu)); } From 902393ac83f8cef926e0f66ba1ca300cf480e64e Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 26 Jun 2026 10:40:25 -0400 Subject: [PATCH 4/8] Fix merge/changelog cruft --- CHANGELOG.md | 14 +++++++------- .../XLiffLocalizationManagerTests.cs | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f0a1c5..a4d0f4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,13 +38,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - [L10NSharp.Windows.Forms] Restored project-local Resources support for `FallbackLanguagesDlgBase` button images (`Move`, `Move_up`, and `Move_down`). - [L10NSharp.Windows.Forms] Corrected resource manager base name to `L10NSharp.Windows.Forms.Properties.Resources`. - [L10NSharp.Windows.Forms.Tests] Corrected resource manager base name to `L10NSharp.Windows.Forms.Tests.Properties.Resources`. -- [L10NSharp] Fixed `NullReferenceException` in `XLiffBody.AddTransUnit` when a trans-unit has no source variant (e.g. from a malformed XLIFF file). (#149) -- [L10NSharp] Fixed file handle leak in `XliffLocalizationManager.CreateOrUpdateDefaultXliffFileIfNecessary` when an exception was thrown between `File.Open` and `Close`. (#151) -- [L10NSharp] Fixed race condition in `XLiffBody.AddTransUnitRaw` where two concurrent threads with the same translation-unit ID could both bypass the duplicate check and silently overwrite each other's entry. (#150) -- [L10NSharp] Eliminated unnecessary cross-instance lock contention in `XLiffBody` by making `_transUnitIdLock` instance-level instead of static. (#150) -- [L10NSharp] Fixed `FilenamesToAddToCache` yielding both the custom and installed XLIFF for the same language when `UseLanguageCodeFolders` is `true`, causing custom translations to be silently overwritten by installed ones. (#140) -- [L10NSharp] Fixed `ExtractXliff` accumulating duplicate "Not found in static scan" notes on successive runs; the note is now replaced rather than appended, and removed when the string is subsequently found. (#113) -- [L10NSharp] Fixed `LocalizationManager.GetString` silently falling back to English when called with a one-shot `IEnumerable` for `preferredLanguageIds`; the sequence is now materialized before use. (#139) +- [L10NSharp] Fixed file handle leak in `XliffLocalizationManager.CreateOrUpdateDefaultXliffFileIfNecessary` when an exception was thrown between `File.Open` and `Close`. +- [L10NSharp] Fixed race condition in `XLiffBody.AddTransUnitRaw` where two concurrent threads with the same translation-unit ID could both bypass the duplicate check and silently overwrite each other's entry. +- [L10NSharp] Eliminated unnecessary cross-instance lock contention in `XLiffBody` by making `_transUnitIdLock` instance-level instead of static. +- [L10NSharp] Fixed `FilenamesToAddToCache` yielding both the custom and installed XLIFF for the same language when `UseLanguageCodeFolders` is `true`, causing custom translations to be silently overwritten by installed ones. +- [L10NSharp] Fixed `ExtractXliff` accumulating duplicate "Not found in static scan" notes on successive runs; the note is now replaced rather than appended, and removed when the string is subsequently found. +- [L10NSharp] Fixed `LocalizationManager.GetString` silently falling back to English when called with a one-shot `IEnumerable` for `preferredLanguageIds`; the sequence is now materialized before use. +- [L10NSharp] Fixed `NullReferenceException` in `XLiffBody.AddTransUnit` when a trans-unit has no source variant (e.g. from a malformed XLIFF file). ### Removed diff --git a/src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs b/src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs index 3e6a8c3..7a36950 100644 --- a/src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs +++ b/src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs @@ -278,6 +278,7 @@ public void AddTransUnit_NullSourceNullTarget_DoesNotThrow() var body = new XLiffBody(); var tu = new XLiffTransUnit { Id = "some-id", Source = null, Target = null }; Assert.DoesNotThrow(() => body.AddTransUnit(tu)); + } private void CheckMergedTransUnit(XLiffTransUnit tu, string sourceText, string[] notes, bool isDynamic) { From bc61e81648bcc41c5c58e200da87ad1c2748cd24 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 26 Jun 2026 10:52:47 -0400 Subject: [PATCH 5/8] Guard against null Source in MergeXliffDocuments (#149) A malformed baseline XLIFF can contain a trans-unit with no source variant, causing a NullReferenceException when MergeXliffDocuments compares it against the current scan. Use null-conditional access so a null old source is simply treated as differing from the new source. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 2 +- .../XLiffLocalizationManagerTests.cs | 24 ++++++++++++++++++- .../XLiffUtils/XliffLocalizationManager.cs | 4 ++-- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4d0f4c..e95f5ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,7 +44,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - [L10NSharp] Fixed `FilenamesToAddToCache` yielding both the custom and installed XLIFF for the same language when `UseLanguageCodeFolders` is `true`, causing custom translations to be silently overwritten by installed ones. - [L10NSharp] Fixed `ExtractXliff` accumulating duplicate "Not found in static scan" notes on successive runs; the note is now replaced rather than appended, and removed when the string is subsequently found. - [L10NSharp] Fixed `LocalizationManager.GetString` silently falling back to English when called with a one-shot `IEnumerable` for `preferredLanguageIds`; the sequence is now materialized before use. -- [L10NSharp] Fixed `NullReferenceException` in `XLiffBody.AddTransUnit` when a trans-unit has no source variant (e.g. from a malformed XLIFF file). +- [L10NSharp] Fixed `NullReferenceException` in `XLiffBody.AddTransUnit` and `XliffLocalizationManager.MergeXliffDocuments` when a trans-unit has no source variant (e.g., from a malformed XLIFF file). ### Removed diff --git a/src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs b/src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs index 7a36950..9442aaa 100644 --- a/src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs +++ b/src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs @@ -280,7 +280,29 @@ public void AddTransUnit_NullSourceNullTarget_DoesNotThrow() Assert.DoesNotThrow(() => body.AddTransUnit(tu)); } - private void CheckMergedTransUnit(XLiffTransUnit tu, string sourceText, string[] notes, bool isDynamic) + [Test] + public void MergeXliffDocuments_BaselineUnitHasNullSource_DoesNotThrow() + { + var newDoc = new XLiffDocument(); + newDoc.AddTransUnit(new XLiffTransUnit { + Id = "Some.id", + Source = new XLiffTransUnitVariant { Lang = "en", Value = "Current text." } + }); + + var oldDoc = new XLiffDocument(); + oldDoc.AddTransUnit(new XLiffTransUnit { Id = "Some.id", Source = null }); + + XLiffDocument mergedDoc = null; + Assert.DoesNotThrow(() => + mergedDoc = XliffLocalizationManager.MergeXliffDocuments(newDoc, oldDoc, true)); + + var tu = mergedDoc.GetTransUnitForId("Some.id"); + Assert.IsNotNull(tu); + Assert.That(tu.Notes.Any(n => n.Text.StartsWith("OLD TEXT")), Is.True); + } + + private static void CheckMergedTransUnit( + XLiffTransUnit tu, string sourceText, string[] notes, bool isDynamic) { Assert.IsNotNull(tu); Assert.That("en", Is.EqualTo(tu.Source.Lang)); diff --git a/src/L10NSharp/XLiffUtils/XliffLocalizationManager.cs b/src/L10NSharp/XLiffUtils/XliffLocalizationManager.cs index 59ad35d..4110369 100644 --- a/src/L10NSharp/XLiffUtils/XliffLocalizationManager.cs +++ b/src/L10NSharp/XLiffUtils/XliffLocalizationManager.cs @@ -630,11 +630,11 @@ internal static XLiffDocument MergeXliffDocuments(XLiffDocument xliffNew, XLiffD tu.AddNote(note.NoteLang, "[OLD NOTE] " + note.Text); } } - if (tu.Source.Value != tuOld.Source.Value) + if (tu.Source?.Value != tuOld.Source?.Value) { ++changedStringCount; changedStringIds.Add(tu.Id); - tu.AddNote("en", $"OLD TEXT (before {xliffNew.File.ProductVersion}): {tuOld.Source.Value}"); + tu.AddNote("en", $"OLD TEXT (before {xliffNew.File.ProductVersion}): {tuOld.Source?.Value}"); } if (tuOld.Dynamic && !tu.Dynamic) { From 2e3be29fa67c9f70115041549a7e0fd9e0846711 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 26 Jun 2026 11:10:19 -0400 Subject: [PATCH 6/8] Suppress empty OLD TEXT note for blank baseline source (#149) When the baseline trans-unit's source is null/whitespace, the unit is still counted as changed, but skip the OLD TEXT note since it would carry no value. Uses IsNullOrWhiteSpace to match other XLiffUtils source-value checks. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs | 3 ++- src/L10NSharp/XLiffUtils/XliffLocalizationManager.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs b/src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs index 9442aaa..f439045 100644 --- a/src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs +++ b/src/L10NSharp.Tests/XLiffLocalizationManagerTests.cs @@ -296,9 +296,10 @@ public void MergeXliffDocuments_BaselineUnitHasNullSource_DoesNotThrow() Assert.DoesNotThrow(() => mergedDoc = XliffLocalizationManager.MergeXliffDocuments(newDoc, oldDoc, true)); + // A null old source has no value to report, so no "OLD TEXT" note is added. var tu = mergedDoc.GetTransUnitForId("Some.id"); Assert.IsNotNull(tu); - Assert.That(tu.Notes.Any(n => n.Text.StartsWith("OLD TEXT")), Is.True); + Assert.That(tu.Notes.Any(n => n.Text.StartsWith("OLD TEXT")), Is.False); } private static void CheckMergedTransUnit( diff --git a/src/L10NSharp/XLiffUtils/XliffLocalizationManager.cs b/src/L10NSharp/XLiffUtils/XliffLocalizationManager.cs index 4110369..7f3e6c8 100644 --- a/src/L10NSharp/XLiffUtils/XliffLocalizationManager.cs +++ b/src/L10NSharp/XLiffUtils/XliffLocalizationManager.cs @@ -634,7 +634,8 @@ internal static XLiffDocument MergeXliffDocuments(XLiffDocument xliffNew, XLiffD { ++changedStringCount; changedStringIds.Add(tu.Id); - tu.AddNote("en", $"OLD TEXT (before {xliffNew.File.ProductVersion}): {tuOld.Source?.Value}"); + if (!string.IsNullOrWhiteSpace(tuOld.Source?.Value)) + tu.AddNote("en", $"OLD TEXT (before {xliffNew.File.ProductVersion}): {tuOld.Source.Value}"); } if (tuOld.Dynamic && !tu.Dynamic) { From 1e0f48e6214d941aa866c07de50d1ed403db7152 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 26 Jun 2026 11:28:08 -0400 Subject: [PATCH 7/8] Simplify null checks in XLiffBody with null-conditional operators (#149) Collapse the Target/Source null guards in AddTransUnit, NumberTranslated, and NumberApproved to use ?. access. In the NumberTranslated 'differs from source' heuristic, also require a non-blank source (IsNullOrWhiteSpace) so a blank source no longer trivially counts a unit as translated. AddTransUnit keeps != null since an empty/whitespace value is still a legitimate stored translation. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/L10NSharp/XLiffUtils/XLiffBody.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/L10NSharp/XLiffUtils/XLiffBody.cs b/src/L10NSharp/XLiffUtils/XLiffBody.cs index c32f11f..9391a0b 100644 --- a/src/L10NSharp/XLiffUtils/XLiffBody.cs +++ b/src/L10NSharp/XLiffUtils/XLiffBody.cs @@ -164,12 +164,12 @@ public bool AddTransUnit(XLiffTransUnit tu) { if (!AddTransUnitRaw(tu)) return false; - + // If the target exists, store its value in the dictionary lookup. Otherwise, store // the source value there. - if (tu.Target != null && tu.Target.Value != null) + if (tu.Target?.Value != null) TranslationsById[tu.Id] = tu.Target.Value; - else if (tu.Source != null) + else if (tu.Source?.Value != null) TranslationsById[tu.Id] = tu.Source.Value; return true; } @@ -234,14 +234,15 @@ internal int NumberTranslated _translatedCount = 0; foreach (var tu in TransUnitsUnordered) { - if (tu.Target == null || string.IsNullOrWhiteSpace(tu.Target.Value)) + if (string.IsNullOrWhiteSpace(tu.Target?.Value)) continue; if (tu.TranslationStatus == TranslationStatus.Approved || tu.Target.TargetState == XLiffTransUnitVariant.TranslationState.Translated) { ++_translatedCount; } - else if (tu.Source != null && tu.Target.Value != tu.Source.Value && + else if (!string.IsNullOrWhiteSpace(tu.Source?.Value) && + tu.Target.Value != tu.Source.Value && tu.Target.TargetState == XLiffTransUnitVariant.TranslationState.Undefined) { ++_translatedCount; @@ -271,7 +272,7 @@ internal int NumberApproved _approvedCount = 0; foreach (var tu in TransUnitsUnordered) { - if (tu.Target == null || string.IsNullOrWhiteSpace(tu.Target.Value)) + if (string.IsNullOrWhiteSpace(tu.Target?.Value)) continue; if (tu.TranslationStatus == TranslationStatus.Approved) ++_approvedCount; From ad0824e113d4d225c60f78184a94851c959fac9b Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 26 Jun 2026 11:30:36 -0400 Subject: [PATCH 8/8] Include NumberTranslated in NRE changelog entry (#149) Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e95f5ca..d823475 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,7 +44,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - [L10NSharp] Fixed `FilenamesToAddToCache` yielding both the custom and installed XLIFF for the same language when `UseLanguageCodeFolders` is `true`, causing custom translations to be silently overwritten by installed ones. - [L10NSharp] Fixed `ExtractXliff` accumulating duplicate "Not found in static scan" notes on successive runs; the note is now replaced rather than appended, and removed when the string is subsequently found. - [L10NSharp] Fixed `LocalizationManager.GetString` silently falling back to English when called with a one-shot `IEnumerable` for `preferredLanguageIds`; the sequence is now materialized before use. -- [L10NSharp] Fixed `NullReferenceException` in `XLiffBody.AddTransUnit` and `XliffLocalizationManager.MergeXliffDocuments` when a trans-unit has no source variant (e.g., from a malformed XLIFF file). +- [L10NSharp] Fixed `NullReferenceException` in `XLiffBody.AddTransUnit`, `XLiffBody.NumberTranslated`, and `XliffLocalizationManager.MergeXliffDocuments` when a trans-unit has no source variant (e.g. from a malformed XLIFF file). ### Removed