From ea3e670f012361596c7205ad435fd52292f9f79d Mon Sep 17 00:00:00 2001 From: jsboige Date: Fri, 3 Jul 2026 15:43:20 +0200 Subject: [PATCH] =?UTF-8?q?fix(mindmap):=20#636=20=C2=A72=20=E2=80=94=20wi?= =?UTF-8?q?re=20Virtues=20mindmap=20i18n=20for=20En/Ru/Pt/Es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause (analogous to #216, documented in #601): the Virtues mind map rendered French node text for every language because of a 2-layer gap — Layer B, the MindMapLocalization Virtue entry was a stale stub only rewriting the tree-root literal "Vertus"; Layer A, the document config expressions referenced computed FR-only getters ({item.Text}, {item.Family}…) that StaticConversions could not target. Fix (jsboige arbitration: wire the 4 languages whose data + entity binding already exist; defer the 3 that need an entity extension): - VirtueMindMapDocumentConfig: 5 expression defaults now reference the raw FR-suffixed props ({item.TitleFr}, {item.FamilyFr}, {item.SubfamilyFr}, {item.SubsubfamilyFr}, item.DescriptionFr) so the StaticConversions can rewrite them per language. FR stays a no-op (Text => TitleFr). - AssetConverterConfig.MindMapLocalization: replaced the "Vertus" stub with two per-field tables mirroring Fallacies (title/description + family hierarchy, most-specific-first), covering en/ru/pt/es only. - Ar/Fa/Zh deferred: the Virtue entity has no Title/Family Ar/Fa/Zh property yet, so those three keep rendering FR (follow-up chantier). Tests: replaced the #601 FR-frozen characterization guardrail (per its own instruction to "flip these assertions to the localized expectation") with VirtueMindMapLocalizationTests — pins localized en/ru/pt/es, FR-frozen ar/fa/zh, the config-table shape, and the entity boundary. 129 mindmap+localization tests pass, 0 fail. Code only; regenerated SVG assets follow in the same PR. Co-Authored-By: Claude Fable 5 --- ...tueMindMapFRFrozenCharacterizationTests.cs | 193 --------------- .../VirtueMindMapLocalizationTests.cs | 231 ++++++++++++++++++ .../AssetConverterConfig.cs | 24 +- .../Mindmapper/VirtueMindMapDocumentConfig.cs | 11 +- 4 files changed, 258 insertions(+), 201 deletions(-) delete mode 100644 Generation/Converters/Argumentum.AssetConverter.Tests/MindmapGeneration/VirtueMindMapFRFrozenCharacterizationTests.cs create mode 100644 Generation/Converters/Argumentum.AssetConverter.Tests/MindmapGeneration/VirtueMindMapLocalizationTests.cs diff --git a/Generation/Converters/Argumentum.AssetConverter.Tests/MindmapGeneration/VirtueMindMapFRFrozenCharacterizationTests.cs b/Generation/Converters/Argumentum.AssetConverter.Tests/MindmapGeneration/VirtueMindMapFRFrozenCharacterizationTests.cs deleted file mode 100644 index b6e27fd69..000000000 --- a/Generation/Converters/Argumentum.AssetConverter.Tests/MindmapGeneration/VirtueMindMapFRFrozenCharacterizationTests.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System.Linq; -using System.Reflection; -using Argumentum.AssetConverter; -using Argumentum.AssetConverter.Entities; -using Argumentum.AssetConverter.Mindmapper; -using FluentAssertions; -using Xunit; - -namespace Argumentum.AssetConverter.Tests.MindmapGeneration -{ - /// - /// CHARACTERIZATION (Golden-Master) suite pinning the CURRENT behaviour of the Virtues mind map - /// localization — which is that Virtues .content.svg are FR-frozen across all 8 release - /// languages, in stark contrast to Fallacies (localized 8-lang, pinned by - /// ). - /// - /// This is NOT a regression test of desired behaviour. It is an executable snapshot of the - /// behaviour verified in investigation docs/investigations/2025-06-25-virtues-mindmap-fr-frozen-mechanism.md - /// (#601, merged at 71033f8c): the Virtues mind map pipeline renders French node text for - /// every language because of a 2-layer gap — - /// Layer B (config): the Virtues MindMapLocalization entry is a stale stub that - /// only rewrites the tree-root literal "Vertus", for 4 of 7 target languages. - /// Layer A (entity): exposes no TitleAr/TitleFa/TitleZh - /// properties (the CSV columns exist post-#590/#595 but are invisible to the mind map). - /// - /// Each test below is GREEN today and is a GUARDRAIL: it is expected to FAIL when the 3-step fix - /// documented in #601 lands (entity ar/fa/zh props + {item.TitleFr} expression + mirrored - /// config table). When that happens, flip these assertions to the localized expectation (mirroring - /// ) rather than silently deleting them — the - /// failure is the signal that the FR-frozen gap is closed. - /// - /// Additive only: no production code or existing test is modified. 0 write under Cards/ - /// (release freeze). Dispatch ai-01 2026-06-27, primary (characterization test, GO). - /// - public class VirtueMindMapFRFrozenCharacterizationTests - { - /// - /// Fresh default config — its MindMapLocalization initializer is the real production - /// entry list (4 entries: Fallacy text, Fallacy hierarchy, Virtue root title, DocumentName). - /// Constructed per-test so localization mutations never leak across tests. - /// - private static AssetConverterConfig FreshConfig => new AssetConverterConfig(); - - // ───────────────────────────────────────────────────────────────────────────── - // (1) THE HEADLINE — Virtues mind map node text is FR-frozen. The pipeline localizes a - // document config by applying every MindMapLocalization entry via - // DocumentLocalization.DoReflectionTranslate. For Fallacies this rewrites the title - // expression to {fallacy.TextZh} etc.; for Virtues it is a NO-OP — the only conversion - // that targets VirtueMindMapDocumentConfig.TitleExpression looks for the literal "Vertus" - // (the tree-root word), which is NOT present in "{item.Text}". So the expression survives - // localization untouched, and at render time TitleFunc evaluates {item.Text} = Virtue.Text - // = TitleFr = French, for every target language. This test pins that no-op for all 7 - // non-FR release languages. GUARDRAIL: fails when the #601 fix changes - // DefaultTitleExpression to {item.TitleFr} and mirrors the conversion table. - // ───────────────────────────────────────────────────────────────────────────── - - [Theory] - // The 7 non-FR release languages. (es is declared but, like ar/fa/zh, cannot fire either — - // the conversion's sourceText "Vertus" is absent from "{item.Text}" regardless of language.) - [InlineData("en")] - [InlineData("ru")] - [InlineData("pt")] - [InlineData("es")] - [InlineData("ar")] - [InlineData("fa")] - [InlineData("zh")] - public void VirtueTitleExpression_RemainsFrozenFRAfterLocalizationForEveryNonFrLang(string lang) - { - // Fresh Virtue config — its TitleExpression defaults to "{item.Text}" (see - // VirtueMindMapDocumentConfig.DefaultTitleExpression). - var config = new VirtueMindMapDocumentConfig(); - - // Apply ALL MindMapLocalization entries exactly like the pipeline does (mirrors - // MindMapLocalizationRegressionTests.FallacyExpressions_LocalizeForGapLanguages_es_ar_fa_zh). - foreach (var localization in FreshConfig.LocalizationConfig.MindMapLocalization) - { - localization.DoReflectionTranslate(config, lang); - } - - // CHARACTERIZATION: the expression is UNCHANGED — localization is a silent no-op on - // "{item.Text}". This is exactly why the rendered .content.svg carries Virtue.Text = - // TitleFr = French for every language. - config.TitleExpression.Should().Be("{item.Text}", - $"Virtue TitleExpression must stay FR-frozen ({{item.Text}}) for lang '{lang}' — the " + - $"production Virtue conversion is a root-literal stub that cannot rewrite {{item.Text}}. " + - $"When #601's fix lands ({'{'}item.TitleFr{'}'} + TitleFr→Title{lang} conversion), this " + - "assertion is EXPECTED to fail and should be updated to the localized expression."); - - // Negative pin: the expression must NOT carry any per-language title token — that is the - // signature of a Fallacies-style localization, which Virtues do not have today. - config.TitleExpression.Should().NotContain($"Title{char.ToUpper(lang[0])}{lang.Substring(1)}", - $"the FR-frozen expression must not reference a localized Title property for '{lang}' — " + - "if it did, Layer A/B would already be fixed and this characterization is stale"); - } - - // ───────────────────────────────────────────────────────────────────────────── - // (2) ROOT CAUSE — LAYER B (config): the single MindMapLocalization entry that targets Virtues - // is a stale stub. It only rewrites the tree-root literal "Vertus" (NOT a Virtue entity - // field), and only for en/ru/pt/es — ar/fa/zh are absent entirely. This documents WHY (1) - // is a no-op even for the 4 covered languages (the substring "Vertus" is not in "{item.Text}"), - // and why ar/fa/zh could not localize even if the expression matched. GUARDRAIL: fails when - // #601 step 3 replaces this stub with a TitleFr→TitleZh mirror of the Fallacy text entry. - // ───────────────────────────────────────────────────────────────────────────── - - /// - /// Locates the REAL Virtues-root DocumentLocalization from the default config. NB: it is NOT - /// located by TargetProperties.Contains(nameof(VirtueMindMapDocumentConfig.TitleExpression)), - /// because nameof(...TitleExpression) returns the bare property name "TitleExpression", - /// which is SHARED with the Fallacy text entry (#1) — that entry also lists "TitleExpression" - /// among its 5 target properties, so a name-based lookup returns #1, not #3. The unambiguous - /// discriminator is the conversion whose sourceText is the tree-root literal "Vertus", - /// which exists only in the Virtues entry. (Robust to list reordering.) - /// - private static DocumentLocalization VirtueRootTitleLocalization => - FreshConfig.LocalizationConfig.MindMapLocalization - .First(l => l.StaticConversions.Any(c => c.sourceText == "Vertus")); - - [Fact] - public void VirtueLocalizationEntry_IsStaleStub_RootLiteralOnly_4Langs_NoArFaZh() - { - var entry = VirtueRootTitleLocalization; - - // The entry exists and targets exactly the Virtue title expression. - entry.TargetProperties.Should().ContainSingle( - "the production config carries exactly one Virtue-targeting localization entry"); - - // CHARACTERIZATION: the stub has exactly ONE conversion, and it rewrites the tree-root - // LITERAL "Vertus" — not a Virtue.* entity property name. This is the root-only design - // documented by the config comment "data is FR-only, only tree root name changes" (#601 - // Layer B). It cannot localize node titles because node titles come from {item.Text}. - entry.StaticConversions.Should().ContainSingle( - "the Virtue stub is a single root-literal conversion, not a per-field table"); - var conversion = entry.StaticConversions.Single(); - conversion.sourceText.Should().Be("Vertus", - "the stub rewrites the tree-root literal 'Vertus', not a Virtue entity field — so it " + - "cannot touch node titles sourced from {item.Text}"); - - // CHARACTERIZATION: only 4 of 7 target languages are covered. ar/fa/zh are entirely - // absent — even the tree root would stay "Vertus" (French) for those three languages. - var coveredLangs = conversion.textConversions.Select(c => c.Language).ToArray(); - coveredLangs.Should().BeEquivalentTo(new[] { "en", "ru", "pt", "es" }, - "the stub covers only en/ru/pt/es — ar/fa/zh are absent, so the tree root itself stays " + - "FR for those languages today"); - coveredLangs.Should().NotContain(new[] { "ar", "fa", "zh" }, - "ar/fa/zh have no conversion at all in the Virtue stub (Layer B gap)"); - } - - // ───────────────────────────────────────────────────────────────────────────── - // (3) ROOT CAUSE — LAYER A (entity): Virtue maps only TitleFr/En/Ru/Pt/Es — there is no - // TitleAr/TitleFa/TitleZh property to bind {item.TitleAr} etc. against. Additionally the - // convenience accessor Virtue.Text hard-delegates to TitleFr, which is why {item.Text} - // (the current default expression) yields French for every language. GUARDRAIL: fails when - // #601 step 1 adds the ar/fa/zh title properties (and/or step 2 makes Text language-aware). - // ───────────────────────────────────────────────────────────────────────────── - - [Fact] - public void VirtueEntity_LacksArFaZhTitleProperties_AndTextDelegatesToFrozenFr() - { - var virtueType = typeof(Virtue); - - // CHARACTERIZATION: the ar/fa/zh title properties do NOT exist on the entity. The CSV - // columns (title_ar/title_fa/title_zh) were added by #590/#595 but are invisible to the - // mind map pipeline because nothing maps them. This is Layer A of the #601 root cause. - virtueType.GetProperty("TitleAr").Should().BeNull( - "Virtue exposes no TitleAr property — the ar CSV column is invisible to the mind map"); - virtueType.GetProperty("TitleFa").Should().BeNull( - "Virtue exposes no TitleFa property — the fa CSV column is invisible to the mind map"); - virtueType.GetProperty("TitleZh").Should().BeNull( - "Virtue exposes no TitleZh property — the zh CSV column is invisible to the mind map"); - - // Sanity: the FR/EN/RU/PT/ES title properties DO exist (so the gap is specifically ar/fa/zh, - // not a wholesale absence of localization columns). - virtueType.GetProperty("TitleFr").Should().NotBeNull("TitleFr is the FR source-of-truth"); - virtueType.GetProperty("TitleEn").Should().NotBeNull("TitleEn exists (en is mapped)"); - virtueType.GetProperty("TitleRu").Should().NotBeNull("TitleRu exists (ru is mapped)"); - virtueType.GetProperty("TitlePt").Should().NotBeNull("TitlePt exists (pt is mapped)"); - virtueType.GetProperty("TitleEs").Should().NotBeNull("TitleEs exists (es is mapped)"); - - // CHARACTERIZATION: the default expression {item.Text} resolves to TitleFr because the - // Text accessor is hard-wired to the French title. This is the direct mechanism by which - // the FR-frozen node text reaches the .content.svg. - var virtue = new Virtue { TitleFr = "Échange enrichissant" }; - virtue.Text.Should().Be("Échange enrichissant", - "Virtue.Text delegates to TitleFr — the default expression {item.Text} therefore " + - "renders the French title regardless of the target language"); - - virtue.TitleEn = "Enriching exchange"; - virtue.Text.Should().Be("Échange enrichissant", - "even when an EN translation is present, Virtue.Text ignores it — Text is not " + - "language-aware, it always returns TitleFr"); - } - } -} diff --git a/Generation/Converters/Argumentum.AssetConverter.Tests/MindmapGeneration/VirtueMindMapLocalizationTests.cs b/Generation/Converters/Argumentum.AssetConverter.Tests/MindmapGeneration/VirtueMindMapLocalizationTests.cs new file mode 100644 index 000000000..4271df01e --- /dev/null +++ b/Generation/Converters/Argumentum.AssetConverter.Tests/MindmapGeneration/VirtueMindMapLocalizationTests.cs @@ -0,0 +1,231 @@ +using System.Linq; +using Argumentum.AssetConverter; +using Argumentum.AssetConverter.Entities; +using Argumentum.AssetConverter.Mindmapper; +using FluentAssertions; +using Xunit; + +namespace Argumentum.AssetConverter.Tests.MindmapGeneration +{ + /// + /// Regression suite for the Virtues mind map localization — the successor of the #601 + /// FR-frozen characterization suite (VirtueMindMapFRFrozenCharacterizationTests). + /// + /// #601 (docs/investigations/2025-06-25-virtues-mindmap-fr-frozen-mechanism.md) root-caused + /// the Virtues mind map rendering French node text for every language as a 2-layer gap: + /// Layer B (config): the Virtue MindMapLocalization entry was a stale stub that + /// 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). + /// + /// 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. + /// + public class VirtueMindMapLocalizationTests + { + /// + /// Fresh default config — its MindMapLocalization initializer is the real production + /// entry list. Constructed per-test so localization mutations never leak across tests. + /// + private static AssetConverterConfig FreshConfig => new AssetConverterConfig(); + + /// + /// Applies EVERY production MindMapLocalization entry to a fresh Virtue document config + /// exactly as the pipeline does (mirrors + /// ). + /// + private static VirtueMindMapDocumentConfig LocalizedVirtueConfig(string lang) + { + var config = new VirtueMindMapDocumentConfig(); + foreach (var localization in FreshConfig.LocalizationConfig.MindMapLocalization) + { + localization.DoReflectionTranslate(config, lang); + } + return config; + } + + // ───────────────────────────────────────────────────────────────────────────── + // (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. + // ───────────────────────────────────────────────────────────────────────────── + + [Theory] + // lang, expectedTitle, expectedDescription + [InlineData("en", "TitleEn", "DescriptionEn")] + [InlineData("ru", "TitleRu", "DescriptionRu")] + [InlineData("pt", "TitlePt", "DescriptionPt")] + [InlineData("es", "TitleEs", "DescriptionEs")] + public void VirtueTitleAndDescription_LocalizeForWiredLanguages_en_ru_pt_es( + 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)"); + config.TitleExpression.Should().NotContain("TitleFr", + $"the FR source token must be fully rewritten for wired lang '{lang}'"); + + config.DescriptionExpression.Should().Contain(expectedDescription, + $"the Virtue description expression must reference {expectedDescription} for wired lang '{lang}'"); + config.DescriptionExpression.Should().NotContain("DescriptionFr", + $"the FR description token must be fully rewritten for wired 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. + // ───────────────────────────────────────────────────────────────────────────── + + [Theory] + // lang, expectedFamily, expectedSubfamily, expectedSubsubfamily + [InlineData("en", "FamilyEn", "SubfamilyEn", "SubsubfamilyEn")] + [InlineData("ru", "FamilyRu", "SubfamilyRu", "SubsubfamilyRu")] + [InlineData("pt", "FamilyPt", "SubfamilyPt", "SubsubfamilyPt")] + [InlineData("es", "FamilyEs", "SubfamilyEs", "SubsubfamilyEs")] + public void VirtueFamilyHierarchy_LocalizesForWiredLanguages_en_ru_pt_es( + 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}'"); + config.SousFamilleExpression.Should().Be($"{{item.{expectedSubfamily}}}", + $"SousFamille must localize to {expectedSubfamily} for wired lang '{lang}'"); + config.SoussousFamilleExpression.Should().Be($"{{item.{expectedSubsubfamily}}}", + $"Soussousfamille must localize to {expectedSubsubfamily} for wired 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. + config.SoussousFamilleExpression.Should().NotContain("SubfamilyFr", + "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}'"); + } + + // ───────────────────────────────────────────────────────────────────────────── + // (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. + // ───────────────────────────────────────────────────────────────────────────── + + [Theory] + [InlineData("ar")] + [InlineData("fa")] + [InlineData("zh")] + public void VirtueExpressions_StayFrForDeferredLanguages_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}'"); + } + + // ───────────────────────────────────────────────────────────────────────────── + // (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. + // ───────────────────────────────────────────────────────────────────────────── + + [Fact] + public void VirtueLocalizationEntries_MirrorFallacyTables_NotStaleStub() + { + var mindMapLoc = FreshConfig.LocalizationConfig.MindMapLocalization; + + // The Virtue title/description entry: real per-field table, sourced from Virtue.TitleFr. + var titleEntry = mindMapLoc.Single(l => + l.TargetProperties.Contains(nameof(VirtueMindMapDocumentConfig.TitleExpression)) + && l.StaticConversions.Any(c => c.sourceText == nameof(Virtue.TitleFr))); + titleEntry.TargetProperties.Should().Contain(nameof(VirtueMindMapDocumentConfig.DescriptionExpression), + "the Virtue text entry also localizes the description expression"); + + // The Virtue family-hierarchy entry: sourced from Virtue.SubsubfamilyFr (most specific). + var familyEntry = mindMapLoc.Single(l => + l.TargetProperties.Contains(nameof(VirtueMindMapDocumentConfig.FamilleExpression)) + && l.StaticConversions.Any(c => c.sourceText == nameof(Virtue.SubsubfamilyFr))); + familyEntry.TargetProperties.Should().BeEquivalentTo(new[] + { + nameof(VirtueMindMapDocumentConfig.FamilleExpression), + nameof(VirtueMindMapDocumentConfig.SousFamilleExpression), + 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). + 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"); + } + } + + // The retired stale stub: no Virtue localization rewrites the tree-root literal "Vertus". + mindMapLoc.Should().NotContain( + l => l.StaticConversions.Any(c => c.sourceText == "Vertus"), + "the #601 root-literal 'Vertus' stub is superseded by the per-field tables"); + } + + // ───────────────────────────────────────────────────────────────────────────── + // (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. + // ───────────────────────────────────────────────────────────────────────────── + + [Fact] + public void VirtueEntity_WiresEnRuPtEs_DefersArFaZh() + { + var virtueType = typeof(Virtue); + + // Wired languages have their title property (so Layer A is present for en/ru/pt/es). + 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. + 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 dc5abab08..a4be0e8eb 100644 --- a/Generation/Converters/Argumentum.AssetConverter/AssetConverterConfig.cs +++ b/Generation/Converters/Argumentum.AssetConverter/AssetConverterConfig.cs @@ -238,14 +238,32 @@ 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 root title translation (data is FR-only, only tree root name changes) + // 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. new DocumentLocalization(){ TargetProperties = new List(new [] { - nameof(VirtueMindMapDocumentConfig.TitleExpression) + nameof(VirtueMindMapDocumentConfig.TitleExpression), + nameof(VirtueMindMapDocumentConfig.DescriptionExpression), }), StaticConversions = new List<(string sourceText, List<(string Language, string destText)> textConversions)>(new[]{ - ("Vertus", new List<(string Language, string destText)>(new []{("en", "Virtues"), ("ru", "Dobrodeteli"), ("pt", "Virtudes"), ("es", "Virtudes") }) ) + (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))}) ), + }), + }, + // Virtue family hierarchy: FR names → localized names. + // Order: most specific first (Subsubfamily > Subfamily > Family) to avoid partial matches. + new DocumentLocalization(){ + TargetProperties = new List(new [] + { + nameof(VirtueMindMapDocumentConfig.FamilleExpression), + nameof(VirtueMindMapDocumentConfig.SousFamilleExpression), + 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))}) ), }), }, // Document name: _fr. → _en./_ru./_pt. diff --git a/Generation/Converters/Argumentum.AssetConverter/Mindmapper/VirtueMindMapDocumentConfig.cs b/Generation/Converters/Argumentum.AssetConverter/Mindmapper/VirtueMindMapDocumentConfig.cs index b3fd645fe..ed163d69d 100644 --- a/Generation/Converters/Argumentum.AssetConverter/Mindmapper/VirtueMindMapDocumentConfig.cs +++ b/Generation/Converters/Argumentum.AssetConverter/Mindmapper/VirtueMindMapDocumentConfig.cs @@ -34,7 +34,8 @@ public class VirtueMindMapDocumentConfig : VirtueDocumentConfigBase private static readonly System.Diagnostics.StackTrace temp1 = new(); private static readonly System.Drawing.Color temp2 = Color.AliceBlue; - const string DefaultTitleExpression = @"{item.Text}"; + // #636 §2: reference the raw FR-suffixed property so MindMapLocalization StaticConversions can rewrite it per language (TitleFr→TitleEn/Ru/Pt/Es). FR is a no-op (item.Text => TitleFr). + const string DefaultTitleExpression = @"{item.TitleFr}"; public string TitleExpression { get; set; } = DefaultTitleExpression; @@ -61,7 +62,7 @@ public Func TitleFunc public bool AddNodePath { get; set; } = false; - const string DefaultFamilleExpression = @"{item.Family}"; + const string DefaultFamilleExpression = @"{item.FamilyFr}"; public string FamilleExpression { get; set; } = DefaultFamilleExpression; [IgnoreDataMember] @@ -75,7 +76,7 @@ public Func FamilleFunc } - const string DefaultSousFamilleExpression = @"{item.SubFamily}"; + const string DefaultSousFamilleExpression = @"{item.SubfamilyFr}"; public string SousFamilleExpression { get; set; } = DefaultSousFamilleExpression; [IgnoreDataMember] @@ -89,7 +90,7 @@ public Func SousFamilleFunc } - const string DefaultSoussousFamilleExpression = @"{item.SubSubFamily}"; + const string DefaultSoussousFamilleExpression = @"{item.SubsubfamilyFr}"; public string SoussousFamilleExpression { get; set; } = DefaultSoussousFamilleExpression; [IgnoreDataMember] @@ -105,7 +106,7 @@ public Func SoussousFamilleFunc public string DescriptionExpression { get; set; } = @"

- {HttpUtility.HtmlEncode(item.Description)} + {HttpUtility.HtmlEncode(item.DescriptionFr)}

";