• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

xaviersolau / BlazorJsonLocalization / 17992101609

24 Sep 2025 11:13PM UTC coverage: 79.916% (+2.0%) from 77.941%
17992101609

Pull #86

github

web-flow
Merge 284fca638 into d1acab631
Pull Request #86: Improve localizationgen tool to generate code in intermediate binary …

147 of 184 new or added lines in 5 files covered. (79.89%)

1 existing line in 1 file now uncovered.

1146 of 1434 relevant lines covered (79.92%)

34.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.22
/src/tools/SoloX.BlazorJsonLocalization.Tools.Core/Impl/LocalizationGenerator.cs
1
// ----------------------------------------------------------------------
2
// <copyright file="LocalizationGenerator.cs" company="Xavier Solau">
3
// Copyright © 2021 Xavier Solau.
4
// Licensed under the MIT license.
5
// See LICENSE file in the project root for full license information.
6
// </copyright>
7
// ----------------------------------------------------------------------
8

9
using System;
10
using System.Collections.Generic;
11
using System.Collections.Immutable;
12
using System.IO;
13
using System.Linq;
14
using Microsoft.CodeAnalysis;
15
using Microsoft.CodeAnalysis.CSharp.Syntax;
16
using SoloX.BlazorJsonLocalization.Attributes;
17
using SoloX.BlazorJsonLocalization.Tools.Core.Patterns.Impl;
18
using SoloX.BlazorJsonLocalization.Tools.Core.Selectors;
19
using SoloX.GeneratorTools.Core.CSharp.Generator.Impl;
20
using SoloX.GeneratorTools.Core.CSharp.Generator.Selectors;
21
using SoloX.GeneratorTools.Core.CSharp.Workspace;
22
using SoloX.GeneratorTools.Core.Generator;
23
using SoloX.GeneratorTools.Core.Generator.Impl;
24
using SoloX.GeneratorTools.Core.Utils;
25

26
namespace SoloX.BlazorJsonLocalization.Tools.Core.Impl
27
{
28
    /// <summary>
29
    /// Json localizer generator that will generate the String localizer classes and the Json resources.
30
    /// </summary>
31
    public class LocalizationGenerator : ILocalizationGenerator
32
    {
33
        private readonly IGeneratorLogger<LocalizationGenerator> logger;
34
        private readonly ICSharpWorkspaceFactory workspaceFactory;
35

36
        private const string ResourcesFolderName = "JsonResourcesFolder";
37

38
        private static readonly GeneratorOptions DefaultOptions = new GeneratorOptions(false, false, false, true);
1✔
39

40
        /// <summary>
41
        /// Initializes a new instance of the <see cref="LocalizationGenerator"/> class.
42
        /// </summary>
43
        /// <param name="logger">Logger that will be used for logs.</param>
44
        /// <param name="workspaceFactory">The workspace factory to use.</param>
45
        public LocalizationGenerator(IGeneratorLogger<LocalizationGenerator> logger, ICSharpWorkspaceFactory workspaceFactory)
46
        {
47
            this.logger = logger;
18✔
48
            this.workspaceFactory = workspaceFactory;
18✔
49
        }
18✔
50

51
        /// <summary>
52
        /// Process generation for the given project file. It can be used from a CLI tool.
53
        /// </summary>
54
        /// <param name="projectFile">The project file to process.</param>
55
        /// <param name="generatorOptions">Generator options.</param>
56
        public ILocalizationGeneratorResults Generate(string projectFile, GeneratorOptions? generatorOptions = null)
57
        {
58
            if (generatorOptions == null)
1✔
59
            {
60
                generatorOptions = DefaultOptions;
×
61
            }
62

63
            var inputFiles = new List<string>();
1✔
64
            var generatedCodeFiles = new List<string>();
1✔
65
            var generatedResourceFiles = new List<string>();
1✔
66

67
            this.logger.LogInformation($"Loading {Path.GetFileName(projectFile)}.");
1✔
68

69
            var workspace = this.workspaceFactory.CreateWorkspace();
1✔
70

71
            var project = workspace.RegisterProject(projectFile);
1✔
72

73
            var projectParameters = new ProjectParameters(project.ProjectPath, project.RootNameSpace);
1✔
74

75
            var locator = new RelativeLocator(
1✔
76
                Path.Combine(projectParameters.ProjectPath, "obj"),
1✔
77
                "none",
1✔
78
                suffix: "Impl");
1✔
79

80
            var fileWriter = new MemoryWriter(".g.cs", (fileName, textCode) =>
1✔
81
            {
1✔
82
                generatedCodeFiles.Add(fileName);
2✔
83

1✔
84
                var generatedCode = textCode.ReadToEnd();
2✔
85

1✔
86
                var previousCode = File.Exists(fileName) ? File.ReadAllText(fileName) : null;
2✔
87

1✔
88
                if (previousCode == null || previousCode != generatedCode)
2✔
89
                {
1✔
90
                    var location = Path.GetDirectoryName(fileName);
2✔
91
                    if (!Directory.Exists(location))
2✔
92
                    {
1✔
93
                        Directory.CreateDirectory(location);
1✔
94
                    }
1✔
95

1✔
96
                    this.logger.LogInformation($"Writing CS file: {fileName}.");
2✔
97
                    File.WriteAllText(fileName, generatedCode);
2✔
98
                }
1✔
99
                else
1✔
100
                {
1✔
NEW
101
                    this.logger.LogInformation($"No change in CS file: {fileName}.");
×
102
                }
1✔
103
            });
1✔
104

105
            var jsonLocator = new RelativeLocator(
1✔
106
                Path.Combine(project.ProjectPath, ResourcesFolderName),
1✔
107
                project.RootNameSpace);
1✔
108

109
            var jsonWriter = new MemoryWriter(".json", (l, s) =>
1✔
110
            {
1✔
111
                generatedResourceFiles.Add(l);
3✔
112

1✔
113
                var generatedJson = s.ReadToEnd();
3✔
114

1✔
115
                var previousJson = File.Exists(l) ? File.ReadAllText(l) : null;
3✔
116

1✔
117
                if (previousJson == null || previousJson != generatedJson)
3✔
118
                {
1✔
119
                    var location = Path.GetDirectoryName(l);
3✔
120
                    if (!Directory.Exists(location))
3✔
121
                    {
1✔
122
                        Directory.CreateDirectory(location);
1✔
123
                    }
1✔
124

1✔
125
                    this.logger.LogInformation($"Writing JSON file: {l}.");
3✔
126
                    File.WriteAllText(l, generatedJson);
3✔
127
                }
1✔
128
                else
1✔
129
                {
1✔
NEW
130
                    this.logger.LogInformation($"No change in JSON file: {l}.");
×
131
                }
1✔
132
            });
1✔
133

134
            var jsonReader = new FileReader(".json");
1✔
135

136
            // Generate with a filter on current project interface declarations.
137
            this.Generate(
1✔
138
                workspace,
1✔
139
                locator,
1✔
140
                fileWriter,
1✔
141
                jsonLocator,
1✔
142
                jsonReader,
1✔
143
                jsonWriter,
1✔
144
                project.Files,
1✔
145
                generatorOptions);
1✔
146

147
            if (generatorOptions.RegisterCompile)
1✔
148
            {
149
                GeneratePropsCodeFile(
1✔
150
                    projectParameters.ProjectPath,
1✔
151
                    project.ProjectFile,
1✔
152
                    Path.Combine(projectParameters.ProjectPath, "obj"),
1✔
153
                    generatedCodeFiles);
1✔
154
            }
155

156
            if (generatorOptions.RegisterEmbeddedResource)
1✔
157
            {
158
                GeneratePropsResourceFile(
1✔
159
                    projectParameters.ProjectPath,
1✔
160
                    project.ProjectFile,
1✔
161
                    Path.Combine(projectParameters.ProjectPath, "obj"),
1✔
162
                    generatedResourceFiles);
1✔
163
            }
164

165
            // Clean files
166
            RemovePreviousFilesIfAny(Path.Combine(projectParameters.ProjectPath, "obj", "LocalizationGenerator.Code.Output"), generatedCodeFiles);
1✔
167

168
            return new LocalizationGeneratorResults(
1✔
169
                inputFiles,
1✔
170
                generatedCodeFiles,
1✔
171
                generatedResourceFiles);
1✔
172
        }
173

174
        private static void RemovePreviousFilesIfAny(string fileListOutput, List<string> generatedFiles)
175
        {
176
            if (File.Exists(fileListOutput))
1✔
177
            {
NEW
178
                var fileSet = new HashSet<string>(generatedFiles);
×
179

NEW
180
                var oldList = File.ReadAllLines(fileListOutput);
×
181

NEW
182
                foreach (var oldFile in oldList)
×
183
                {
NEW
184
                    if (!fileSet.Contains(oldFile) && File.Exists(oldFile))
×
185
                    {
NEW
186
                        File.Delete(oldFile);
×
187
                    }
188
                }
189
            }
190

191
            File.WriteAllLines(fileListOutput, generatedFiles);
1✔
192
        }
1✔
193

194
        /// <summary>
195
        /// Process generation for the given compilation unit. It can be used by a code analyzer tool.
196
        /// </summary>
197
        /// <param name="compilation">Compilation unit to process.</param>
198
        /// <param name="projectParameters">Project parameters.</param>
199
        /// <param name="classes">Targeted interfaces.</param>
200
        /// <param name="context">Source production context to generate the C# code.</param>
201
        /// <param name="generatorOptions">Generator options.</param>
202
        public ILocalizationGeneratorResults Generate(
203
            ProjectParameters projectParameters,
204
            Compilation compilation,
205
            ImmutableArray<InterfaceDeclarationSyntax> classes,
206
            SourceProductionContext context,
207
            GeneratorOptions? generatorOptions = null)
208
        {
209
            if (projectParameters == null)
1✔
210
            {
NEW
211
                throw new ArgumentNullException(nameof(projectParameters));
×
212
            }
213

214
            if (compilation == null)
1✔
215
            {
216
                throw new ArgumentNullException(nameof(compilation));
×
217
            }
218

219
            if (generatorOptions == null)
1✔
220
            {
221
                generatorOptions = DefaultOptions;
1✔
222
            }
223

224
            var inputFiles = new List<string>();
1✔
225
            var generatedCodeFiles = new List<string>();
1✔
226
            var generatedResourceFiles = new List<string>();
1✔
227

228
            var ns = projectParameters.RootNamespace;
1✔
229
            var projectFolder = projectParameters.ProjectPath;
1✔
230

231
            var workspace = this.workspaceFactory.CreateWorkspace(compilation);
1✔
232

233
            var locator = new RelativeLocator(Path.Combine(projectFolder), ns);
1✔
234
            var fileWriter = new MemoryWriter(".g.cs", (l, s) =>
1✔
235
            {
1✔
236
                generatedCodeFiles.Add(l);
2✔
237

1✔
238
                var name = l.StartsWith(projectFolder, StringComparison.Ordinal) ? l.Substring(projectFolder.Length) : l;
2✔
239

1✔
240
                name = name.Replace('/', '.').Replace('\\', '.').TrimStart('.');
2✔
241

1✔
242
                this.logger.LogInformation($"Adding source file: {name}.");
2✔
243

1✔
244
                context.AddSource(name, s.ReadToEnd());
2✔
245
            });
3✔
246

247
            var jsonLocator = new RelativeLocator(Path.Combine(projectFolder, ResourcesFolderName), ns);
1✔
248
            var jsonWriter = new FileWriter(".json", f =>
1✔
249
            {
1✔
250
                this.logger.LogInformation($"Writing JSON file: {f}.");
3✔
251

1✔
252
                generatedResourceFiles.Add(f);
3✔
253
            });
4✔
254
            var jsonReader = new FileReader(".json");
1✔
255

256
            // Generate with a filter on current project interface declarations.
257
            this.Generate(
1✔
258
                workspace,
1✔
259
                locator,
1✔
260
                fileWriter,
1✔
261
                jsonLocator,
1✔
262
                jsonReader,
1✔
263
                jsonWriter,
1✔
264
                workspace.SyntaxTrees.Where(s => compilation.ContainsSyntaxTree(s.SyntaxTree)),
16✔
265
                generatorOptions);
1✔
266

267
            if (generatorOptions.RegisterEmbeddedResource)
1✔
268
            {
269
                // For now this is not properly working since it is too late for MSBuild props. We need
270
                // to rebuild the project to take into account the embedded resources.
NEW
271
                GeneratePropsResourceFile(
×
NEW
272
                    projectParameters.ProjectPath,
×
NEW
273
                    $"{compilation.AssemblyName}.csproj",
×
NEW
274
                    Path.Combine(projectParameters.ProjectPath, "obj"),
×
NEW
275
                    generatedResourceFiles);
×
276
            }
277

278
            return new LocalizationGeneratorResults(
1✔
279
                inputFiles,
1✔
280
                generatedCodeFiles,
1✔
281
                generatedResourceFiles);
1✔
282
        }
283

284
        internal void Generate(
285
            ICSharpWorkspace workspace,
286
            ILocator locator,
287
            IWriter fileWriter,
288
            ILocator jsonLocator,
289
            IReader jsonReader,
290
            IWriter jsonWriter,
291
            IEnumerable<ICSharpFile> files,
292
            GeneratorOptions generatorOptions)
293
        {
294
            this.logger.LogDebug($"Register pattern files.");
18✔
295

296
            workspace.RegisterFile(GetContentFile("./Patterns/Itf/IMyObjectStringLocalizerPattern.cs"));
18✔
297
            workspace.RegisterFile(GetContentFile("./Patterns/Impl/MyObjectStringLocalizerPattern.cs"));
18✔
298
            workspace.RegisterFile(GetContentFile("./Patterns/Impl/MyObjectStringLocalizerPatternExtensions.cs"));
18✔
299

300
            workspace.RegisterFile(GetContentFile("./Patterns/Itf/IMyObjectSubStringLocalizerPattern.cs"));
18✔
301
            workspace.RegisterFile(GetContentFile("./Patterns/Impl/MyObjectSubStringLocalizerPattern.cs"));
18✔
302

303
            workspace.RegisterAssemblyTypes(
18✔
304
                typeof(SubLocalizerPropertySelector).Assembly,
18✔
305
                new[] { typeof(SubLocalizerPropertySelector), typeof(LocalizerPropertySelector), typeof(LocalizerArgumentSelector), typeof(LocalizerAttribute), typeof(SubLocalizerAttribute) });
18✔
306

307
            this.logger.LogInformation($"Processing data loading.");
18✔
308

309
            var resolver = workspace.DeepLoad();
18✔
310

311
            this.logger.LogInformation($"Generating localization source files.");
18✔
312

313
            this.logger.LogDebug($"Generating from pattern MyObjectStringLocalizerPattern.");
18✔
314

315
            var generator1 = new AutomatedGenerator(
18✔
316
                fileWriter,
18✔
317
                locator,
18✔
318
                resolver,
18✔
319
                typeof(MyObjectStringLocalizerPattern),
18✔
320
                this.logger,
18✔
321
                new SelectorResolver());
18✔
322

323
            generator1.AddIgnoreUsing("SoloX.BlazorJsonLocalization.Attributes", "SoloX.BlazorJsonLocalization.Tools.Core.Handlers", "SoloX.BlazorJsonLocalization.Tools.Core.Selectors");
18✔
324

325
            var gen1Items = generator1.Generate(files);
18✔
326

327
            this.logger.LogDebug($"Generating from pattern MyObjectStringLocalizerPatternExtensions.");
18✔
328

329
            var generator2 = new AutomatedGenerator(
18✔
330
                fileWriter,
18✔
331
                locator,
18✔
332
                resolver,
18✔
333
                typeof(MyObjectStringLocalizerPatternExtensions),
18✔
334
                this.logger,
18✔
335
                new SelectorResolver());
18✔
336

337
            generator2.AddIgnoreUsing("SoloX.BlazorJsonLocalization.Attributes", "SoloX.BlazorJsonLocalization.Tools.Core.Handlers", "SoloX.BlazorJsonLocalization.Tools.Core.Selectors");
18✔
338

339
            var gen2Items = generator2.Generate(files);
18✔
340

341
            this.logger.LogDebug($"Generating from pattern MyObjectSubStringLocalizerPattern.");
18✔
342

343
            var generator3 = new AutomatedGenerator(
18✔
344
                fileWriter,
18✔
345
                locator,
18✔
346
                resolver,
18✔
347
                typeof(MyObjectSubStringLocalizerPattern),
18✔
348
                this.logger,
18✔
349
                new SelectorResolver());
18✔
350

351
            generator3.AddIgnoreUsing("SoloX.BlazorJsonLocalization.Attributes", "SoloX.BlazorJsonLocalization.Tools.Core.Handlers", "SoloX.BlazorJsonLocalization.Tools.Core.Selectors");
18✔
352

353
            var gen3Items = generator3.Generate(files);
18✔
354

355
            this.logger.LogInformation($"Generating localization resource files.");
18✔
356

357
            var jsonGenerator = new JsonFileGenerator(
18✔
358
                jsonReader,
18✔
359
                jsonWriter,
18✔
360
                jsonLocator,
18✔
361
                resolver,
18✔
362
                new AttributeSelector<LocalizerAttribute>(),
18✔
363
                this.logger,
18✔
364
                ResourcesFolderName,
18✔
365
                generatorOptions);
18✔
366

367
            var jsonItems = jsonGenerator.Generate(files);
18✔
368
        }
18✔
369

370
        private static void GeneratePropsResourceFile(
371
            string projectPath,
372
            string projectFile,
373
            string objFile,
374
            List<string> generatedResourceFiles)
375
        {
376
            var absProjectPath = Path.GetFullPath(projectPath);
1✔
377

378
            if (!absProjectPath.EndsWith(Path.PathSeparator.ToString(), StringComparison.Ordinal))
1✔
379
            {
380
                absProjectPath = absProjectPath + Path.DirectorySeparatorChar;
1✔
381
            }
382

383
            var resourceFiles = generatedResourceFiles.Select(f => Path.Combine(".", f.Replace(absProjectPath, string.Empty)));
10✔
384

385
            var propsFile = Path.Combine(objFile, $"{projectFile}.LocalizationGenerator.Resource.g.props");
1✔
386
            var list = new List<string>();
1✔
387

388
            list.Add("<Project>");
1✔
389

390
            list.Add("  <ItemGroup>");
1✔
391

392
            foreach (var rfItem in resourceFiles!)
8✔
393
            {
394
                list.Add($"    <None Remove=\"{rfItem}\" />");
3✔
395
            }
396

397
            list.Add("  </ItemGroup>");
1✔
398
            list.Add("  <ItemGroup>");
1✔
399

400
            foreach (var rfItem in resourceFiles!)
8✔
401
            {
402
                list.Add($"    <EmbeddedResource Remove=\"{rfItem}\" />");
3✔
403
            }
404

405
            list.Add("  </ItemGroup>");
1✔
406
            list.Add("  <ItemGroup>");
1✔
407

408
            foreach (var rfItem in resourceFiles!)
8✔
409
            {
410
                list.Add($"    <EmbeddedResource Include=\"{rfItem}\" />");
3✔
411
            }
412

413
            list.Add("  </ItemGroup>");
1✔
414

415
            list.Add("</Project>");
1✔
416

417
            File.WriteAllLines(propsFile, list);
1✔
418
        }
1✔
419

420
        private static void GeneratePropsCodeFile(
421
            string projectPath,
422
            string projectFile,
423
            string objPath,
424
            List<string> generatedCodeFiles)
425
        {
426
            var absObjPath = Path.GetFullPath(objPath);
1✔
427

428
            if (!absObjPath.EndsWith(Path.PathSeparator.ToString(), StringComparison.Ordinal))
1✔
429
            {
430
                absObjPath = absObjPath + Path.DirectorySeparatorChar;
1✔
431
            }
432

433
            var codeFiles = generatedCodeFiles.Select(f => Path.Combine("$(MSBuildThisFileDirectory)", f.Replace(absObjPath, string.Empty)));
5✔
434

435
            var propsFile = Path.Combine(objPath, $"{projectFile}.LocalizationGenerator.Code.g.props");
1✔
436
            var list = new List<string>();
1✔
437

438
            list.Add("<Project>");
1✔
439

440
            list.Add("  <ItemGroup>");
1✔
441

442
            foreach (var rfItem in codeFiles!)
6✔
443
            {
444
                list.Add($"    <None Remove=\"{rfItem}\" />");
2✔
445
            }
446

447
            list.Add("  </ItemGroup>");
1✔
448

449
            list.Add("  <ItemGroup>");
1✔
450

451
            foreach (var rfItem in codeFiles!)
6✔
452
            {
453
                list.Add($"    <Compile Include=\"{rfItem}\" Link=\"{Path.GetFileName(rfItem)}\" Visible=\"false\" />");
2✔
454
            }
455

456
            list.Add("  </ItemGroup>");
1✔
457

458
            list.Add("</Project>");
1✔
459

460
            File.WriteAllLines(propsFile, list);
1✔
461
        }
1✔
462

463
        private static string GetContentFile(string contentFile)
464
        {
465
            var assembly = typeof(LocalizationGenerator).Assembly;
90✔
466

467
            if (!string.IsNullOrEmpty(assembly.Location))
90✔
468
            {
469
                var file = Path.Combine(Path.GetDirectoryName(assembly.Location), contentFile);
90✔
470

471
                if (File.Exists(file))
90✔
472
                {
473
                    return file;
90✔
474
                }
475
            }
476

NEW
477
            var res = assembly.GetManifestResourceNames();
×
478

NEW
479
            var resName = assembly.GetName().Name + contentFile.Substring(1).Replace('/', '.');
×
480

NEW
481
            using var stream = assembly.GetManifestResourceStream(resName);
×
482

NEW
483
            var tempFile = Path.GetTempFileName();
×
484

NEW
485
            using var tempStream = File.OpenWrite(tempFile);
×
486

NEW
487
            stream.CopyTo(tempStream);
×
488

NEW
489
            return tempFile;
×
UNCOV
490
        }
×
491
    }
492
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc