From 21afe078e4aaf5c8111bf57b2a8b98d7282c7e62 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 5 Jun 2026 10:59:40 -0400 Subject: [PATCH 1/6] Fix FilenamesToAddToCache yielding duplicates when UseLanguageCodeFolders=true (#140) The UseLanguageCodeFolders branch for the installed folder was missing the !langIdsOfCustomizedLocales.Contains(langId) guard that the non-UseLanguageCodeFolders branch already applies. Without it, both the custom and installed XLIFF for the same language were yielded, and whichever loaded last silently overwrote the other in the cache. --- src/L10NSharp/XLiffUtils/XliffLocalizationManager.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/L10NSharp/XLiffUtils/XliffLocalizationManager.cs b/src/L10NSharp/XLiffUtils/XliffLocalizationManager.cs index fcd284c..16ffc5a 100644 --- a/src/L10NSharp/XLiffUtils/XliffLocalizationManager.cs +++ b/src/L10NSharp/XLiffUtils/XliffLocalizationManager.cs @@ -369,8 +369,11 @@ public IEnumerable FilenamesToAddToCache if (string.IsNullOrEmpty(langId) || langId == LocalizationManager.kDefaultLang) continue; - langIdsOfCustomizedLocales.Add(langId); - yield return xliffFile; + if (!langIdsOfCustomizedLocales.Contains(langId)) + { + langIdsOfCustomizedLocales.Add(langId); + yield return xliffFile; + } } } else From de32d1294205a80eb2113102fba9b68a8cc1a358 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 5 Jun 2026 11:26:18 -0400 Subject: [PATCH 2/6] Update CHANGELOG for #140 fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 895773a..11adf4c 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 `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) ### Removed From b911f08037ebbeb65f8db3568d8f482c092725de Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 5 Jun 2026 11:43:56 -0400 Subject: [PATCH 3/6] Add regression test: custom translation wins over installed with UseLanguageCodeFolders (#140) Co-Authored-By: Claude Sonnet 4.6 --- .../LocalizationManagerTestsBase.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/L10NSharp.Tests/LocalizationManagerTestsBase.cs b/src/L10NSharp.Tests/LocalizationManagerTestsBase.cs index 5189f53..d8f01af 100644 --- a/src/L10NSharp.Tests/LocalizationManagerTestsBase.cs +++ b/src/L10NSharp.Tests/LocalizationManagerTestsBase.cs @@ -448,6 +448,59 @@ public void GetString_OverloadThatTakesListOfLanguages_WorksWithFolders(IEnumera } } + /// + /// Regression test for fix/135: when UseLanguageCodeFolders is true and both an installed + /// and a user-modified file exist for the same language, the custom (user-modified) file + /// must win. Before the fix, FilenamesToAddToCache yielded both files and whichever loaded + /// last would overwrite the other. + /// + [Test] + public void CustomTranslation_TakesPrecedenceOverInstalled_WithLanguageCodeFolders() + { + try + { + LocalizationManager.UseLanguageCodeFolders = true; + using (var folder = new TempFolder()) + { + // Set up the English default in the installed directory. + AddEnglishTranslation(GetInstalledDirectory(folder), "1.0"); + + // Set up an installed Arabic translation with value "inArabic". + AddArabicTranslation(GetInstalledDirectory(folder)); + + // Set up a user-modified Arabic translation for the same "theId" string + // with a different value to confirm the custom one wins. + var customArabicDoc = CreateNewDocument(null, "en", "ar"); + var customTu = CreateTransUnit("theId", false, + CreateTransUnitVariant("en", "wrong"), + CreateTransUnitVariant("ar", "custom arabic translation"), + "Test", TranslationStatus.Approved); + customArabicDoc.AddTransUnit(customTu); + var customArabicPath = Path.Combine(GetUserModifiedDirectory(folder), + LocalizationManager.GetTranslationFileNameForLanguage(AppId, "ar")); + Directory.CreateDirectory(Path.GetDirectoryName(customArabicPath)); + customArabicDoc.Save(customArabicPath); + + // Set up the localization manager. + var manager = CreateLocalizationManager(AppId, AppName, AppVersion, + GetInstalledDirectory(folder), GetGeneratedDirectory(folder), + GetUserModifiedDirectory(folder)); + LocalizationManagerInternal.LoadedManagers[AppId] = manager; + + LocalizationManager.SetUILanguage("ar"); + + // SUT: the custom translation must win over the installed one. + Assert.AreEqual("custom arabic translation", + LocalizationManager.GetString("theId", "default"), + "Custom (user-modified) translation should take precedence over installed translation when UseLanguageCodeFolders is true"); + } + } + finally + { + LocalizationManager.UseLanguageCodeFolders = false; + } + } + [Test] public void GetUiLanguages_AzeriHasHackedNativeName() { From cbb9546c247f19af83db3f56ba1b5038b1f943e3 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Mon, 8 Jun 2026 09:07:01 -0400 Subject: [PATCH 4/6] Trim comment --- src/L10NSharp.Tests/LocalizationManagerTestsBase.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/L10NSharp.Tests/LocalizationManagerTestsBase.cs b/src/L10NSharp.Tests/LocalizationManagerTestsBase.cs index d8f01af..cffae11 100644 --- a/src/L10NSharp.Tests/LocalizationManagerTestsBase.cs +++ b/src/L10NSharp.Tests/LocalizationManagerTestsBase.cs @@ -449,10 +449,8 @@ public void GetString_OverloadThatTakesListOfLanguages_WorksWithFolders(IEnumera } /// - /// Regression test for fix/135: when UseLanguageCodeFolders is true and both an installed - /// and a user-modified file exist for the same language, the custom (user-modified) file - /// must win. Before the fix, FilenamesToAddToCache yielded both files and whichever loaded - /// last would overwrite the other. + /// When UseLanguageCodeFolders is true and both an installed and a user-modified file + /// exist for the same language, the custom (user-modified) file must win. /// [Test] public void CustomTranslation_TakesPrecedenceOverInstalled_WithLanguageCodeFolders() From 0d3400df73e7bea2a5716b9ca8030ca9715067bd Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Mon, 8 Jun 2026 09:43:10 -0400 Subject: [PATCH 5/6] Remove redundant try/finally blocks in test base TearDown already resets UseLanguageCodeFolders to false, making the finally clauses in GetUiLanguages_FindsAllWithFolders, GetDynamicStringInEnglish_NoDefault_FindsEnglishWithFolders, and CustomTranslation_TakesPrecedenceOverInstalled_WithLanguageCodeFolders redundant. Co-Authored-By: Claude Sonnet 4.6 --- .../LocalizationManagerTestsBase.cs | 109 +++++++----------- 1 file changed, 44 insertions(+), 65 deletions(-) diff --git a/src/L10NSharp.Tests/LocalizationManagerTestsBase.cs b/src/L10NSharp.Tests/LocalizationManagerTestsBase.cs index cffae11..d5a184c 100644 --- a/src/L10NSharp.Tests/LocalizationManagerTestsBase.cs +++ b/src/L10NSharp.Tests/LocalizationManagerTestsBase.cs @@ -380,39 +380,25 @@ public void GetUiLanguages_FindsAll() [Test] public void GetUiLanguages_FindsAllWithFolders() { - try - { - LocalizationManager.UseLanguageCodeFolders = true; - using (var folder = new TempFolder()) - { - SetupManager(folder); - var cultures = new List(LocalizationManager.GetUILanguages(true)); - Assert.AreEqual(4, cultures.Count); - CollectionAssert.AreEquivalent(new[] { "ar", "en", "fr", "es" }, cultures.Select(c => c.IetfLanguageTag).ToArray()); - Assert.That(cultures, Is.Ordered.By("DisplayName")); - } - } - finally + LocalizationManager.UseLanguageCodeFolders = true; + using (var folder = new TempFolder()) { - LocalizationManager.UseLanguageCodeFolders = false; + SetupManager(folder); + var cultures = new List(LocalizationManager.GetUILanguages(true)); + Assert.AreEqual(4, cultures.Count); + CollectionAssert.AreEquivalent(new[] { "ar", "en", "fr", "es" }, cultures.Select(c => c.IetfLanguageTag).ToArray()); + Assert.That(cultures, Is.Ordered.By("DisplayName")); } } [Test] public void GetDynamicStringInEnglish_NoDefault_FindsEnglishWithFolders() { - try - { - LocalizationManager.UseLanguageCodeFolders = true; - using (var folder = new TempFolder()) - { - SetupManager(folder); - Assert.That(LocalizationManager.GetDynamicString(AppId, "blahId", null), Is.EqualTo("blah"), "With no default supplied, should find saved English"); - } - } - finally + LocalizationManager.UseLanguageCodeFolders = true; + using (var folder = new TempFolder()) { - LocalizationManager.UseLanguageCodeFolders = false; + SetupManager(folder); + Assert.That(LocalizationManager.GetDynamicString(AppId, "blahId", null), Is.EqualTo("blah"), "With no default supplied, should find saved English"); } } @@ -455,47 +441,40 @@ public void GetString_OverloadThatTakesListOfLanguages_WorksWithFolders(IEnumera [Test] public void CustomTranslation_TakesPrecedenceOverInstalled_WithLanguageCodeFolders() { - try - { - LocalizationManager.UseLanguageCodeFolders = true; - using (var folder = new TempFolder()) - { - // Set up the English default in the installed directory. - AddEnglishTranslation(GetInstalledDirectory(folder), "1.0"); - - // Set up an installed Arabic translation with value "inArabic". - AddArabicTranslation(GetInstalledDirectory(folder)); - - // Set up a user-modified Arabic translation for the same "theId" string - // with a different value to confirm the custom one wins. - var customArabicDoc = CreateNewDocument(null, "en", "ar"); - var customTu = CreateTransUnit("theId", false, - CreateTransUnitVariant("en", "wrong"), - CreateTransUnitVariant("ar", "custom arabic translation"), - "Test", TranslationStatus.Approved); - customArabicDoc.AddTransUnit(customTu); - var customArabicPath = Path.Combine(GetUserModifiedDirectory(folder), - LocalizationManager.GetTranslationFileNameForLanguage(AppId, "ar")); - Directory.CreateDirectory(Path.GetDirectoryName(customArabicPath)); - customArabicDoc.Save(customArabicPath); - - // Set up the localization manager. - var manager = CreateLocalizationManager(AppId, AppName, AppVersion, - GetInstalledDirectory(folder), GetGeneratedDirectory(folder), - GetUserModifiedDirectory(folder)); - LocalizationManagerInternal.LoadedManagers[AppId] = manager; - - LocalizationManager.SetUILanguage("ar"); - - // SUT: the custom translation must win over the installed one. - Assert.AreEqual("custom arabic translation", - LocalizationManager.GetString("theId", "default"), - "Custom (user-modified) translation should take precedence over installed translation when UseLanguageCodeFolders is true"); - } - } - finally + LocalizationManager.UseLanguageCodeFolders = true; + using (var folder = new TempFolder()) { - LocalizationManager.UseLanguageCodeFolders = false; + // Set up the English default in the installed directory. + AddEnglishTranslation(GetInstalledDirectory(folder), "1.0"); + + // Set up an installed Arabic translation with value "inArabic". + AddArabicTranslation(GetInstalledDirectory(folder)); + + // Set up a user-modified Arabic translation for the same "theId" string + // with a different value to confirm the custom one wins. + var customArabicDoc = CreateNewDocument(null, "en", "ar"); + var customTu = CreateTransUnit("theId", false, + CreateTransUnitVariant("en", "wrong"), + CreateTransUnitVariant("ar", "custom arabic translation"), + "Test", TranslationStatus.Approved); + customArabicDoc.AddTransUnit(customTu); + var customArabicPath = Path.Combine(GetUserModifiedDirectory(folder), + LocalizationManager.GetTranslationFileNameForLanguage(AppId, "ar")); + Directory.CreateDirectory(Path.GetDirectoryName(customArabicPath)); + customArabicDoc.Save(customArabicPath); + + // Set up the localization manager. + var manager = CreateLocalizationManager(AppId, AppName, AppVersion, + GetInstalledDirectory(folder), GetGeneratedDirectory(folder), + GetUserModifiedDirectory(folder)); + LocalizationManagerInternal.LoadedManagers[AppId] = manager; + + LocalizationManager.SetUILanguage("ar"); + + // SUT: the custom translation must win over the installed one. + Assert.AreEqual("custom arabic translation", + LocalizationManager.GetString("theId", "default"), + "Custom (user-modified) translation should take precedence over installed translation when UseLanguageCodeFolders is true"); } } From e6bc0d4ce3d2a9c4261d5a61942be7f6ac1b8f74 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Mon, 8 Jun 2026 09:46:28 -0400 Subject: [PATCH 6/6] Fix spacing --- src/L10NSharp.Tests/LocalizationManagerTestsBase.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/L10NSharp.Tests/LocalizationManagerTestsBase.cs b/src/L10NSharp.Tests/LocalizationManagerTestsBase.cs index d5a184c..dcba6f6 100644 --- a/src/L10NSharp.Tests/LocalizationManagerTestsBase.cs +++ b/src/L10NSharp.Tests/LocalizationManagerTestsBase.cs @@ -13,7 +13,7 @@ namespace L10NSharp.Tests { - public abstract class LocalizationManagerTestsBase where T: IDocument + public abstract class LocalizationManagerTestsBase where T : IDocument { protected const string AppId = "test"; protected const string AppName = "unit test"; @@ -216,19 +216,19 @@ public void CreateOrUpdateDefaultTranslationFileIfNecessary_Missing_IncludesStri Assert.AreEqual("My Own English String", ProxyLocalizationManager.MyOwnGetString("myOwn.English.String.Id", "My Own English String")); - + Assert.AreEqual("My Own English String (with comment)", ProxyLocalizationManager.MyOwnGetString("myOwn.English.String.Id.With.Comment", "My Own English String (with comment)", "This is used to test the case where MyOwnGetString is passed as an extra method to use for extraction.")); - + Assert.AreEqual("Click me", ProxyLocalizationManager.MyOwnGetString("myDlg.btnClickMe.Text", "Click me", "This is the text from the third version of MyOwnGetString.", "Click this thingy to do stuff.", "Ctrl-T", btnClickMe)); - + Assert.AreEqual("String to Localize", "String to Localize".Localize()); - + Assert.AreEqual("Another String to Localize", "Another String to Localize".Localize("With.Id.And.Comment", "This is used to test the case where Localize is passed as an extra method to use for extraction.")); }