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 b6e27fd6..00000000
--- 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 00000000..4271df01
--- /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 dc5abab0..a4be0e8e 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 b3fd645f..ed163d69 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; } =
@"