Skip to content

Commit eb9185a

Browse files
committed
Merge issue #6 round-3 fixes: default-language merge, scanner false positives, VS Code Add-Key, @Inject detection
2 parents 7c4727e + efe3e2e commit eb9185a

29 files changed

Lines changed: 2942 additions & 148 deletions

Commands/WebCommand.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,10 @@ public override int Execute(CommandContext context, Settings settings, Cancellat
197197
return new JsonResourceBackend(config.Json);
198198

199199
if (!string.IsNullOrEmpty(format))
200-
return factory.GetBackend(format);
200+
return factory.GetBackend(format, config);
201201

202-
// Auto-detect from path
203-
return factory.ResolveFromPath(absoluteResourcePath);
202+
// Auto-detect from path (pass config so DefaultLanguageCode is honored)
203+
return factory.ResolveFromPath(absoluteResourcePath, config);
204204
});
205205

206206
// Configure CORS if enabled in configuration

Controllers/ResourcesController.cs

Lines changed: 92 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,36 @@ public ResourcesController(IConfiguration configuration, IResourceBackend backen
2323
}
2424

2525
/// <summary>
26-
/// List all resource files
26+
/// List the language columns shown in the editor. Returns one entry per
27+
/// EFFECTIVE language code across all resource groups (default file and an
28+
/// explicit culture file sharing the configured DefaultLanguageCode collapse
29+
/// into a single column), so the headers agree with the merged cells from
30+
/// <see cref="GetAllKeys"/>. A within-group default-vs-culture collision is
31+
/// surfaced via <see cref="ResourceFileInfo.HasLanguageConflict"/>; the
32+
/// legitimate cross-group case (two groups each having an "it" file) is NOT
33+
/// treated as a conflict.
2734
/// </summary>
2835
[HttpGet]
2936
public ActionResult<IEnumerable<ResourceFileInfo>> GetResources()
3037
{
3138
try
3239
{
33-
var languages = _backend.Discovery.DiscoverLanguages(_resourcePath);
34-
var result = languages.Select(l => new ResourceFileInfo
40+
var directory = _backend.Discovery.DiscoverResourceGroups(_resourcePath);
41+
var allFiles = directory.Groups.SelectMany(g => g.Files).ToList();
42+
var columns = MergedLanguageColumns.Build(allFiles);
43+
44+
var result = columns.Select(col => new ResourceFileInfo
3545
{
36-
FileName = l.Name,
37-
FilePath = l.FilePath,
38-
Code = l.Code,
39-
IsDefault = l.IsDefault
46+
FileName = col.Name,
47+
FilePath = col.WinningFilePath,
48+
Code = col.Code,
49+
IsDefault = col.IsDefault,
50+
// Cross-group merge would falsely flag two legit same-code files
51+
// (e.g. CustomerResources.it + GlassResources.it). Only flag a
52+
// conflict when a SINGLE group has >1 file for this effective code.
53+
HasLanguageConflict = directory.Groups.Any(g =>
54+
g.Files.Count(f => MergedLanguageColumns.EffectiveCode(f) == col.Code) > 1),
55+
ConflictingFilePaths = col.ConflictingFilePaths.ToList()
4056
});
4157
return Ok(result);
4258
}
@@ -72,16 +88,33 @@ public ActionResult<IEnumerable<ResourceKeyInfo>> GetAllKeys()
7288
.OrderBy(k => k)
7389
.ToList();
7490

91+
var fileByPath = group.Files.ToDictionary(f => f.FilePath ?? string.Empty);
92+
var columns = MergedLanguageColumns.Build(group.Files);
93+
7594
foreach (var key in keys)
7695
{
7796
var values = new Dictionary<string, string?>();
7897
var isPlural = false;
98+
var conflictCodes = new List<string>();
7999

80-
foreach (var (file, resource) in resources)
100+
foreach (var col in columns)
81101
{
82-
var entry = resource.Entries.FirstOrDefault(e => e.Key == key);
83-
values[string.IsNullOrEmpty(file.Code) ? "default" : file.Code] = entry?.Value;
84-
if (entry?.IsPlural == true) isPlural = true;
102+
var winnerEntry = resources[fileByPath[col.WinningFilePath]].Entries.FirstOrDefault(e => e.Key == key);
103+
var resolvedEntry = winnerEntry;
104+
105+
// default wins; culture files fill the gap only when the winner has no value.
106+
if (string.IsNullOrEmpty(resolvedEntry?.Value))
107+
{
108+
foreach (var lp in col.ConflictingFilePaths)
109+
{
110+
var le = resources[fileByPath[lp]].Entries.FirstOrDefault(e => e.Key == key);
111+
if (!string.IsNullOrEmpty(le?.Value)) { resolvedEntry = le; break; }
112+
}
113+
}
114+
115+
values[col.Code] = resolvedEntry?.Value;
116+
if (resolvedEntry?.IsPlural == true) isPlural = true;
117+
if (col.HasConflict) conflictCodes.Add(col.Code);
85118
}
86119

87120
var occurrenceCount = defaultResource?.Entries.Count(e => e.Key == key) ?? 1;
@@ -93,7 +126,9 @@ public ActionResult<IEnumerable<ResourceKeyInfo>> GetAllKeys()
93126
Values = values,
94127
OccurrenceCount = occurrenceCount,
95128
HasDuplicates = occurrenceCount > 1,
96-
IsPlural = isPlural
129+
IsPlural = isPlural,
130+
HasLanguageConflict = conflictCodes.Count > 0,
131+
ConflictingLanguages = conflictCodes
97132
});
98133
}
99134
}
@@ -158,22 +193,49 @@ public ActionResult<ResourceKeyDetails> GetKey(string keyName, [FromQuery] strin
158193

159194
var hasDuplicates = occurrences.Count > 1;
160195

196+
// Merge files of this group into display columns so that the suffix-less
197+
// default file and an explicit culture file sharing the same effective code
198+
// collapse into one column (default wins; cultures only fill gaps).
199+
var byPath = resourceFiles.ToDictionary(rf => rf.Language.FilePath ?? string.Empty);
200+
var columns = MergedLanguageColumns.Build(group.Files);
201+
202+
// Resolves the i-th occurrence of keyName for a column, taking the default
203+
// winner first and falling back to a colliding culture file only when the
204+
// winner lacks that occurrence.
205+
ResourceValue? ResolveOccurrence(LanguageColumn col, int index)
206+
{
207+
var winnerEntries = byPath[col.WinningFilePath].Entries.Where(e => e.Key == keyName).ToList();
208+
var entry = index < winnerEntries.Count ? winnerEntries[index] : null;
209+
210+
if (entry == null)
211+
{
212+
foreach (var lp in col.ConflictingFilePaths)
213+
{
214+
var loserEntries = byPath[lp].Entries.Where(e => e.Key == keyName).ToList();
215+
if (index < loserEntries.Count) { entry = loserEntries[index]; break; }
216+
}
217+
}
218+
219+
if (entry == null) return null;
220+
return new ResourceValue
221+
{
222+
Value = entry.Value,
223+
Comment = entry.Comment,
224+
IsPlural = entry.IsPlural,
225+
PluralForms = entry.PluralForms
226+
};
227+
}
228+
161229
// If no duplicates, return simple response
162230
if (!hasDuplicates)
163231
{
164232
var values = new Dictionary<string, ResourceValue>();
165-
foreach (var file in resourceFiles)
233+
foreach (var col in columns)
166234
{
167-
var entry = file.Entries.FirstOrDefault(e => e.Key == keyName);
168-
if (entry != null)
235+
var resolved = ResolveOccurrence(col, 0);
236+
if (resolved != null)
169237
{
170-
values[file.Language.Code ?? "default"] = new ResourceValue
171-
{
172-
Value = entry.Value,
173-
Comment = entry.Comment,
174-
IsPlural = entry.IsPlural,
175-
PluralForms = entry.PluralForms
176-
};
238+
values[col.Code] = resolved;
177239
}
178240
}
179241

@@ -192,18 +254,12 @@ public ActionResult<ResourceKeyDetails> GetKey(string keyName, [FromQuery] strin
192254
for (int i = 0; i < occurrences.Count; i++)
193255
{
194256
var occurrenceValues = new Dictionary<string, ResourceValue>();
195-
foreach (var file in resourceFiles)
257+
foreach (var col in columns)
196258
{
197-
var entries = file.Entries.Where(e => e.Key == keyName).ToList();
198-
if (i < entries.Count)
259+
var resolved = ResolveOccurrence(col, i);
260+
if (resolved != null)
199261
{
200-
occurrenceValues[file.Language.Code ?? "default"] = new ResourceValue
201-
{
202-
Value = entries[i].Value,
203-
Comment = entries[i].Comment,
204-
IsPlural = entries[i].IsPlural,
205-
PluralForms = entries[i].PluralForms
206-
};
262+
occurrenceValues[col.Code] = resolved;
207263
}
208264
}
209265

@@ -216,18 +272,12 @@ public ActionResult<ResourceKeyDetails> GetKey(string keyName, [FromQuery] strin
216272

217273
// Return first occurrence in Values for backward compatibility
218274
var firstValues = new Dictionary<string, ResourceValue>();
219-
foreach (var file in resourceFiles)
275+
foreach (var col in columns)
220276
{
221-
var entry = file.Entries.FirstOrDefault(e => e.Key == keyName);
222-
if (entry != null)
277+
var resolved = ResolveOccurrence(col, 0);
278+
if (resolved != null)
223279
{
224-
firstValues[file.Language.Code ?? "default"] = new ResourceValue
225-
{
226-
Value = entry.Value,
227-
Comment = entry.Comment,
228-
IsPlural = entry.IsPlural,
229-
PluralForms = entry.PluralForms
230-
};
280+
firstValues[col.Code] = resolved;
231281
}
232282
}
233283

Controllers/ScanController.cs

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Microsoft.AspNetCore.Mvc;
55
using LocalizationManager.Core;
66
using LocalizationManager.Core.Abstractions;
7+
using LocalizationManager.Core.Configuration;
78
using LocalizationManager.Core.Scanning;
89
using LocalizationManager.Models.Api;
910

@@ -16,13 +17,15 @@ public class ScanController : ControllerBase
1617
private readonly string _resourcePath;
1718
private readonly string _sourcePath;
1819
private readonly IResourceBackend _backend;
20+
private readonly ConfigurationService _configService;
1921
private readonly CodeScanner _scanner;
2022

21-
public ScanController(IConfiguration configuration, IResourceBackend backend)
23+
public ScanController(IConfiguration configuration, IResourceBackend backend, ConfigurationService configService)
2224
{
2325
_resourcePath = configuration["ResourcePath"] ?? Directory.GetCurrentDirectory();
2426
_sourcePath = configuration["SourcePath"] ?? Directory.GetParent(_resourcePath)?.FullName ?? _resourcePath;
2527
_backend = backend;
28+
_configService = configService;
2629
_scanner = new CodeScanner();
2730
}
2831

@@ -43,9 +46,6 @@ public ActionResult<ScanResponse> Scan([FromBody] ScanRequest? request)
4346
return StatusCode(500, new ErrorResponse { Error = "No default language file found" });
4447
}
4548

46-
// Get all keys from resource files
47-
var allResourceKeys = defaultFile.Entries.Select(e => e.Key).Distinct().ToHashSet();
48-
4949
// Scan source code
5050
var excludePatterns = request?.ExcludePatterns ?? new List<string>
5151
{
@@ -55,7 +55,12 @@ public ActionResult<ScanResponse> Scan([FromBody] ScanRequest? request)
5555
"**/.git/**"
5656
};
5757

58-
var result = _scanner.Scan(_sourcePath, resourceFiles, false, excludePatterns, null, null);
58+
// Apply configured scanning settings (resource class names / localization methods)
59+
var scanConfig = _configService.GetConfiguration().Scanning;
60+
var classNames = scanConfig?.ResourceClassNames;
61+
var methods = scanConfig?.LocalizationMethods;
62+
63+
var result = _scanner.Scan(_sourcePath, resourceFiles, false, excludePatterns, classNames, methods);
5964

6065
return Ok(new ScanResponse
6166
{
@@ -125,10 +130,15 @@ public ActionResult<ScanResponse> ScanFile([FromBody] FileScanRequest request)
125130
return StatusCode(500, new ErrorResponse { Error = "No default language file found" });
126131
}
127132

133+
// Apply configured scanning settings (resource class names / localization methods)
134+
var scanConfig = _configService.GetConfiguration().Scanning;
135+
var classNames = scanConfig?.ResourceClassNames;
136+
var methods = scanConfig?.LocalizationMethods;
137+
128138
// Scan the single file with optional content override
129139
var result = fileContent != null
130-
? _scanner.ScanSingleFileContent(filePath, fileContent, resourceFiles, false, null, null)
131-
: _scanner.ScanSingleFile(filePath, resourceFiles, false, null, null);
140+
? _scanner.ScanSingleFileContent(filePath, fileContent, resourceFiles, false, classNames, methods)
141+
: _scanner.ScanSingleFile(filePath, resourceFiles, false, classNames, methods);
132142

133143
// Return same response format as full scan
134144
return Ok(new ScanResponse
@@ -182,7 +192,11 @@ public ActionResult<UnusedKeysResponse> GetUnused()
182192
"**/bin/**", "**/obj/**", "**/node_modules/**", "**/.git/**"
183193
};
184194

185-
var result = _scanner.Scan(_sourcePath, resourceFiles, false, excludePatterns, null, null);
195+
var scanConfig = _configService.GetConfiguration().Scanning;
196+
var classNames = scanConfig?.ResourceClassNames;
197+
var methods = scanConfig?.LocalizationMethods;
198+
199+
var result = _scanner.Scan(_sourcePath, resourceFiles, false, excludePatterns, classNames, methods);
186200

187201
return Ok(new UnusedKeysResponse { UnusedKeys = result.UnusedKeys });
188202
}
@@ -214,7 +228,11 @@ public ActionResult<MissingKeysResponse> GetMissing()
214228
"**/bin/**", "**/obj/**", "**/node_modules/**", "**/.git/**"
215229
};
216230

217-
var result = _scanner.Scan(_sourcePath, resourceFiles, false, excludePatterns, null, null);
231+
var scanConfig = _configService.GetConfiguration().Scanning;
232+
var classNames = scanConfig?.ResourceClassNames;
233+
var methods = scanConfig?.LocalizationMethods;
234+
235+
var result = _scanner.Scan(_sourcePath, resourceFiles, false, excludePatterns, classNames, methods);
218236

219237
return Ok(new MissingKeysResponse { MissingKeys = result.MissingKeys.Select(k => k.Key).ToList() });
220238
}
@@ -240,7 +258,11 @@ public ActionResult<KeyReferencesResponse> GetReferences(string keyName)
240258
"**/bin/**", "**/obj/**", "**/node_modules/**", "**/.git/**"
241259
};
242260

243-
var result = _scanner.Scan(_sourcePath, resourceFiles, false, excludePatterns, null, null);
261+
var scanConfig = _configService.GetConfiguration().Scanning;
262+
var classNames = scanConfig?.ResourceClassNames;
263+
var methods = scanConfig?.LocalizationMethods;
264+
265+
var result = _scanner.Scan(_sourcePath, resourceFiles, false, excludePatterns, classNames, methods);
244266
var keyUsage = result.AllKeyUsages.FirstOrDefault(k => k.Key == keyName);
245267

246268
if (keyUsage == null)

0 commit comments

Comments
 (0)