using System.Globalization; using MeterVault.App.Localization; using MeterVault.Core.Domain; namespace MeterVault.Integration.Tests.Localization; /// /// Every domain enum value the UI renders has wording in every language (SDD §12, M7). /// /// /// ends each mapping with a fallback arm that returns the bare identifier, /// so adding an enum value can never throw mid-render on a dashboard. The cost of that safety is that /// a forgotten value degrades silently to English — these tests are what make it loud instead, by /// demanding a matching Enum_<Type>_<Value> resource for every declared value. /// public sealed class EnumDisplayNameTests { [Fact] public void Every_localized_enum_value_has_a_resource_in_every_language() { var missing = new List(); foreach (var type in DisplayNames.LocalizedEnums) { foreach (var name in Enum.GetNames(type)) { // None is the empty bitmask, deliberately rendered as nothing at all. if (type == typeof(ReadingFlags) && name == nameof(ReadingFlags.None)) { continue; } var key = $"Enum_{type.Name}_{name}"; foreach (var culture in Loc.SupportedCultures) { var value = Strings.ResourceManager.GetString(key, CultureInfo.GetCultureInfo(culture)); if (string.IsNullOrWhiteSpace(value)) { missing.Add($"{key} [{culture}]"); } } } } Assert.True(missing.Count == 0, $"Missing enum display names: {string.Join(", ", missing)}"); } [Fact] public void Display_names_are_translated_rather_than_echoing_the_identifier() { // Not every value can differ — "Tasmota", "Bonus" and "Global" are the same word in German — // but if a whole enum came back as its own identifiers, the mapping was never written. var untranslated = new List(); foreach (var type in DisplayNames.LocalizedEnums) { var names = Enum.GetNames(type); var translated = 0; foreach (var name in names) { var german = Strings.ResourceManager.GetString($"Enum_{type.Name}_{name}", CultureInfo.GetCultureInfo("de")); if (german is not null && !string.Equals(german, name, StringComparison.Ordinal)) { translated++; } } if (translated == 0) { untranslated.Add(type.Name); } } Assert.True(untranslated.Count == 0, $"Enums with no German wording at all: {string.Join(", ", untranslated)}"); } [Theory] [InlineData("en", "Consumption")] [InlineData("de", "Verbrauch")] public void The_reader_s_language_decides_the_wording(string culture, string expected) => Assert.Equal(expected, WithUiCulture(culture, () => ConsumptionKind.Consumption.Display())); [Fact] public void A_bitmask_lists_the_flags_it_actually_carries() { // Blank rather than "None": the readings table has a flag column that is empty on almost // every row, and printing a word down the whole page is noise, not information. Assert.Equal(string.Empty, ReadingFlags.None.Display()); var both = WithUiCulture("de", () => (ReadingFlags.CounterReset | ReadingFlags.MeterSwap).Display()); Assert.Equal("Zählerreset, Zählerwechsel", both); Assert.Equal("Meter swap", WithUiCulture("en", () => ReadingFlags.MeterSwap.Display())); } [Fact] public void An_undeclared_enum_value_degrades_to_its_identifier_instead_of_throwing() { // Guards the fallback arm itself: a value cast in from the database (or a future migration) // must not take a dashboard down. var unknown = (MeterMode)999; Assert.Equal("999", unknown.Display()); } private static T WithUiCulture(string culture, Func body) { var previous = CultureInfo.CurrentUICulture; try { CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo(culture); return body(); } finally { CultureInfo.CurrentUICulture = previous; } } }