From 45f6ecba1d0564cffd30cd367b31d40a4f878870 Mon Sep 17 00:00:00 2001 From: jsboige Date: Sun, 7 Jun 2026 01:54:36 +0200 Subject: [PATCH] =?UTF-8?q?fix(i18n):=20localize=20M=C3=A9mo=20Back=20taxo?= =?UTF-8?q?nomy=20labels=20EN/RU/PT=20(#443=20follow-up)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Mémo Back card kept its taxonomy labels (Famille / Sous-Famille / Soussousfamille) in French for EN/RU/PT because the Memo BackFieldConversions only carried tagline_fr. At runtime the Back is rendered through BackFieldConversions (TranslateCardSetInfo, front:false), so its taxonomy display tokens were never localized — only the subtitle (via StaticConversions) and the Face (via FrontFieldConversions) were. Add Soussousfamille/Sous-Famille/Famille → Family columns to the Memo BackFieldConversions, most-specific-first so "Famille}}" cannot clobber "Sous-Famille}}". Safe for the FR-invariant ifCond family selector: FormatField appends "}}" with NO space, so it matches the {{Famille}} display tokens but not the `Famille "=="` operand nor {{Famille_camelCase}} CSS class — the data-driven 8/8 family grouping stays untouched. Adds FallaciesLocalizationTests regression coverage that applies BackFieldConversions (not Front) to the real Memo Back template for EN/RU/PT, asserting taxonomy tokens localize while the grouping selector stays FR, plus a config-shape ordering test. Full suite: 155 pass / 0 fail. Refs #358 #435 #443 Co-Authored-By: Claude Opus 4.8 --- .../FallaciesLocalizationTests.cs | 78 +++++++++++++++++++ .../AssetConverterConfig.cs | 8 ++ 2 files changed, 86 insertions(+) diff --git a/Generation/Converters/Argumentum.AssetConverter.Tests/Localization/FallaciesLocalizationTests.cs b/Generation/Converters/Argumentum.AssetConverter.Tests/Localization/FallaciesLocalizationTests.cs index dbe493dcc..a98f1d537 100644 --- a/Generation/Converters/Argumentum.AssetConverter.Tests/Localization/FallaciesLocalizationTests.cs +++ b/Generation/Converters/Argumentum.AssetConverter.Tests/Localization/FallaciesLocalizationTests.cs @@ -58,6 +58,21 @@ private static string ApplyFrontSubstitution(CardSetLocalization loc, string tem return template; } + // Mirrors the Back branch of CardSetLocalization.TranslateCardSetInfo (front:false) — at runtime + // the Memo Back card is localized through BackFieldConversions, NOT FrontFieldConversions. + private static string ApplyBackSubstitution(CardSetLocalization loc, string template, string destLang) + { + foreach (var fieldConversion in loc.BackFieldConversions) + { + var sourcePattern = loc.FormatField(fieldConversion.sourceFieldName); + var conv = fieldConversion.fieldConversions.FirstOrDefault(c => c.Language == destLang); + if (string.IsNullOrEmpty(conv.destFieldName)) continue; + var destPattern = loc.FormatField(conv.destFieldName); + template = template.Replace(sourcePattern, destPattern); + } + return template; + } + [Theory] [InlineData("Cards/Fallacies/Argumentum_Fallacies_Face_fr.json", "en")] [InlineData("Cards/Fallacies/Argumentum_Fallacies_Face_fr.json", "ru")] @@ -150,6 +165,69 @@ public void Memo_Back_Template_Subtitle_Is_Translated_And_Selector_Stays_FR(stri $"{destLang} Memo Back ifCond must keep text_fr (FR-invariant selector for family grouping)"); } + [Theory] + [InlineData("en", "Family", "Subfamily", "Subsubfamily")] + [InlineData("ru", "Family_ru", "Subfamily_ru", "Subsubfamily_ru")] + [InlineData("pt", "Family_pt", "Subfamily_pt", "Subsubfamily_pt")] + public void Memo_Back_Taxonomy_Display_Tokens_Are_Localized_While_Grouping_Selector_Stays_FR( + string destLang, string family, string subfamily, string subsubfamily) + { + // Regression test for the #358/#435/#443 follow-up — the Memo Back card kept its taxonomy + // labels in French (Famille / Sous-Famille / Soussousfamille) in EN/RU/PT because the Memo + // BackFieldConversions only carried tagline_fr. At runtime the Back is rendered through + // BackFieldConversions (TranslateCardSetInfo, front:false), so the taxonomy DISPLAY tokens + // must be localized there — while the FR-invariant ifCond family selector (Famille == text_fr) + // must stay untouched so the 8 families still group correctly. + var original = ReadTemplate("Cards/Memo/Argumentum_Memo_Back_fr.json"); + var loc = GetFallaciesLocalization(); + + var translated = ApplyBackSubstitution(loc, original, destLang); + translated = loc.DoStaticConversions(translated, destLang); + + // (a) Display tokens localized to the real CSV columns (casing matters — CsvHelper is case-sensitive). + translated.Should().Contain("{{" + family + "}}", $"{destLang} Back must bind family label to CSV column '{family}'"); + translated.Should().Contain("{{" + subfamily + "}}", $"{destLang} Back must bind subfamily label to CSV column '{subfamily}'"); + translated.Should().Contain("{{" + subsubfamily + "}}", $"{destLang} Back must bind subsubfamily label to CSV column '{subsubfamily}'"); + + // (b) French display tokens must be gone. + translated.Should().NotContain("{{Famille}}", $"{destLang} Back family label must no longer be the FR token"); + translated.Should().NotContain("{{Sous-Famille}}", $"{destLang} Back subfamily label must no longer be the FR token"); + translated.Should().NotContain("{{Soussousfamille}}", $"{destLang} Back subsubfamily label must no longer be the FR token"); + + // (c) The FR-invariant grouping selector must survive: ifCond keeps Famille == text_fr. + // NB: this template is read raw from the .json (no JSON-unescape), so the operator quotes + // appear escaped on disk as \"==\" — assert against that on-disk form. + translated.Should().Contain("Famille \\\"==\\\"", $"{destLang} Back ifCond family operand must stay FR (data-driven grouping)"); + translated.Should().Contain("text_fr ", $"{destLang} Back ifCond must keep text_fr (FR-invariant selector)"); + + // (d) The CSS colour class binding ({{Famille_camelCase}}) must stay intact. + translated.Should().Contain("Famille_camelCase", $"{destLang} Back CSS colour class binding must be preserved"); + + // (e) Subtitle still localized via StaticConversions. + translated.Should().NotContain("L'art de jamais avoir tort", $"{destLang} Back subtitle must be translated (#358)"); + } + + [Fact] + public void Memo_Back_Conversions_Include_Taxonomy_Ordered_MostSpecificFirst() + { + var loc = GetFallaciesLocalization(); + var names = loc.BackFieldConversions.Select(c => c.sourceFieldName).ToList(); + + names.Should().Contain("Soussousfamille", "Memo Back must localize the subsubfamily label (#443 follow-up)"); + names.Should().Contain("Sous-Famille", "Memo Back must localize the subfamily label (#443 follow-up)"); + names.Should().Contain("Famille", "Memo Back must localize the family label (#443 follow-up)"); + names.Should().Contain("tagline_fr", "the original tagline mapping must be preserved"); + + var soussousIndex = names.IndexOf("Soussousfamille"); + var sousIndex = names.IndexOf("Sous-Famille"); + var familleIndex = names.IndexOf("Famille"); + + soussousIndex.Should().BeLessThan(sousIndex, + "Soussousfamille must precede Sous-Famille so 'Famille}}' does not clobber 'Sous-Famille}}'"); + sousIndex.Should().BeLessThan(familleIndex, + "Sous-Famille must precede Famille to prevent partial overlap"); + } + [Fact] public void Rules_Localization_Is_Configured_For_All_Target_Languages() { diff --git a/Generation/Converters/Argumentum.AssetConverter/AssetConverterConfig.cs b/Generation/Converters/Argumentum.AssetConverter/AssetConverterConfig.cs index 15af033ed..c0103cb89 100644 --- a/Generation/Converters/Argumentum.AssetConverter/AssetConverterConfig.cs +++ b/Generation/Converters/Argumentum.AssetConverter/AssetConverterConfig.cs @@ -116,6 +116,14 @@ public class AssetConverterConfig ("example_fr", new List<(string Language, string destFieldName)>(new []{("en", "example_en"), ("ru", "example_ru"), ("pt", "example_pt"), ("es", "example_es"), ("ar", "example_ar"), ("fa", "example_fa"), ("zh", "example_zh") }) ), }), BackFieldConversions = new List<(string sourceFieldName, List<(string Language, string destFieldName)> fieldConversions)>(new []{ + // Memo Back renders the SAME taxonomy display tokens as the Face ({{Famille}}/{{Sous-Famille}}/{{Soussousfamille}}) — #358/#435/#443 follow-up. + // Without these the Back stayed FR in EN/RU/PT (only the subtitle localized via StaticConversions). Order: most specific first + // (Soussousfamille > Sous-Famille > Famille) so "Famille}}" does not clobber the longer "Sous-Famille}}". + // SAFE for the FR-invariant family selector: FormatField appends "}}" with NO space, so it matches {{Famille}} but NOT the + // ifCond operand `Famille "=="` (space before "==") nor {{Famille_camelCase}} (CSS colour class). Grouping stays data-driven (8/8). + ("Soussousfamille", new List<(string Language, string destFieldName)>(new []{("en", "Subsubfamily"), ("ru", "Subsubfamily_ru"), ("pt", "Subsubfamily_pt"), ("es", "Subsubfamily_es"), ("ar", "Subsubfamily_ar"), ("fa", "Subsubfamily_fa"), ("zh", "Subsubfamily_zh") }) ), + ("Sous-Famille", new List<(string Language, string destFieldName)>(new []{("en", "Subfamily"), ("ru", "Subfamily_ru"), ("pt", "Subfamily_pt"), ("es", "Subfamily_es"), ("ar", "Subfamily_ar"), ("fa", "Subfamily_fa"), ("zh", "Subfamily_zh") }) ), + ("Famille", new List<(string Language, string destFieldName)>(new []{("en", "Family"), ("ru", "Family_ru"), ("pt", "Family_pt"), ("es", "Family_es"), ("ar", "Family_ar"), ("fa", "Family_fa"), ("zh", "Family_zh") }) ), ("tagline_fr", new List<(string Language, string destFieldName)>(new []{("en", "tagline_en"), ("ru", "tagline_ru"), ("pt", "tagline_pt"), ("es", "tagline_es"), ("ar", "tagline_ar"), ("fa", "tagline_fa"), ("zh", "tagline_zh") }) ), }), // StaticConversions: handle hardcoded FR text in Memo templates that FrontFieldConversions