From b37267040f65ae43e2523093ed9fa1a0a0ad9a4a Mon Sep 17 00:00:00 2001 From: jsboige Date: Mon, 6 Jul 2026 01:23:24 +0200 Subject: [PATCH] fix(mindmap): #665 wire Virtue mindmap localization for ar/fa/zh (was rendering French) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Virtues mind map rendered French node text for ar/fa/zh because the localization was only half-wired. #636 §2 wired en/ru/pt/es and deferred the ar/fa/zh trio pending a Virtue entity extension. #686's PR body claimed the ar/fa/zh wiring (commit c2e51192) but it never landed on master: HEAD:Virtue.cs had 0 ar/fa/zh properties, the MindMapLocalization Virtue blocks listed only en/ru/pt/es, and the code comment still read "Ar/Fa/Zh not yet mapped ... render FR". This completes the two-layer wiring, mirroring the working Fallacy pattern: - Layer A (entity): Virtue.cs gains Title/Description/Remark/Link/Family/ Subfamily/Subsubfamily x Ar/Fa/Zh properties + ClassMap .Optional() bindings to the CSV *_ar/_fa/_zh columns (already fully translated, 223/223 native script). - Layer B (config): the two Virtue MindMapLocalization tables now carry ar/fa/zh entries, so {item.TitleFr} -> {item.TitleAr/Fa/Zh} etc. for all 8 languages. Tests: - Flipped the #636 §2 deferral guardrail (VirtueMindMapLocalizationTests) in place to the completed wired-all-8 contract, per its own explicit instruction. - Added VirtueMindMap_GeneratesNativeScript_ForArFaZh: generates a real .mm from the taxonomy CSV (headless, FreeMind GUI disabled) and asserts the Arabic/Persian/CJK Unicode block is present in node text - the regression that would have caught "Virtues mind maps in French instead of the target languages". Full suite: 587 pass / 1 known-fail (#133 OWL round-trip, pre-existing) / 5 skip. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../MindmapGeneration/MmGeneratorTests.cs | 90 ++++++++++ .../VirtueMindMapLocalizationTests.cs | 164 ++++++++++-------- .../AssetConverterConfig.cs | 15 +- .../Entities/Virtue.cs | 48 +++++ 4 files changed, 239 insertions(+), 78 deletions(-) diff --git a/Generation/Converters/Argumentum.AssetConverter.Tests/MindmapGeneration/MmGeneratorTests.cs b/Generation/Converters/Argumentum.AssetConverter.Tests/MindmapGeneration/MmGeneratorTests.cs index 6cf993483..e85741b23 100644 --- a/Generation/Converters/Argumentum.AssetConverter.Tests/MindmapGeneration/MmGeneratorTests.cs +++ b/Generation/Converters/Argumentum.AssetConverter.Tests/MindmapGeneration/MmGeneratorTests.cs @@ -243,6 +243,96 @@ private async Task> GetVirtueTestDataAsync(string csvFileName) return records; } + /// + /// Loads Virtue records from an absolute CSV path (used to feed the real taxonomy CSV, + /// which carries the fully-translated *_ar/_fa/_zh columns, into the mind-map path). + /// + private static async Task> LoadVirtuesFromPathAsync(string csvPath) + { + var config = new CsvConfiguration(CultureInfo.InvariantCulture) + { + HeaderValidated = null, + MissingFieldFound = null + }; + using var reader = new StreamReader(csvPath); + using var csv = new CsvReader(reader, config); + csv.Context.RegisterClassMap(); + + var records = new List(); + await foreach (var record in csv.GetRecordsAsync()) + { + records.Add(record); + } + return records; + } + + /// + /// Walks up from the test bin directory to locate the committed Virtues taxonomy CSV + /// (Cards/Fallacies/Argumentum Virtues - Taxonomy.csv, at the repo root in every checkout). + /// + private static string FindRepoVirtuesCsv() + { + var dir = new DirectoryInfo(System.AppContext.BaseDirectory); + for (int i = 0; i < 12 && dir != null; i++, dir = dir.Parent) + { + var candidate = Path.Combine(dir.FullName, "Cards", "Fallacies", "Argumentum Virtues - Taxonomy.csv"); + if (File.Exists(candidate)) return candidate; + } + return null; + } + + /// + /// #665 empirical guard — the Virtue mind map must render NATIVE script (not French fallback) + /// for ar/fa/zh once the entity + MindMapLocalization tables are wired. Generates a real .mm + /// from the actual taxonomy CSV (FreeMind GUI disabled via FreeMindPath=""), applying the + /// production localization exactly as the pipeline does, and asserts the target Unicode block is + /// present in the node text. This is the regression that would have caught the "Virtues mind maps + /// in French instead of the target languages" defect. + /// + [Theory] + [InlineData("ar", 0x0600, 0x06FF)] // Arabic block + [InlineData("fa", 0x0600, 0x06FF)] // Persian (Arabic script + Persian extensions, both in this block) + [InlineData("zh", 0x4E00, 0x9FFF)] // CJK Unified Ideographs + public async Task VirtueMindMap_GeneratesNativeScript_ForArFaZh(string lang, int lo, int hi) + { + // Arrange — real taxonomy CSV (carries the translated *_ar/_fa/_zh columns). + var csvPath = FindRepoVirtuesCsv(); + csvPath.Should().NotBeNull("the committed Virtues taxonomy CSV must be locatable from the test bin dir"); + + var virtues = await LoadVirtuesFromPathAsync(csvPath); + virtues.Should().NotBeEmpty(); + + // Apply the production MindMapLocalization for the target language (rewrites the FR-suffixed + // source tokens in the expressions to the per-language Virtue properties), as the pipeline does. + var generator = new VirtueMindMapDocumentConfig { DocumentName = $"virtues-{lang}.mm" }; + foreach (var localization in _config.LocalizationConfig.MindMapLocalization) + { + localization.DoReflectionTranslate(generator, lang); + } + + var originalInteractive = Program.IsInteractive; + try + { + Program.IsInteractive = false; // no interactive SVG prompt + // Act — .mm is serialized before any SVG export; FreeMindPath="" ⇒ no GUI, XSLT throwaway. + await generator.GenerateMindMapFile(virtues, _config, _tempTestDirectory, lang); + } + finally + { + Program.IsInteractive = originalInteractive; + } + + var mmPath = Path.Combine(_tempTestDirectory, generator.DocumentName); + File.Exists(mmPath).Should().BeTrue($"the {lang} Virtue mind map .mm must be generated"); + var mm = await File.ReadAllTextAsync(mmPath); + + // Assert — native script present in node text (would be ~0 if it fell back to French). + var nativeCount = mm.Count(ch => ch >= lo && ch <= hi); + nativeCount.Should().BeGreaterThan(50, + $"the generated '{lang}' Virtue mind map must contain native-script node text " + + $"(Unicode U+{lo:X4}–U+{hi:X4}), not French fallback — got {nativeCount} native code points"); + } + [Theory] [InlineData("en", "TextEn", "DescEn", "ExampleEn", "LinkEnFallback", "Subfamily", "Subsubfamily", "Family")] [InlineData("ru", "TextRu", "DescRu", "Exampleru", "LinkRuFallback", "SubfamilyRu", "SubsubfamilyRu", "FamilyRu")] diff --git a/Generation/Converters/Argumentum.AssetConverter.Tests/MindmapGeneration/VirtueMindMapLocalizationTests.cs b/Generation/Converters/Argumentum.AssetConverter.Tests/MindmapGeneration/VirtueMindMapLocalizationTests.cs index 4271df01e..860a9aae3 100644 --- a/Generation/Converters/Argumentum.AssetConverter.Tests/MindmapGeneration/VirtueMindMapLocalizationTests.cs +++ b/Generation/Converters/Argumentum.AssetConverter.Tests/MindmapGeneration/VirtueMindMapLocalizationTests.cs @@ -17,22 +17,25 @@ namespace Argumentum.AssetConverter.Tests.MindmapGeneration /// only rewrote the tree-root literal "Vertus". /// Layer A (entity): exposed no per-language title/family binding for /// the mind-map expressions. - /// The characterization suite was an executable GUARDRAIL, expected to FAIL when the fix landed, - /// with an explicit instruction to "flip these assertions to the localized expectation (mirroring - /// ) rather than silently deleting them". /// - /// #636 §2 landed that fix, but PARTIALLY, per jsboige's arbitration (wire the 4 languages whose - /// data + entity binding already exist; defer the 3 that need an entity extension): - /// • Layer B — the stub is replaced by two real per-field conversion tables mirroring Fallacies - /// (title/description + family hierarchy), sourced from the FR-suffixed Virtue.*Fr - /// properties, covering en/ru/pt/es only. - /// • Layer A — the Virtue mind-map expressions now reference the raw FR-suffixed props - /// ({item.TitleFr}, {item.FamilyFr}, …) so the StaticConversions can rewrite them. - /// • Ar/Fa/Zh are DEFERRED: still has no Title/Family Ar/Fa/Zh property, so - /// those languages keep rendering FR until the entity is extended (a follow-up chantier). + /// #636 §2 landed the fix PARTIALLY (wire the 4 languages whose data + entity binding already + /// exist — en/ru/pt/es — and defer the 3 that needed an entity extension — ar/fa/zh). The + /// deferral was an executable guardrail with an explicit instruction to "flip these assertions + /// to the localized expectation rather than silently deleting them" once the entity grew the + /// Ar/Fa/Zh columns. /// - /// This suite pins that mixed end-state: localized for the 4 wired languages, FR for the 3 deferred - /// ones, and the entity boundary that draws the line between them. + /// #665 lands that entity extension and completes the wiring for ALL EIGHT languages: + /// • Layer A — now exposes Title/Description/Remark/Link/Family/Subfamily/ + /// Subsubfamily × Ar/Fa/Zh, with ClassMap .Optional() bindings to the CSV + /// *_ar/_fa/_zh columns (which were already fully translated, 223/223, native script). + /// • Layer B — the two Virtue per-field conversion tables (title/description + family hierarchy) + /// now carry ar/fa/zh entries alongside en/ru/pt/es, so the StaticConversions rewrite + /// {item.TitleFr}{item.TitleAr} etc. for every non-FR language. + /// + /// This suite now pins the completed end-state: localized for all 7 non-FR languages, FR default + /// via the FR-suffixed source tokens, and the entity carrying all eight language columns. The + /// former deferral tests (ar/fa/zh stay FR; entity lacks Ar/Fa/Zh; tables cover en/ru/pt/es only) + /// are flipped in place to their localized counterparts. /// public class VirtueMindMapLocalizationTests { @@ -58,10 +61,10 @@ private static VirtueMindMapDocumentConfig LocalizedVirtueConfig(string lang) } // ───────────────────────────────────────────────────────────────────────────── - // (1) WIRED LANGUAGES — en/ru/pt/es. After localization the title and description - // expressions reference the per-language Virtue property, and the FR source token is - // fully rewritten. This is the flipped counterpart of the old - // VirtueTitleExpression_RemainsFrozenFRAfterLocalizationForEveryNonFrLang guardrail. + // (1) TITLE + DESCRIPTION — all 7 non-FR languages. After localization the title and + // description expressions reference the per-language Virtue property, and the FR source + // token is fully rewritten. ar/fa/zh were flipped here from the #636 §2 deferral guardrail + // once #665 grew their entity columns. // ───────────────────────────────────────────────────────────────────────────── [Theory] @@ -70,27 +73,30 @@ private static VirtueMindMapDocumentConfig LocalizedVirtueConfig(string lang) [InlineData("ru", "TitleRu", "DescriptionRu")] [InlineData("pt", "TitlePt", "DescriptionPt")] [InlineData("es", "TitleEs", "DescriptionEs")] - public void VirtueTitleAndDescription_LocalizeForWiredLanguages_en_ru_pt_es( + [InlineData("ar", "TitleAr", "DescriptionAr")] + [InlineData("fa", "TitleFa", "DescriptionFa")] + [InlineData("zh", "TitleZh", "DescriptionZh")] + public void VirtueTitleAndDescription_LocalizeForEveryNonFrLanguage( string lang, string expectedTitle, string expectedDescription) { var config = LocalizedVirtueConfig(lang); config.TitleExpression.Should().Be($"{{item.{expectedTitle}}}", $"the Virtue title expression must be rewritten from {{item.TitleFr}} to {{item.{expectedTitle}}} " + - $"for the wired lang '{lang}' (#636 §2 Layer B table)"); + $"for lang '{lang}' (#636 §2 / #665 Layer B table)"); config.TitleExpression.Should().NotContain("TitleFr", - $"the FR source token must be fully rewritten for wired lang '{lang}'"); + $"the FR source token must be fully rewritten for lang '{lang}'"); config.DescriptionExpression.Should().Contain(expectedDescription, - $"the Virtue description expression must reference {expectedDescription} for wired lang '{lang}'"); + $"the Virtue description expression must reference {expectedDescription} for lang '{lang}'"); config.DescriptionExpression.Should().NotContain("DescriptionFr", - $"the FR description token must be fully rewritten for wired lang '{lang}'"); + $"the FR description token must be fully rewritten for lang '{lang}'"); } // ───────────────────────────────────────────────────────────────────────────── - // (2) WIRED LANGUAGES — family hierarchy. Same four languages; pins the most-specific-first - // ordering (Subsubfamily > Subfamily > Family) so no partial-match cross-talk, mirroring - // the Fallacy FamilyHierarchy_LocalizationOrderingPreventsPartialMatchContamination test. + // (2) FAMILY HIERARCHY — all 7 non-FR languages; pins the most-specific-first ordering + // (Subsubfamily > Subfamily > Family) so no partial-match cross-talk, mirroring the + // Fallacy FamilyHierarchy_LocalizationOrderingPreventsPartialMatchContamination test. // ───────────────────────────────────────────────────────────────────────────── [Theory] @@ -99,17 +105,20 @@ public void VirtueTitleAndDescription_LocalizeForWiredLanguages_en_ru_pt_es( [InlineData("ru", "FamilyRu", "SubfamilyRu", "SubsubfamilyRu")] [InlineData("pt", "FamilyPt", "SubfamilyPt", "SubsubfamilyPt")] [InlineData("es", "FamilyEs", "SubfamilyEs", "SubsubfamilyEs")] - public void VirtueFamilyHierarchy_LocalizesForWiredLanguages_en_ru_pt_es( + [InlineData("ar", "FamilyAr", "SubfamilyAr", "SubsubfamilyAr")] + [InlineData("fa", "FamilyFa", "SubfamilyFa", "SubsubfamilyFa")] + [InlineData("zh", "FamilyZh", "SubfamilyZh", "SubsubfamilyZh")] + public void VirtueFamilyHierarchy_LocalizesForEveryNonFrLanguage( string lang, string expectedFamily, string expectedSubfamily, string expectedSubsubfamily) { var config = LocalizedVirtueConfig(lang); config.FamilleExpression.Should().Be($"{{item.{expectedFamily}}}", - $"Famille must localize to {expectedFamily} for wired lang '{lang}'"); + $"Famille must localize to {expectedFamily} for lang '{lang}'"); config.SousFamilleExpression.Should().Be($"{{item.{expectedSubfamily}}}", - $"SousFamille must localize to {expectedSubfamily} for wired lang '{lang}'"); + $"SousFamille must localize to {expectedSubfamily} for lang '{lang}'"); config.SoussousFamilleExpression.Should().Be($"{{item.{expectedSubsubfamily}}}", - $"Soussousfamille must localize to {expectedSubsubfamily} for wired lang '{lang}'"); + $"Soussousfamille must localize to {expectedSubsubfamily} for lang '{lang}'"); // The corruption signature of a broken most-specific-first ordering: the middle token // ("SubfamilyFr") is a prefix-family of "SubsubfamilyFr". Order guards it; pin its absence. @@ -117,45 +126,52 @@ public void VirtueFamilyHierarchy_LocalizesForWiredLanguages_en_ru_pt_es( "the Soussousfamille expression must resolve wholly — no leftover intermediate FR token"); (config.FamilleExpression + config.SousFamilleExpression + config.SoussousFamilleExpression) .Should().NotContain("Fr}", - $"no FR family token may survive localization for wired lang '{lang}'"); + $"no FR family token may survive localization for lang '{lang}'"); } // ───────────────────────────────────────────────────────────────────────────── - // (3) DEFERRED LANGUAGES — ar/fa/zh. The Virtue conversion tables cover only en/ru/pt/es - // (Layer A entity gap), so every Virtue expression stays FR-frozen for these three. - // This is the intentional deferral boundary; it flips to localized only once the entity - // grows TitleAr/Fa/Zh + Family/… Ar/Fa/Zh properties and the config tables extend. + // (3) FORMERLY-DEFERRED LANGUAGES — ar/fa/zh. This is the in-place flip of the old + // VirtueExpressions_StayFrForDeferredLanguages_ar_fa_zh guardrail: with #665 the Virtue + // conversion tables + entity now cover ar/fa/zh, so every Virtue expression localizes and + // no FR-suffixed source token survives. // ───────────────────────────────────────────────────────────────────────────── [Theory] [InlineData("ar")] [InlineData("fa")] [InlineData("zh")] - public void VirtueExpressions_StayFrForDeferredLanguages_ar_fa_zh(string lang) + public void VirtueExpressions_NoLongerFrozenFr_ForFormerlyDeferredLanguages_ar_fa_zh(string lang) { var config = LocalizedVirtueConfig(lang); - - config.TitleExpression.Should().Be("{item.TitleFr}", - $"Virtue title stays FR for deferred lang '{lang}' — the entity has no Title{lang} property yet"); - config.FamilleExpression.Should().Be("{item.FamilyFr}", - $"Virtue family stays FR for deferred lang '{lang}'"); - config.SousFamilleExpression.Should().Be("{item.SubfamilyFr}", - $"Virtue subfamily stays FR for deferred lang '{lang}'"); - config.SoussousFamilleExpression.Should().Be("{item.SubsubfamilyFr}", - $"Virtue subsubfamily stays FR for deferred lang '{lang}'"); - config.DescriptionExpression.Should().Contain("DescriptionFr", - $"Virtue description stays FR for deferred lang '{lang}'"); + var suffix = lang == "ar" ? "Ar" : lang == "fa" ? "Fa" : "Zh"; + + config.TitleExpression.Should().Be($"{{item.Title{suffix}}}", + $"Virtue title now localizes for formerly-deferred lang '{lang}' (#665 entity extension)"); + config.FamilleExpression.Should().Be($"{{item.Family{suffix}}}", + $"Virtue family now localizes for '{lang}'"); + config.SousFamilleExpression.Should().Be($"{{item.Subfamily{suffix}}}", + $"Virtue subfamily now localizes for '{lang}'"); + config.SoussousFamilleExpression.Should().Be($"{{item.Subsubfamily{suffix}}}", + $"Virtue subsubfamily now localizes for '{lang}'"); + config.DescriptionExpression.Should().Contain($"Description{suffix}", + $"Virtue description now localizes for '{lang}'"); + + // No FR-suffixed source token may survive for the formerly-deferred languages. + (config.TitleExpression + config.DescriptionExpression + config.FamilleExpression + + config.SousFamilleExpression + config.SoussousFamilleExpression) + .Should().NotContain("Fr}", + $"no FR source token may survive localization for formerly-deferred lang '{lang}'"); } // ───────────────────────────────────────────────────────────────────────────── - // (4) CONFIG SHAPE — Layer B is no longer a stale root-literal stub. There are now two - // Virtue-targeting entries (title/description + family hierarchy), each a real per-field - // conversion table sourced from Virtue.*Fr properties, covering en/ru/pt/es. The old - // single "Vertus" root-literal conversion is gone. + // (4) CONFIG SHAPE — the two Virtue-targeting entries (title/description + family hierarchy) + // are real per-field conversion tables sourced from Virtue.*Fr properties, and every + // conversion now covers all 7 non-FR languages (en/ru/pt/es/ar/fa/zh). The retired #601 + // single "Vertus" root-literal conversion stays gone. // ───────────────────────────────────────────────────────────────────────────── [Fact] - public void VirtueLocalizationEntries_MirrorFallacyTables_NotStaleStub() + public void VirtueLocalizationEntries_MirrorFallacyTables_CoverAllEightLanguages() { var mindMapLoc = FreshConfig.LocalizationConfig.MindMapLocalization; @@ -177,14 +193,14 @@ public void VirtueLocalizationEntries_MirrorFallacyTables_NotStaleStub() nameof(VirtueMindMapDocumentConfig.SoussousFamilleExpression), }, "the Virtue family entry mirrors the Fallacy family-hierarchy shape"); - // Every Virtue conversion covers exactly en/ru/pt/es — ar/fa/zh deferred (Layer A gap). + // Every Virtue conversion now covers all 7 non-FR languages — ar/fa/zh wired by #665. foreach (var entry in new[] { titleEntry, familyEntry }) { foreach (var (_, textConversions) in entry.StaticConversions) { textConversions.Select(c => c.Language).Should().BeEquivalentTo( - new[] { "en", "ru", "pt", "es" }, - "the Virtue tables are wired for en/ru/pt/es only; ar/fa/zh are deferred until the entity grows their columns"); + new[] { "en", "ru", "pt", "es", "ar", "fa", "zh" }, + "the Virtue tables are wired for all 7 non-FR languages (#665 completed the ar/fa/zh trio)"); } } @@ -195,34 +211,40 @@ public void VirtueLocalizationEntries_MirrorFallacyTables_NotStaleStub() } // ───────────────────────────────────────────────────────────────────────────── - // (5) ENTITY BOUNDARY — documents WHY ar/fa/zh are deferred (Layer A). Virtue maps - // Fr/En/Ru/Pt/Es titles but no Ar/Fa/Zh; the convenience accessor Text still delegates - // to TitleFr. Flips to a wired expectation only when the entity extension chantier lands. + // (5) ENTITY BOUNDARY — Virtue now maps all eight languages (Layer A complete). This is the + // in-place flip of VirtueEntity_WiresEnRuPtEs_DefersArFaZh: TitleAr/Fa/Zh now exist, which + // is the concrete reason ar/fa/zh localize. The convenience accessor Text still delegates + // to TitleFr (localization flows through the expression table, not Text). // ───────────────────────────────────────────────────────────────────────────── [Fact] - public void VirtueEntity_WiresEnRuPtEs_DefersArFaZh() + public void VirtueEntity_WiresAllEightLanguages() { var virtueType = typeof(Virtue); - // Wired languages have their title property (so Layer A is present for en/ru/pt/es). + // FR source-of-truth + the four previously-wired languages. virtueType.GetProperty("TitleFr").Should().NotBeNull("TitleFr is the FR source-of-truth"); virtueType.GetProperty("TitleEn").Should().NotBeNull("TitleEn exists (en is wired)"); virtueType.GetProperty("TitleRu").Should().NotBeNull("TitleRu exists (ru is wired)"); virtueType.GetProperty("TitlePt").Should().NotBeNull("TitlePt exists (pt is wired)"); virtueType.GetProperty("TitleEs").Should().NotBeNull("TitleEs exists (es is wired)"); - // Deferred languages have NO title property yet — the concrete reason ar/fa/zh stay FR. - virtueType.GetProperty("TitleAr").Should().BeNull( - "Virtue exposes no TitleAr property yet — ar mind map localization is deferred"); - virtueType.GetProperty("TitleFa").Should().BeNull( - "Virtue exposes no TitleFa property yet — fa mind map localization is deferred"); - virtueType.GetProperty("TitleZh").Should().BeNull( - "Virtue exposes no TitleZh property yet — zh mind map localization is deferred"); - - // The Text convenience accessor still delegates to the FR title (unchanged by #636 §2 — - // the fix works through the {item.TitleFr} expression + config table, not by making Text - // language-aware). Kept as a pin so a future Text change is a conscious decision. + // The formerly-deferred trio now has its title property — the concrete reason ar/fa/zh localize. + virtueType.GetProperty("TitleAr").Should().NotBeNull( + "Virtue now exposes TitleAr — ar mind map localization is wired (#665)"); + virtueType.GetProperty("TitleFa").Should().NotBeNull( + "Virtue now exposes TitleFa — fa mind map localization is wired (#665)"); + virtueType.GetProperty("TitleZh").Should().NotBeNull( + "Virtue now exposes TitleZh — zh mind map localization is wired (#665)"); + + // Full-family coverage for the formerly-deferred trio (family hierarchy also localizes). + virtueType.GetProperty("FamilyAr").Should().NotBeNull("FamilyAr exists (ar family hierarchy wired)"); + virtueType.GetProperty("SubfamilyFa").Should().NotBeNull("SubfamilyFa exists (fa family hierarchy wired)"); + virtueType.GetProperty("SubsubfamilyZh").Should().NotBeNull("SubsubfamilyZh exists (zh family hierarchy wired)"); + + // The Text convenience accessor still delegates to the FR title — localization flows through + // the {item.TitleFr} expression + config table, not by making Text language-aware. Kept as a + // pin so a future Text change is a conscious decision. var virtue = new Virtue { TitleFr = "Échange enrichissant", TitleEn = "Enriching exchange" }; virtue.Text.Should().Be("Échange enrichissant", "Virtue.Text delegates to TitleFr; localization flows through the expression table, not Text"); diff --git a/Generation/Converters/Argumentum.AssetConverter/AssetConverterConfig.cs b/Generation/Converters/Argumentum.AssetConverter/AssetConverterConfig.cs index a4be0e8eb..b350a4651 100644 --- a/Generation/Converters/Argumentum.AssetConverter/AssetConverterConfig.cs +++ b/Generation/Converters/Argumentum.AssetConverter/AssetConverterConfig.cs @@ -238,8 +238,9 @@ public class AssetConverterConfig (nameof(Fallacy.Famille), new List<(string Language, string destText)>(new []{("en", "Family"), ("ru", nameof(Fallacy.FamilyRu)), ("pt", nameof(Fallacy.FamilyPt)), ("es", nameof(Fallacy.FamilyEs)), ("ar", nameof(Fallacy.FamilyAr)), ("fa", nameof(Fallacy.FamilyFa)), ("zh", nameof(Fallacy.FamilyZh))}) ), }), }, - // Virtue text fields: FR property names → localized property names (#636 §2). - // Wired for En/Ru/Pt/Es (data present in CSV + Virtue entity). Ar/Fa/Zh not yet mapped on the Virtue entity → deferred, render FR. + // Virtue text fields: FR property names → localized property names (#636 §2, #665). + // Wired for all 8 languages (En/Ru/Pt/Es/Ar/Fa/Zh): data present in CSV (title_/description_*_ar/fa/zh) + // and now mapped on the Virtue entity (properties + ClassMap .Optional() bindings, #665). new DocumentLocalization(){ TargetProperties = new List(new [] { @@ -247,8 +248,8 @@ public class AssetConverterConfig nameof(VirtueMindMapDocumentConfig.DescriptionExpression), }), StaticConversions = new List<(string sourceText, List<(string Language, string destText)> textConversions)>(new[]{ - (nameof(Virtue.TitleFr), new List<(string Language, string destText)>(new []{("en", nameof(Virtue.TitleEn)), ("ru", nameof(Virtue.TitleRu)), ("pt", nameof(Virtue.TitlePt)), ("es", nameof(Virtue.TitleEs))}) ), - (nameof(Virtue.DescriptionFr), new List<(string Language, string destText)>(new []{("en", nameof(Virtue.DescriptionEn)), ("ru", nameof(Virtue.DescriptionRu)), ("pt", nameof(Virtue.DescriptionPt)), ("es", nameof(Virtue.DescriptionEs))}) ), + (nameof(Virtue.TitleFr), new List<(string Language, string destText)>(new []{("en", nameof(Virtue.TitleEn)), ("ru", nameof(Virtue.TitleRu)), ("pt", nameof(Virtue.TitlePt)), ("es", nameof(Virtue.TitleEs)), ("ar", nameof(Virtue.TitleAr)), ("fa", nameof(Virtue.TitleFa)), ("zh", nameof(Virtue.TitleZh))}) ), + (nameof(Virtue.DescriptionFr), new List<(string Language, string destText)>(new []{("en", nameof(Virtue.DescriptionEn)), ("ru", nameof(Virtue.DescriptionRu)), ("pt", nameof(Virtue.DescriptionPt)), ("es", nameof(Virtue.DescriptionEs)), ("ar", nameof(Virtue.DescriptionAr)), ("fa", nameof(Virtue.DescriptionFa)), ("zh", nameof(Virtue.DescriptionZh))}) ), }), }, // Virtue family hierarchy: FR names → localized names. @@ -261,9 +262,9 @@ public class AssetConverterConfig nameof(VirtueMindMapDocumentConfig.SoussousFamilleExpression), }), StaticConversions = new List<(string sourceText, List<(string Language, string destText)> textConversions)>(new[]{ - (nameof(Virtue.SubsubfamilyFr), new List<(string Language, string destText)>(new []{("en", nameof(Virtue.SubsubfamilyEn)), ("ru", nameof(Virtue.SubsubfamilyRu)), ("pt", nameof(Virtue.SubsubfamilyPt)), ("es", nameof(Virtue.SubsubfamilyEs))}) ), - (nameof(Virtue.SubfamilyFr), new List<(string Language, string destText)>(new []{("en", nameof(Virtue.SubfamilyEn)), ("ru", nameof(Virtue.SubfamilyRu)), ("pt", nameof(Virtue.SubfamilyPt)), ("es", nameof(Virtue.SubfamilyEs))}) ), - (nameof(Virtue.FamilyFr), new List<(string Language, string destText)>(new []{("en", nameof(Virtue.FamilyEn)), ("ru", nameof(Virtue.FamilyRu)), ("pt", nameof(Virtue.FamilyPt)), ("es", nameof(Virtue.FamilyEs))}) ), + (nameof(Virtue.SubsubfamilyFr), new List<(string Language, string destText)>(new []{("en", nameof(Virtue.SubsubfamilyEn)), ("ru", nameof(Virtue.SubsubfamilyRu)), ("pt", nameof(Virtue.SubsubfamilyPt)), ("es", nameof(Virtue.SubsubfamilyEs)), ("ar", nameof(Virtue.SubsubfamilyAr)), ("fa", nameof(Virtue.SubsubfamilyFa)), ("zh", nameof(Virtue.SubsubfamilyZh))}) ), + (nameof(Virtue.SubfamilyFr), new List<(string Language, string destText)>(new []{("en", nameof(Virtue.SubfamilyEn)), ("ru", nameof(Virtue.SubfamilyRu)), ("pt", nameof(Virtue.SubfamilyPt)), ("es", nameof(Virtue.SubfamilyEs)), ("ar", nameof(Virtue.SubfamilyAr)), ("fa", nameof(Virtue.SubfamilyFa)), ("zh", nameof(Virtue.SubfamilyZh))}) ), + (nameof(Virtue.FamilyFr), new List<(string Language, string destText)>(new []{("en", nameof(Virtue.FamilyEn)), ("ru", nameof(Virtue.FamilyRu)), ("pt", nameof(Virtue.FamilyPt)), ("es", nameof(Virtue.FamilyEs)), ("ar", nameof(Virtue.FamilyAr)), ("fa", nameof(Virtue.FamilyFa)), ("zh", nameof(Virtue.FamilyZh))}) ), }), }, // Document name: _fr. → _en./_ru./_pt. diff --git a/Generation/Converters/Argumentum.AssetConverter/Entities/Virtue.cs b/Generation/Converters/Argumentum.AssetConverter/Entities/Virtue.cs index 9e7d58590..e02563bb8 100644 --- a/Generation/Converters/Argumentum.AssetConverter/Entities/Virtue.cs +++ b/Generation/Converters/Argumentum.AssetConverter/Entities/Virtue.cs @@ -67,6 +67,30 @@ public class Virtue : CsvBase, IMindMapItem public string RemarkEs { get; set; } public string LinkEs { get; set; } + public string FamilyAr { get; set; } + public string SubfamilyAr { get; set; } + public string SubsubfamilyAr { get; set; } + public string TitleAr { get; set; } + public string DescriptionAr { get; set; } + public string RemarkAr { get; set; } + public string LinkAr { get; set; } + + public string FamilyFa { get; set; } + public string SubfamilyFa { get; set; } + public string SubsubfamilyFa { get; set; } + public string TitleFa { get; set; } + public string DescriptionFa { get; set; } + public string RemarkFa { get; set; } + public string LinkFa { get; set; } + + public string FamilyZh { get; set; } + public string SubfamilyZh { get; set; } + public string SubsubfamilyZh { get; set; } + public string TitleZh { get; set; } + public string DescriptionZh { get; set; } + public string RemarkZh { get; set; } + public string LinkZh { get; set; } + // #499 Phase 1 — 12 relational/AIF columns appended to the Virtues prod CSV (66→78). // crossLink_Opposes is the only one populated (the prevented Fallacy-family PK list); // the other 7 relation types + AIF Exception/Other are structurally empty by design. @@ -140,6 +164,30 @@ public VirtueClassMap() Map(m => m.RemarkEs).Name("remark_es").Optional(); Map(m => m.LinkEs).Name("link_es").Optional(); + Map(m => m.FamilyAr).Name("family_ar").Optional(); + Map(m => m.SubfamilyAr).Name("subfamily_ar").Optional(); + Map(m => m.SubsubfamilyAr).Name("subsubfamily_ar").Optional(); + Map(m => m.TitleAr).Name("title_ar").Optional(); + Map(m => m.DescriptionAr).Name("description_ar").Optional(); + Map(m => m.RemarkAr).Name("remark_ar").Optional(); + Map(m => m.LinkAr).Name("link_ar").Optional(); + + Map(m => m.FamilyFa).Name("family_fa").Optional(); + Map(m => m.SubfamilyFa).Name("subfamily_fa").Optional(); + Map(m => m.SubsubfamilyFa).Name("subsubfamily_fa").Optional(); + Map(m => m.TitleFa).Name("title_fa").Optional(); + Map(m => m.DescriptionFa).Name("description_fa").Optional(); + Map(m => m.RemarkFa).Name("remark_fa").Optional(); + Map(m => m.LinkFa).Name("link_fa").Optional(); + + Map(m => m.FamilyZh).Name("family_zh").Optional(); + Map(m => m.SubfamilyZh).Name("subfamily_zh").Optional(); + Map(m => m.SubsubfamilyZh).Name("subsubfamily_zh").Optional(); + Map(m => m.TitleZh).Name("title_zh").Optional(); + Map(m => m.DescriptionZh).Name("description_zh").Optional(); + Map(m => m.RemarkZh).Name("remark_zh").Optional(); + Map(m => m.LinkZh).Name("link_zh").Optional(); + // #499 Phase 1 — 12 relational/AIF columns. All Optional(): 9 are structurally // empty by design; 3 are populated for the 222 real Virtue nodes (pk=0 root empty). Map(m => m.CrossLinkPredatesOn).Name("crossLink_PredatesOn").Optional();