Skip to content

Commit 378e858

Browse files
committed
Add multi-base resource group support to the TUI editor
The Terminal.Gui editor assumed a flat single-group model: it crashed on startup with a multi-group directory (duplicate DataTable column names), showed only the first group's keys, and silently wrote edits to the wrong group's file. - Data model: entries are tracked per group (EntryReference.BaseName); the table builds one column per distinct culture code plus a Group column, and loads keys from every group's default file. Hidden _BaseName column makes row->entry mapping group-aware. - Operations are scoped to a key's group: edit, plural edit, add, delete, merge, translate, copy/paste and add/remove language all resolve target files by (BaseName, Code) instead of by Code alone. Add Language creates a file for every group; Remove Language removes a culture across all groups. - Default-language labels use IsDefault and the configured default code (e.g. "it (Default)") instead of assuming an empty code, matching this session's core fix. - Extra-key and duplicate detection run per group. Adds headless FakeDriver smoke tests covering multi-group and single-group construction (the previously-crashing path).
1 parent 7ccf967 commit 378e858

8 files changed

Lines changed: 676 additions & 254 deletions
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright (c) 2025 Nikolaos Protopapas
2+
// Licensed under the MIT License
3+
4+
using LocalizationManager.Core.Backends.Resx;
5+
using LocalizationManager.Core.Models;
6+
using LocalizationManager.UI;
7+
using Terminal.Gui;
8+
using Xunit;
9+
10+
namespace LocalizationManager.Tests.IntegrationTests;
11+
12+
/// <summary>
13+
/// Smoke tests for the TUI editor against multi-base resource group directories.
14+
/// Before multi-group support, constructing the editor with two groups that share a
15+
/// culture code crashed (duplicate DataTable column names). These tests build a real
16+
/// multi-group directory and confirm the window constructs without throwing.
17+
/// </summary>
18+
[Collection("Tui")]
19+
public class TuiMultiGroupSmokeTests : IDisposable
20+
{
21+
private readonly string _tempDir;
22+
private readonly ResxResourceDiscovery _discovery = new("it");
23+
private readonly ResxResourceReader _reader = new();
24+
25+
public TuiMultiGroupSmokeTests()
26+
{
27+
_tempDir = Path.Combine(Path.GetTempPath(), $"TuiMultiGroup_{Guid.NewGuid()}");
28+
Directory.CreateDirectory(_tempDir);
29+
}
30+
31+
public void Dispose()
32+
{
33+
try { Application.Shutdown(); } catch { /* not initialized */ }
34+
if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, true);
35+
}
36+
37+
private void WriteResx(string fileName, params (string Key, string Value)[] entries)
38+
{
39+
var body = string.Join("\n", entries.Select(e =>
40+
$" <data name=\"{e.Key}\" xml:space=\"preserve\"><value>{e.Value}</value></data>"));
41+
var content =
42+
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n" +
43+
" <resheader name=\"resmimetype\"><value>text/microsoft-resx</value></resheader>\n" +
44+
" <resheader name=\"version\"><value>2.0</value></resheader>\n" +
45+
" <resheader name=\"reader\"><value>System.Resources.ResXResourceReader</value></resheader>\n" +
46+
" <resheader name=\"writer\"><value>System.Resources.ResXResourceWriter</value></resheader>\n" +
47+
body + "\n</root>\n";
48+
File.WriteAllText(Path.Combine(_tempDir, fileName), content);
49+
}
50+
51+
private List<ResourceFile> DiscoverAndRead()
52+
{
53+
var languages = _discovery.DiscoverLanguages(_tempDir);
54+
return languages.Select(l => _reader.Read(l)).ToList();
55+
}
56+
57+
[Fact]
58+
public void Constructor_MultipleGroupsSharingCulture_DoesNotThrow()
59+
{
60+
// Two groups, each with a default + Italian file → same culture code "it" appears twice.
61+
WriteResx("CustomerResources.resx", ("Customer_Name", "Name"));
62+
WriteResx("CustomerResources.it.resx", ("Customer_Name", "Nome"));
63+
WriteResx("GlassResources.resx", ("Glass_Width", "Width"));
64+
WriteResx("GlassResources.it.resx", ("Glass_Width", "Larghezza"));
65+
66+
var resourceFiles = DiscoverAndRead();
67+
var backend = new ResxResourceBackend("it");
68+
69+
Application.Init(new FakeDriver());
70+
try
71+
{
72+
// The window construction is what used to crash with a DuplicateNameException.
73+
var window = new ResourceEditorWindow(resourceFiles, backend, "it", _tempDir);
74+
Assert.NotNull(window);
75+
}
76+
finally
77+
{
78+
Application.Shutdown();
79+
}
80+
}
81+
82+
[Fact]
83+
public void Constructor_SingleGroup_DoesNotThrow()
84+
{
85+
WriteResx("Resources.resx", ("Hello", "Hello"));
86+
WriteResx("Resources.it.resx", ("Hello", "Ciao"));
87+
88+
var resourceFiles = DiscoverAndRead();
89+
var backend = new ResxResourceBackend("it");
90+
91+
Application.Init(new FakeDriver());
92+
try
93+
{
94+
var window = new ResourceEditorWindow(resourceFiles, backend, "it", _tempDir);
95+
Assert.NotNull(window);
96+
}
97+
finally
98+
{
99+
Application.Shutdown();
100+
}
101+
}
102+
}

UI/ResourceEditorWindow/ResourceEditorWindow.BatchOperations.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ private void BulkDelete()
254254
// Delete each selected entry
255255
foreach (var entryRef in selectedEntries)
256256
{
257-
DeleteSpecificOccurrence(entryRef.Key, entryRef.OccurrenceNumber);
257+
DeleteSpecificOccurrence(entryRef.Key, entryRef.OccurrenceNumber, entryRef.BaseName);
258258
}
259259

260260
_hasUnsavedChanges = true;

0 commit comments

Comments
 (0)