diff --git a/BlazorAppTest/BlazorAppTest.csproj b/BlazorAppTest/BlazorAppTest.csproj
index 05f954d1e..be476b1f0 100644
--- a/BlazorAppTest/BlazorAppTest.csproj
+++ b/BlazorAppTest/BlazorAppTest.csproj
@@ -28,4 +28,7 @@
+
+
+
diff --git a/BlazorAppTest/Resources/ResourcesServiceCollectionInstaller.cs b/BlazorAppTest/Resources/ResourcesServiceCollectionInstaller.cs
deleted file mode 100644
index c4f6dd4a8..000000000
--- a/BlazorAppTest/Resources/ResourcesServiceCollectionInstaller.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace BlazorAppTest.Resources;
-
-public static partial class ResourcesServiceCollectionInstaller
-{
-
-}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index a6ae3f882..008b4bc54 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -22,7 +22,9 @@
-
+
+
+
diff --git a/Havit.Blazor.sln b/Havit.Blazor.sln
index d9eb6000c..f5c43e800 100644
--- a/Havit.Blazor.sln
+++ b/Havit.Blazor.sln
@@ -78,6 +78,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Havit.Blazor.TestApp", "Hav
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Havit.Blazor.TestApp.Client", "Havit.Blazor.TestApp\Havit.Blazor.TestApp.Client\Havit.Blazor.TestApp.Client.csproj", "{38D87399-13C1-4C86-9343-712EAE6095F0}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Havit.SourceGenerators.StrongApiStringLocalizers.Tests", "Havit.SourceGenerators.StrongApiStringLocalizers.Tests\Havit.SourceGenerators.StrongApiStringLocalizers.Tests.csproj", "{9236499E-62FF-4C2F-92A0-408143D67E72}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -180,6 +182,10 @@ Global
{38D87399-13C1-4C86-9343-712EAE6095F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{38D87399-13C1-4C86-9343-712EAE6095F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{38D87399-13C1-4C86-9343-712EAE6095F0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9236499E-62FF-4C2F-92A0-408143D67E72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9236499E-62FF-4C2F-92A0-408143D67E72}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9236499E-62FF-4C2F-92A0-408143D67E72}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9236499E-62FF-4C2F-92A0-408143D67E72}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -196,6 +202,7 @@ Global
{E64B4752-B697-4B5A-91F0-8A8581B1ABE5} = {79C29E4F-EF98-4F43-9782-B0D58A533C4D}
{F6A546C8-60C3-47AC-A58B-66E3513DCC7A} = {79C29E4F-EF98-4F43-9782-B0D58A533C4D}
{45BC138B-4F00-43DD-BF5D-D2FC36972857} = {D7B56FC7-6322-4B66-B34F-A972805BF740}
+ {9236499E-62FF-4C2F-92A0-408143D67E72} = {201D627C-BC35-4971-95D0-5BA656E771F6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BA2F0FE2-9DA0-4C29-8772-0802E2E67119}
diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers.Tests/Global.resx b/Havit.SourceGenerators.StrongApiStringLocalizers.Tests/Global.resx
new file mode 100644
index 000000000..2447f2125
--- /dev/null
+++ b/Havit.SourceGenerators.StrongApiStringLocalizers.Tests/Global.resx
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Čeština je <b>skvělá</b>!
+
+
+ Hello world!!!
+ Hello world resource comment.
+
+
\ No newline at end of file
diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers.Tests/Havit.SourceGenerators.StrongApiStringLocalizers.Tests.csproj b/Havit.SourceGenerators.StrongApiStringLocalizers.Tests/Havit.SourceGenerators.StrongApiStringLocalizers.Tests.csproj
new file mode 100644
index 000000000..0781a123d
--- /dev/null
+++ b/Havit.SourceGenerators.StrongApiStringLocalizers.Tests/Havit.SourceGenerators.StrongApiStringLocalizers.Tests.csproj
@@ -0,0 +1,35 @@
+
+
+
+ net9.0
+ false
+ enable
+ true
+ Exe
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers.Tests/StrongApiStringLocalizersGeneratorTests.cs b/Havit.SourceGenerators.StrongApiStringLocalizers.Tests/StrongApiStringLocalizersGeneratorTests.cs
new file mode 100644
index 000000000..3580856cd
--- /dev/null
+++ b/Havit.SourceGenerators.StrongApiStringLocalizers.Tests/StrongApiStringLocalizersGeneratorTests.cs
@@ -0,0 +1,124 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel;
+
+namespace Havit.SourceGenerators.StrongApiStringLocalizers.Tests;
+
+[TestClass]
+public class StrongApiStringLocalizersGeneratorTests
+{
+ [TestMethod]
+ public async Task StrongApiStringLocalizersGenerator_Test()
+ {
+ // Arrange
+
+ using var globalResxStream = File.OpenRead("Global.resx");
+
+ string projectDir = Environment.CurrentDirectory;
+
+ var test = new Microsoft.CodeAnalysis.CSharp.Testing.CSharpSourceGeneratorTest
+ {
+ TestState =
+ {
+ AnalyzerConfigFiles =
+ {
+ ($"/.editorconfig", $@"""
+ is_global=true
+ build_property.RootNamespace = MyApp.Resources
+ build_property.ProjectDir = {projectDir}
+ """)
+ }
+ },
+ ReferenceAssemblies = ReferenceAssemblies.Net
+ .Net90
+ .AddPackages(ImmutableArray.Create(
+ new PackageIdentity("Microsoft.Extensions.Localization", "9.0.1"))) // we are using IStringLocalizer from this package in the generated code
+ };
+
+ // resource file
+ test.TestState.AdditionalFiles.Add((Path.Combine(projectDir, "MyResources", "Global.resx"), SourceText.From(globalResxStream)));
+
+ // EXPECTED OUTPUT
+
+ test.TestState.GeneratedSources.Add((typeof(StrongApiStringLocalizersGenerator), "MyApp.Resources.MyResources.IGlobalLocalizer.g.cs", @"//
+
+namespace MyApp.Resources.MyResources;
+
+using System.CodeDom.Compiler;
+using Microsoft.Extensions.Localization;
+
+[GeneratedCode(""Havit.SourceGenerators.StrongApiStringLocalizers.StrongApiStringLocalizersGenerator"", ""2.0.0.0"")]
+public interface IGlobalLocalizer : IStringLocalizer
+{
+ ///
+ /// Čeština je <b>skvělá</b>!
+ ///
+ LocalizedString CzechAndHtml { get; }
+
+ ///
+ /// Hello world resource comment.
+ ///
+ LocalizedString HelloWorld { get; }
+
+}
+"));
+
+ // TestProject - defined by the TestState implementation
+ test.TestState.GeneratedSources.Add((typeof(StrongApiStringLocalizersGenerator), $"MyApp.Resources.MyResources.GlobalLocalizer.g.cs", @"//
+
+namespace MyApp.Resources.MyResources;
+
+using System.CodeDom.Compiler;
+using System.Collections.Generic;
+using Microsoft.Extensions.Localization;
+
+[GeneratedCode(""Havit.SourceGenerators.StrongApiStringLocalizers.StrongApiStringLocalizersGenerator"", ""2.0.0.0"")]
+public class GlobalLocalizer : IGlobalLocalizer
+{
+ private readonly IStringLocalizer _localizer;
+
+ public GlobalLocalizer(IStringLocalizerFactory stringLocalizerFactory)
+ {
+ _localizer = stringLocalizerFactory.Create(""MyResources.Global"", ""TestProject"");
+ }
+
+ ///
+ /// Čeština je <b>skvělá</b>!
+ ///
+ public LocalizedString CzechAndHtml => _localizer[""CzechAndHtml""];
+
+ ///
+ /// Hello world resource comment.
+ ///
+ public LocalizedString HelloWorld => _localizer[""HelloWorld""];
+
+ LocalizedString IStringLocalizer.this[string name] => _localizer[name];
+ LocalizedString IStringLocalizer.this[string name, params object[] arguments] => _localizer[name, arguments];
+ IEnumerable IStringLocalizer.GetAllStrings(bool includeParentCultures) => _localizer.GetAllStrings(includeParentCultures);
+}
+"));
+
+ test.TestState.GeneratedSources.Add((typeof(StrongApiStringLocalizersGenerator), "MyApp.Resources.ServiceCollectionExtensions.g.cs", @"//
+
+namespace MyApp.Resources;
+
+using System.CodeDom.Compiler;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Localization;
+
+[GeneratedCode(""Havit.SourceGenerators.StrongApiStringLocalizers.StrongApiStringLocalizersGenerator"", ""2.0.0.0"")]
+public static class ServiceCollectionExtensions
+{
+ public static IServiceCollection AddGeneratedResourceWrappers(this IServiceCollection services)
+ {
+ services.AddTransient();
+ return services;
+ }
+}
+"));
+
+ // Act + Assert
+ await test.RunAsync();
+ }
+}
diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers/AnalyzerReleases.Shipped.md b/Havit.SourceGenerators.StrongApiStringLocalizers/AnalyzerReleases.Shipped.md
new file mode 100644
index 000000000..60b59dd99
--- /dev/null
+++ b/Havit.SourceGenerators.StrongApiStringLocalizers/AnalyzerReleases.Shipped.md
@@ -0,0 +1,3 @@
+; Shipped analyzer releases
+; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers/AnalyzerReleases.Unshipped.md b/Havit.SourceGenerators.StrongApiStringLocalizers/AnalyzerReleases.Unshipped.md
new file mode 100644
index 000000000..c689753fd
--- /dev/null
+++ b/Havit.SourceGenerators.StrongApiStringLocalizers/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,8 @@
+; Unshipped analyzer release
+; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|--------------------
+HLG1002 | Usage | Warning | Cannot parse RESX file
diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers/BuilderExtensions.cs b/Havit.SourceGenerators.StrongApiStringLocalizers/BuilderExtensions.cs
deleted file mode 100644
index d6f7f8657..000000000
--- a/Havit.SourceGenerators.StrongApiStringLocalizers/BuilderExtensions.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System.Reflection;
-using System.Text;
-
-namespace Havit.SourceGenerators.StrongApiStringLocalizers;
-
-internal static class BuilderExtensions
-{
- public static StringBuilder AppendGeneratedCodeAttribute(this StringBuilder builder)
- {
- builder.Append($"[GeneratedCode(\"{nameof(Havit)}.{nameof(SourceGenerators)}.{nameof(StrongApiStringLocalizers)}.{nameof(LocalizerGenerator)}\", \"{Assembly.GetExecutingAssembly().GetName().Version}\")]");
-
- return builder;
- }
-}
diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers/Havit.SourceGenerators.StrongApiStringLocalizers.csproj b/Havit.SourceGenerators.StrongApiStringLocalizers/Havit.SourceGenerators.StrongApiStringLocalizers.csproj
index bf749b858..8fe1b5c3a 100644
--- a/Havit.SourceGenerators.StrongApiStringLocalizers/Havit.SourceGenerators.StrongApiStringLocalizers.csproj
+++ b/Havit.SourceGenerators.StrongApiStringLocalizers/Havit.SourceGenerators.StrongApiStringLocalizers.csproj
@@ -22,6 +22,8 @@
+
+
diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers/Helpers/HttpUtilityExt.cs b/Havit.SourceGenerators.StrongApiStringLocalizers/Helpers/HttpUtilityExt.cs
new file mode 100644
index 000000000..2e301cbb4
--- /dev/null
+++ b/Havit.SourceGenerators.StrongApiStringLocalizers/Helpers/HttpUtilityExt.cs
@@ -0,0 +1,160 @@
+using System.Text;
+
+// Copy of HFW HttpUtilityExt.
+
+namespace Havit.SourceGenerators.StrongApiStringLocalizers.Helpers;
+
+internal static partial class HttpUtilityExt
+{
+ public static string HtmlEncode(string unicodeText, HtmlEncodeOptions options)
+ {
+ int unicodeValue;
+ StringBuilder result = new StringBuilder();
+
+ bool opIgnoreNonASCIICharacters = (options & HtmlEncodeOptions.IgnoreNonASCIICharacters) == HtmlEncodeOptions.IgnoreNonASCIICharacters;
+ bool opExtendedHtmlEntities = (options & HtmlEncodeOptions.ExtendedHtmlEntities) == HtmlEncodeOptions.ExtendedHtmlEntities;
+ bool opXmlApostropheEntity = (options & HtmlEncodeOptions.XmlApostropheEntity) == HtmlEncodeOptions.XmlApostropheEntity;
+
+ int length = unicodeText.Length;
+ for (int i = 0; i < length; i++)
+ {
+ unicodeValue = unicodeText[i];
+ switch (unicodeValue)
+ {
+ case '&':
+ result.Append("&");
+ break;
+ case '<':
+ result.Append("<");
+ break;
+ case '>':
+ result.Append(">");
+ break;
+ case '"':
+ result.Append(""");
+ break;
+ case '\'':
+ if (opXmlApostropheEntity)
+ {
+ result.Append("'");
+ break;
+ }
+ else
+ {
+ goto default;
+ }
+ case 0xA0: // no-break space
+ if (opExtendedHtmlEntities)
+ {
+ result.Append(" ");
+ break;
+ }
+ else
+ {
+ goto default;
+ }
+ case '€':
+ if (opExtendedHtmlEntities)
+ {
+ result.Append("€");
+ break;
+ }
+ else
+ {
+ goto default;
+ }
+ case '©':
+ if (opExtendedHtmlEntities)
+ {
+ result.Append("©");
+ break;
+ }
+ else
+ {
+ goto default;
+ }
+ case '®':
+ if (opExtendedHtmlEntities)
+ {
+ result.Append("®");
+ break;
+ }
+ else
+ {
+ goto default;
+ }
+ case '™': // trade-mark
+ if (opExtendedHtmlEntities)
+ {
+ result.Append("™");
+ break;
+ }
+ else
+ {
+ goto default;
+ }
+ default:
+ if (((unicodeText[i] >= ' ') && (unicodeText[i] <= 0x007E))
+ || opIgnoreNonASCIICharacters)
+ {
+ result.Append(unicodeText[i]);
+ }
+ else
+ {
+ result.Append("");
+ result.Append(unicodeValue.ToString(System.Globalization.NumberFormatInfo.InvariantInfo));
+ result.Append(";");
+ }
+ break;
+ }
+ }
+ return result.ToString();
+ }
+ public static string HtmlEncode(string unicodeText)
+ {
+ return HtmlEncode(unicodeText, HtmlEncodeOptions.None);
+ }
+}
+
+[Flags]
+public enum HtmlEncodeOptions
+{
+ ///
+ /// Označuje, že nemají být nastaveny žádné options, použije se default postup.
+ /// Default postup převede pouze čtyři základní entity
+ ///
+ /// - < --- <
+ /// - > --- >
+ /// - & --- &
+ /// - " --- "
+ ///
+ ///
+ None = 0,
+
+ ///
+ /// Při konverzi budou ignorovány znaky mimo ASCII hodnoty, nebudou tedy tvořeny číselné entity typu {.
+ ///
+ IgnoreNonASCIICharacters = 1,
+
+ ///
+ /// Při konverzi bude použita rozšířená sada HTML-entit, které by se jinak převedly na číselné entity.
+ /// Např. bude použito ©, , §, atp.
+ ///
+ ExtendedHtmlEntities = 2,
+
+ ///
+ /// Při konverzi převede apostrofy na ' entitu.
+ /// POZOR! ' není standardní HTML entita a třeba IE ji v HTML režimu nepozná!!!
+ ///
+ ///
+ /// V kombinaci se základním dostaneme sadu pěti built-in XML entit:
+ ///
+ /// - < --- <
+ /// - > --- >
+ /// - & --- &
+ /// - " --- "
+ /// - ' --- '
+ ///
+ ///
+ XmlApostropheEntity = 4
+}
diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers/Helpers/SourceTextReader.cs b/Havit.SourceGenerators.StrongApiStringLocalizers/Helpers/SourceTextReader.cs
new file mode 100644
index 000000000..f22b91536
--- /dev/null
+++ b/Havit.SourceGenerators.StrongApiStringLocalizers/Helpers/SourceTextReader.cs
@@ -0,0 +1,24 @@
+using Microsoft.CodeAnalysis.Text;
+
+namespace Havit.SourceGenerators.StrongApiStringLocalizers.Helpers;
+
+// source: https://github.com/dotnet/roslyn-analyzers/blob/8fe7aeb135c64e095f43292c427453858d937184/src/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator/AbstractResxGenerator.cs#L888
+internal sealed class SourceTextReader : TextReader
+{
+ private readonly SourceText _text;
+ private int _position;
+
+ public SourceTextReader(SourceText text)
+ {
+ _text = text;
+ }
+
+ public override int Read(char[] buffer, int index, int count)
+ {
+ var remaining = _text.Length - _position;
+ var charactersToRead = Math.Min(remaining, count);
+ _text.CopyTo(_position, buffer, index, charactersToRead);
+ _position += charactersToRead;
+ return charactersToRead;
+ }
+}
diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers/LocalizerBuilder.cs b/Havit.SourceGenerators.StrongApiStringLocalizers/LocalizerBuilder.cs
deleted file mode 100644
index 6ea908a02..000000000
--- a/Havit.SourceGenerators.StrongApiStringLocalizers/LocalizerBuilder.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using System.Text;
-using Havit.SourceGenerators.StrongApiStringLocalizers;
-
-namespace LocalizerGenerator;
-
-internal class LocalizerBuilder
-{
- public string Name { get; set; }
- public string LocalizerClassName => $"{Name}Localizer";
- public string LocalizerInterfaceName => $"I{Name}Localizer";
- public string Namespace { get; set; }
- public List Properties { get; } = new List();
- public string IStringLocalizerName => $"IStringLocalizer<{Name}>";
- public string BaseClassName => $"DelegatingStringLocalizer<{Name}>";
-
- public string BuildSource()
- {
- var builder = new StringBuilder();
- BuildNamespace(builder);
- return builder.ToString();
- }
-
- private void BuildNamespace(StringBuilder builder)
- {
- builder.Append("namespace ").Append(Namespace).AppendLine();
- builder.AppendLine("{");
- BuildUsings(builder);
- BuildInterface(builder);
- BuildLocalizerClass(builder);
- builder.AppendLine("}");
- }
-
- private void BuildUsings(StringBuilder builder)
- {
- builder.AppendLine("using System.CodeDom.Compiler;");
- builder.AppendLine("using Microsoft.Extensions.Localization;");
- builder.AppendLine("using Havit.Extensions.Localization;");
- }
-
- private void BuildInterface(StringBuilder builder)
- {
- builder.AppendGeneratedCodeAttribute().AppendLine();
- builder.Append("public interface ").Append(LocalizerInterfaceName).Append(" : ").Append(IStringLocalizerName).AppendLine();
- builder.AppendLine("{");
- foreach (var property in Properties)
- {
- builder.Append("LocalizedString ").Append(property).Append(" { get; }").AppendLine();
- }
- builder.AppendLine("}");
- }
-
- private void BuildLocalizerClass(StringBuilder builder)
- {
- builder.AppendGeneratedCodeAttribute().AppendLine();
- builder.Append("public class ").Append(LocalizerClassName).Append(" : ").Append(BaseClassName).Append(", ").Append(LocalizerInterfaceName).AppendLine();
- builder.AppendLine("{");
- BuildCtor(builder);
- foreach (var property in Properties)
- {
- builder.Append("public LocalizedString ").Append(property).Append(" => this[\"").Append(property).Append("\"];").AppendLine();
- }
- builder.AppendLine("}");
- }
-
- private void BuildCtor(StringBuilder builder)
- {
- builder.Append("public ").Append(LocalizerClassName).Append("(").Append(IStringLocalizerName).Append(" innerLocalizer) : base(innerLocalizer)").AppendLine();
- builder.AppendLine("{");
- builder.AppendLine("}");
- }
-}
diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers/LocalizerGenerator.cs b/Havit.SourceGenerators.StrongApiStringLocalizers/LocalizerGenerator.cs
deleted file mode 100644
index c90162401..000000000
--- a/Havit.SourceGenerators.StrongApiStringLocalizers/LocalizerGenerator.cs
+++ /dev/null
@@ -1,120 +0,0 @@
-using System.Text;
-using System.Xml;
-using System.Xml.Linq;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.CSharp.Syntax;
-using Microsoft.CodeAnalysis.Text;
-
-namespace LocalizerGenerator;
-
-[Generator]
-public class LocalizerGenerator : ISourceGenerator
-{
- public const string ServiceCollectionInstallerMarker = "ResourcesServiceCollectionInstaller";
-
-#pragma warning disable RS2008
- private static readonly DiagnosticDescriptor s_xmlParseWarning = new DiagnosticDescriptor(id: "LG0001", title: "Cannot parse XML file", messageFormat: "Cannot parse XML file '{0}'", category: nameof(LocalizerGenerator), DiagnosticSeverity.Warning, isEnabledByDefault: true);
-#pragma warning restore RS2008
-
-
- public void Initialize(GeneratorInitializationContext context)
- { }
-
- public void Execute(GeneratorExecutionContext context)
- {
- foreach (var syntaxTree in context.Compilation.SyntaxTrees)
- {
- var file = Path.GetFileNameWithoutExtension(syntaxTree.FilePath);
- if (!file.Equals(ServiceCollectionInstallerMarker, StringComparison.Ordinal))
- {
- continue;
- }
-
- var syntaxRoot = syntaxTree.GetRoot();
-
- var namespaceBase = FindNamespaceName(syntaxRoot);
- if (namespaceBase == null)
- {
- continue;
- }
-
- var registrationsBuilder = new RegistrationsBuilder();
- registrationsBuilder.Namespace = namespaceBase;
-
- var rootDir = Path.GetDirectoryName(syntaxTree.FilePath);
-#pragma warning disable RS1035 // Do not use APIs banned for analyzers (Directory)
- foreach (var resx in Directory.EnumerateFiles(rootDir, "*.resx", SearchOption.AllDirectories))
- {
- var localizerBuilder = new LocalizerBuilder();
- localizerBuilder.Name = Path.GetFileNameWithoutExtension(resx);
- if (localizerBuilder.Name.Contains("."))
- {
- // language-specific file
- continue;
- }
- var namespaceSuffix = Path.GetDirectoryName(resx).Remove(0, rootDir.Length).Replace(Path.DirectorySeparatorChar, '.');
- localizerBuilder.Namespace = $"{namespaceBase}{namespaceSuffix}";
- var properties = ParseResx(resx);
- if (properties == null)
- {
- context.ReportDiagnostic(Diagnostic.Create(s_xmlParseWarning, Location.None, resx));
- continue;
- }
- localizerBuilder.Properties.AddRange(properties);
- context.AddSource($"{nameof(LocalizerGenerator)}.{localizerBuilder.Namespace}.{localizerBuilder.LocalizerClassName}.generated.cs", SourceText.From(localizerBuilder.BuildSource(), Encoding.UTF8));
-
- var markerClassBuilder = new MarkerClassBuilder();
- markerClassBuilder.Namespace = localizerBuilder.Namespace;
- markerClassBuilder.Name = localizerBuilder.Name;
- context.AddSource($"{nameof(LocalizerGenerator)}.{markerClassBuilder.Namespace}.{markerClassBuilder.Name}.generated.cs", SourceText.From(markerClassBuilder.BuildSource(), Encoding.UTF8));
-
- registrationsBuilder.Localizers.Add(localizerBuilder);
- }
-#pragma warning restore RS1035 // Do not use APIs banned for analyzers
-
- context.AddSource($"{nameof(LocalizerGenerator)}.{registrationsBuilder.Namespace}.{registrationsBuilder.MethodName}.generated.cs", SourceText.From(registrationsBuilder.BuildSource(), Encoding.UTF8));
- }
- }
-
- private static List ParseResx(string path)
- {
- try
- {
- var result = new List();
- var xdoc = XDocument.Load(path);
- foreach (var item in xdoc.Root.Elements("data"))
- {
- var nameAttribute = item.Attribute("name");
- if (nameAttribute == null)
- {
- continue;
- }
-
- result.Add(nameAttribute.Value);
- }
- return result;
- }
- catch (XmlException)
- {
- return null;
- }
- }
-
- private static string FindNamespaceName(SyntaxNode syntaxRoot)
- {
- var namespaceNode = (BaseNamespaceDeclarationSyntax)syntaxRoot.ChildNodes().Where(x => x.IsKind(SyntaxKind.NamespaceDeclaration)).FirstOrDefault();
- if (namespaceNode != null)
- {
- return namespaceNode.Name.ToString();
- }
-
- var fileScopedNamespaceNode = (BaseNamespaceDeclarationSyntax)syntaxRoot.ChildNodes().Where(x => x.IsKind(SyntaxKind.FileScopedNamespaceDeclaration)).FirstOrDefault();
- if (fileScopedNamespaceNode != null)
- {
- return fileScopedNamespaceNode.Name.ToString();
- }
-
- return null;
- }
-}
diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers/MarkerClassBuilder.cs b/Havit.SourceGenerators.StrongApiStringLocalizers/MarkerClassBuilder.cs
deleted file mode 100644
index e589fc6ac..000000000
--- a/Havit.SourceGenerators.StrongApiStringLocalizers/MarkerClassBuilder.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using System.Text;
-using Havit.SourceGenerators.StrongApiStringLocalizers;
-
-namespace LocalizerGenerator;
-
-internal class MarkerClassBuilder
-{
- public string Name { get; set; }
- public string Namespace { get; set; }
-
- public string BuildSource()
- {
- var builder = new StringBuilder();
- BuildUsings(builder);
- BuildNamespace(builder);
- return builder.ToString();
- }
- private void BuildUsings(StringBuilder builder)
- {
- builder.AppendLine("using System.CodeDom.Compiler;");
- builder.AppendLine("using System.ComponentModel;");
- }
-
- private void BuildNamespace(StringBuilder builder)
- {
- builder.Append("namespace ").Append(Namespace).AppendLine();
- builder.AppendLine("{");
- BuildMarkerClass(builder);
- builder.AppendLine("}");
- }
-
- private void BuildMarkerClass(StringBuilder builder)
- {
- builder.AppendGeneratedCodeAttribute().AppendLine();
- builder.Append("[Browsable(false)]").AppendLine();
- builder.Append("[EditorBrowsable(EditorBrowsableState.Never)]").AppendLine();
- builder.Append("public class ").Append(Name).AppendLine();
- builder.AppendLine("{");
- builder.AppendLine("}");
- }
-}
diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers/Model/BuildConfiguration.cs b/Havit.SourceGenerators.StrongApiStringLocalizers/Model/BuildConfiguration.cs
new file mode 100644
index 000000000..c6c97d85e
--- /dev/null
+++ b/Havit.SourceGenerators.StrongApiStringLocalizers/Model/BuildConfiguration.cs
@@ -0,0 +1,8 @@
+namespace Havit.SourceGenerators.StrongApiStringLocalizers.Model;
+
+internal class BuildConfiguration
+{
+ public string RootNamespace { get; set; }
+ public string ProjectDirectory { get; internal set; }
+ public string AssemblyName { get; internal set; }
+}
diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers/Model/ResourceData.cs b/Havit.SourceGenerators.StrongApiStringLocalizers/Model/ResourceData.cs
new file mode 100644
index 000000000..389a0be25
--- /dev/null
+++ b/Havit.SourceGenerators.StrongApiStringLocalizers/Model/ResourceData.cs
@@ -0,0 +1,15 @@
+namespace Havit.SourceGenerators.StrongApiStringLocalizers.Model;
+
+internal class ResourceData
+{
+ public string AssemblyName { get; set; }
+ public string ResxFilePath { get; set; }
+ public string ResourceNamespace { get; set; }
+ public string ResourceName { get; set; }
+
+ public string TargetLocalizerNamespace { get; set; }
+ public string LocalizerImplementationClassName => $"{ResourceName}Localizer";
+ public string LocalizerInterfaceName => $"I{ResourceName}Localizer";
+
+ public List Properties { get; set; }
+}
\ No newline at end of file
diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers/Model/ResourcePropertyItem.cs b/Havit.SourceGenerators.StrongApiStringLocalizers/Model/ResourcePropertyItem.cs
new file mode 100644
index 000000000..9beb22508
--- /dev/null
+++ b/Havit.SourceGenerators.StrongApiStringLocalizers/Model/ResourcePropertyItem.cs
@@ -0,0 +1,7 @@
+namespace Havit.SourceGenerators.StrongApiStringLocalizers.Model;
+
+internal class ResourcePropertyItem
+{
+ public string Name { get; set; }
+ public string Comment { get; set; }
+}
diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers/Model/ServiceCollectionExtensionsData.cs b/Havit.SourceGenerators.StrongApiStringLocalizers/Model/ServiceCollectionExtensionsData.cs
new file mode 100644
index 000000000..988db080c
--- /dev/null
+++ b/Havit.SourceGenerators.StrongApiStringLocalizers/Model/ServiceCollectionExtensionsData.cs
@@ -0,0 +1,7 @@
+namespace Havit.SourceGenerators.StrongApiStringLocalizers.Model;
+
+internal class ServiceCollectionExtensionsData
+{
+ public string RootNamespace { get; set; }
+ public List Resources { get; set; }
+}
\ No newline at end of file
diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers/RegistrationsBuilder.cs b/Havit.SourceGenerators.StrongApiStringLocalizers/RegistrationsBuilder.cs
deleted file mode 100644
index c16a451d4..000000000
--- a/Havit.SourceGenerators.StrongApiStringLocalizers/RegistrationsBuilder.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-using System.Text;
-using Havit.SourceGenerators.StrongApiStringLocalizers;
-
-namespace LocalizerGenerator;
-
-internal class RegistrationsBuilder
-{
- public string Namespace { get; set; }
- public List Localizers { get; } = new List();
- public string MethodName => "AddGeneratedResourceWrappers";
-
- public string BuildSource()
- {
- var builder = new StringBuilder();
- BuildNamespace(builder);
- return builder.ToString();
- }
-
- private void BuildNamespace(StringBuilder builder)
- {
- builder.Append("namespace ").Append(Namespace).AppendLine();
- builder.AppendLine("{");
- BuildUsings(builder);
- BuildClass(builder);
- builder.AppendLine("}");
- }
-
- private void BuildUsings(StringBuilder builder)
- {
- builder.AppendLine("using System.CodeDom.Compiler;");
- builder.AppendLine("using Microsoft.Extensions.DependencyInjection;");
- builder.AppendLine("using Microsoft.Extensions.Localization;");
- foreach (var @namespace in Localizers.Select(x => x.Namespace).Distinct(StringComparer.Ordinal))
- {
- builder.Append("using ").Append(@namespace).Append(";").AppendLine();
- }
- }
-
- private void BuildClass(StringBuilder builder)
- {
- builder.AppendGeneratedCodeAttribute().AppendLine();
- builder.Append("partial class ").Append(LocalizerGenerator.ServiceCollectionInstallerMarker).AppendLine();
- builder.AppendLine("{");
- builder.Append("public static void ").Append(MethodName).Append("(this IServiceCollection services)").AppendLine();
- builder.AppendLine("{");
- BuildRegistrations(builder);
- builder.AppendLine("}");
- builder.AppendLine("}");
- }
-
- private void BuildRegistrations(StringBuilder builder)
- {
- foreach (var localizer in Localizers)
- {
- builder.Append("services.AddScoped<").Append(localizer.LocalizerInterfaceName).Append(", ").Append(localizer.LocalizerClassName).Append(">();").AppendLine();
- }
- }
-}
\ No newline at end of file
diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers/SourceBuilders/LocalizerImplementationSourceBuilder.cs b/Havit.SourceGenerators.StrongApiStringLocalizers/SourceBuilders/LocalizerImplementationSourceBuilder.cs
new file mode 100644
index 000000000..6e22b11ff
--- /dev/null
+++ b/Havit.SourceGenerators.StrongApiStringLocalizers/SourceBuilders/LocalizerImplementationSourceBuilder.cs
@@ -0,0 +1,59 @@
+using System.Text;
+using Havit.SourceGenerators.StrongApiStringLocalizers.Model;
+
+namespace Havit.SourceGenerators.StrongApiStringLocalizers.SourceBuilders;
+
+///
+/// Generated source code for localizer implementation.
+///
+internal class LocalizerImplementationSourceBuilder
+{
+ private readonly ResourceData _resxBuildData;
+
+ public LocalizerImplementationSourceBuilder(ResourceData resxBuildData)
+ {
+ _resxBuildData = resxBuildData;
+ }
+
+ public string BuildSource()
+ {
+ var builder = new StringBuilder();
+ builder.AppendAutoGeneratedDocumenationCommentLine();
+ builder.AppendLine();
+ builder.AppendLine($"namespace {_resxBuildData.TargetLocalizerNamespace};");
+ builder.AppendLine();
+ builder.AppendLine("using System.CodeDom.Compiler;");
+ builder.AppendLine("using System.Collections.Generic;");
+ builder.AppendLine("using Microsoft.Extensions.Localization;");
+ builder.AppendLine();
+ builder.AppendGeneratedCodeAttributeLine();
+ builder.AppendLine($"public class {_resxBuildData.LocalizerImplementationClassName} : {_resxBuildData.LocalizerInterfaceName}");
+ builder.AppendLine("{");
+
+ builder.AppendLine("\tprivate readonly IStringLocalizer _localizer;");
+ builder.AppendLine();
+
+ // constructor
+ builder.AppendLine($"\tpublic {_resxBuildData.LocalizerImplementationClassName}(IStringLocalizerFactory stringLocalizerFactory)");
+ builder.AppendLine("\t{");
+ builder.AppendLine($"\t\t_localizer = stringLocalizerFactory.Create(\"{(_resxBuildData.ResourceNamespace + "." + _resxBuildData.ResourceName).Trim('.')}\", \"{_resxBuildData.AssemblyName}\");");
+ builder.AppendLine("\t}");
+ builder.AppendLine();
+
+ // properties
+ foreach (var property in _resxBuildData.Properties)
+ {
+ builder.AppendSummaryCommentLine(property.Comment);
+ builder.AppendLine($"\tpublic LocalizedString {property.Name} => _localizer[\"{property.Name}\"];");
+ builder.AppendLine();
+ }
+
+ // IStringLocalizer
+ builder.AppendLine("\tLocalizedString IStringLocalizer.this[string name] => _localizer[name];");
+ builder.AppendLine("\tLocalizedString IStringLocalizer.this[string name, params object[] arguments] => _localizer[name, arguments];");
+ builder.AppendLine("\tIEnumerable IStringLocalizer.GetAllStrings(bool includeParentCultures) => _localizer.GetAllStrings(includeParentCultures);");
+ builder.AppendLine("}");
+
+ return builder.ToString();
+ }
+}
diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers/SourceBuilders/LocalizerInterfaceSourceBuilder.cs b/Havit.SourceGenerators.StrongApiStringLocalizers/SourceBuilders/LocalizerInterfaceSourceBuilder.cs
new file mode 100644
index 000000000..900ee151f
--- /dev/null
+++ b/Havit.SourceGenerators.StrongApiStringLocalizers/SourceBuilders/LocalizerInterfaceSourceBuilder.cs
@@ -0,0 +1,41 @@
+using System.Text;
+using Havit.SourceGenerators.StrongApiStringLocalizers.Model;
+
+namespace Havit.SourceGenerators.StrongApiStringLocalizers.SourceBuilders;
+
+///
+/// Generates source code for the localizer interface.
+///
+internal class LocalizerInterfaceSourceBuilder
+{
+ private readonly ResourceData _resxBuildData;
+
+ public LocalizerInterfaceSourceBuilder(ResourceData resxBuildData)
+ {
+ _resxBuildData = resxBuildData;
+ }
+
+ public string BuildSource()
+ {
+ var builder = new StringBuilder();
+ builder.AppendAutoGeneratedDocumenationCommentLine();
+ builder.AppendLine();
+ builder.AppendLine($"namespace {_resxBuildData.TargetLocalizerNamespace};");
+ builder.AppendLine();
+ builder.AppendLine("using System.CodeDom.Compiler;");
+ builder.AppendLine("using Microsoft.Extensions.Localization;");
+ builder.AppendLine();
+ builder.AppendGeneratedCodeAttributeLine();
+ builder.AppendLine($"public interface {_resxBuildData.LocalizerInterfaceName} : IStringLocalizer");
+ builder.AppendLine("{");
+ foreach (var property in _resxBuildData.Properties)
+ {
+ builder.AppendSummaryCommentLine(property.Comment);
+ builder.AppendLine($"\tLocalizedString {property.Name} {{ get; }}");
+ builder.AppendLine();
+ }
+ builder.AppendLine("}");
+
+ return builder.ToString();
+ }
+}
diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers/SourceBuilders/ServiceRegistrationsSourceBuilder.cs b/Havit.SourceGenerators.StrongApiStringLocalizers/SourceBuilders/ServiceRegistrationsSourceBuilder.cs
new file mode 100644
index 000000000..595de4f77
--- /dev/null
+++ b/Havit.SourceGenerators.StrongApiStringLocalizers/SourceBuilders/ServiceRegistrationsSourceBuilder.cs
@@ -0,0 +1,47 @@
+using System.Text;
+using Havit.SourceGenerators.StrongApiStringLocalizers.Model;
+
+namespace Havit.SourceGenerators.StrongApiStringLocalizers.SourceBuilders;
+
+///
+/// Generates source code for service registrations.
+///
+internal class ServiceRegistrationsSourceBuilder
+{
+ private readonly ServiceCollectionExtensionsData _serviceCollectionExtensionsData;
+
+ public ServiceRegistrationsSourceBuilder(ServiceCollectionExtensionsData serviceCollectionExtensionsData)
+ {
+ _serviceCollectionExtensionsData = serviceCollectionExtensionsData;
+ }
+
+ public string BuildSource()
+ {
+ var builder = new StringBuilder();
+ builder.AppendAutoGeneratedDocumenationCommentLine();
+ builder.AppendLine();
+
+ builder.AppendLine($"namespace {_serviceCollectionExtensionsData.RootNamespace};");
+ builder.AppendLine();
+
+ builder.AppendLine("using System.CodeDom.Compiler;");
+ builder.AppendLine("using Microsoft.Extensions.DependencyInjection;");
+ builder.AppendLine("using Microsoft.Extensions.Localization;");
+ builder.AppendLine();
+
+ builder.AppendGeneratedCodeAttributeLine();
+ builder.AppendLine("public static class ServiceCollectionExtensions");
+ builder.AppendLine("{");
+ builder.AppendLine("\tpublic static IServiceCollection AddGeneratedResourceWrappers(this IServiceCollection services)");
+ builder.AppendLine("\t{");
+ foreach (var resource in _serviceCollectionExtensionsData.Resources.Where(resource => resource.Properties != null))
+ {
+ builder.AppendLine($"\t\tservices.AddTransient<{resource.TargetLocalizerNamespace}.{resource.LocalizerInterfaceName}, {resource.TargetLocalizerNamespace}.{resource.LocalizerImplementationClassName}>();");
+ }
+ builder.AppendLine("\t\treturn services;");
+ builder.AppendLine("\t}");
+ builder.AppendLine("}");
+
+ return builder.ToString();
+ }
+}
\ No newline at end of file
diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers/SourceBuilders/StringBuilderExtensions.cs b/Havit.SourceGenerators.StrongApiStringLocalizers/SourceBuilders/StringBuilderExtensions.cs
new file mode 100644
index 000000000..444b1ade3
--- /dev/null
+++ b/Havit.SourceGenerators.StrongApiStringLocalizers/SourceBuilders/StringBuilderExtensions.cs
@@ -0,0 +1,34 @@
+using System.Text;
+using System.Text.RegularExpressions;
+using Havit.SourceGenerators.StrongApiStringLocalizers.Helpers;
+
+namespace Havit.SourceGenerators.StrongApiStringLocalizers.SourceBuilders;
+
+internal static class StringBuilderExtensions
+{
+ public static StringBuilder AppendAutoGeneratedDocumenationCommentLine(this StringBuilder builder)
+ {
+ return builder.AppendLine("// ");
+ }
+
+ public static StringBuilder AppendGeneratedCodeAttributeLine(this StringBuilder builder)
+ {
+ return builder.AppendLine($"[GeneratedCode(\"{nameof(Havit)}.{nameof(SourceGenerators)}.{nameof(StrongApiStringLocalizers)}.{nameof(StrongApiStringLocalizersGenerator)}\", \"{typeof(StringBuilderExtensions).Assembly.GetName().Version}\")]");
+ }
+
+ public static StringBuilder AppendSummaryCommentLine(this StringBuilder builder, string comment)
+ {
+ if (!String.IsNullOrEmpty(comment))
+ {
+ string[] commentLines = Regex.Split(comment, "\r\n|\r|\n");
+ builder.AppendLine("\t/// ");
+ foreach (var commentLine in commentLines)
+ {
+ builder.Append("\t/// ").AppendLine(HttpUtilityExt.HtmlEncode(commentLine, HtmlEncodeOptions.IgnoreNonASCIICharacters));
+ }
+ builder.AppendLine("\t/// ");
+ }
+
+ return builder;
+ }
+}
diff --git a/Havit.SourceGenerators.StrongApiStringLocalizers/StrongApiStringLocalizersGenerator.cs b/Havit.SourceGenerators.StrongApiStringLocalizers/StrongApiStringLocalizersGenerator.cs
new file mode 100644
index 000000000..35196cbd6
--- /dev/null
+++ b/Havit.SourceGenerators.StrongApiStringLocalizers/StrongApiStringLocalizersGenerator.cs
@@ -0,0 +1,147 @@
+using System.Text;
+using System.Xml;
+using System.Xml.Linq;
+using Havit.SourceGenerators.StrongApiStringLocalizers.Helpers;
+using Havit.SourceGenerators.StrongApiStringLocalizers.Model;
+using Havit.SourceGenerators.StrongApiStringLocalizers.SourceBuilders;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Havit.SourceGenerators.StrongApiStringLocalizers;
+
+[Generator]
+public class StrongApiStringLocalizersGenerator : IIncrementalGenerator
+{
+ private static readonly DiagnosticDescriptor s_xmlParseWarning = new DiagnosticDescriptor(id: "HLG1002", title: "Cannot parse RESX file", messageFormat: "Cannot parse RESX file '{0}'", category: "Usage", DiagnosticSeverity.Warning, isEnabledByDefault: true);
+
+ public void Initialize(IncrementalGeneratorInitializationContext initializationContext)
+ {
+ // read project configuration
+ IncrementalValueProvider buildConfigurationProvider = initializationContext.AnalyzerConfigOptionsProvider
+ .Combine(initializationContext.CompilationProvider)
+ .Select((item, _) =>
+ {
+ AnalyzerConfigOptionsProvider analyzerConfig = item.Left;
+ Compilation compilation = item.Right;
+ analyzerConfig.GlobalOptions.TryGetValue("build_property.RootNamespace", out string rootNamespace);
+ analyzerConfig.GlobalOptions.TryGetValue("build_property.ProjectDir", out string projectDir);
+
+ return new BuildConfiguration
+ {
+ RootNamespace = rootNamespace,
+ ProjectDirectory = projectDir,
+ AssemblyName = compilation.AssemblyName
+ };
+ });
+
+ // get resx files (with all required data to generate localizers one by one)
+ // (resx files are available in initializationContext.AdditionalTextsProvider only when library references
+ // nuget package Microsoft.CodeAnalysis)
+ IncrementalValuesProvider resxDataProvider = initializationContext.AdditionalTextsProvider
+ .Where(static file => string.Equals(Path.GetExtension(file.Path), ".resx", StringComparison.OrdinalIgnoreCase)) // .resx
+ .Where(static file => !Path.GetFileNameWithoutExtension(file.Path).Contains(".")) // skip language-specific files - take Resource.resx, skip Resource.cs.resx
+ .Combine(buildConfigurationProvider) // "join" with build configuration
+ .Select(static (item, cancellationToken) =>
+ {
+ AdditionalText additionalText = item.Left;
+ BuildConfiguration buildConfiguration = item.Right;
+ return new ResourceData
+ {
+ ResxFilePath = additionalText.Path,
+ AssemblyName = buildConfiguration.AssemblyName,
+ TargetLocalizerNamespace = GetTargetNamespace(buildConfiguration, additionalText.Path),
+ ResourceNamespace = GetResourceNamespace(buildConfiguration, additionalText.Path),
+ ResourceName = Path.GetFileNameWithoutExtension(additionalText.Path), // ie. Homepage, Glossary, ...
+ Properties = GetResxPropertiesSafe(additionalText, cancellationToken) // ie.. Yes, No, OK, Cancel, ...
+ };
+ });
+
+ // get list of resx files (with all required data to generate service collection extension)
+ IncrementalValueProvider serviceCollectionExtensionsDataProvider = resxDataProvider
+ .Collect()
+ .Combine(buildConfigurationProvider)
+ .Select(static (item, cancellationToken) => new ServiceCollectionExtensionsData
+ {
+ RootNamespace = item.Right.RootNamespace,
+ Resources = item.Left.OrderBy(item => item.TargetLocalizerNamespace).ThenBy(item => item.ResourceName).ToList()
+ });
+
+ initializationContext.RegisterSourceOutput(resxDataProvider, static (sourceContext, resxBuildData) =>
+ {
+ if (resxBuildData.Properties == null)
+ {
+ // XML could not be parsed
+ sourceContext.ReportDiagnostic(Diagnostic.Create(s_xmlParseWarning, Location.None, resxBuildData.ResxFilePath));
+ }
+ else
+ {
+ // generate localizer interface (ie. Homepage.resx => IHomepageLocalizer)
+ LocalizerInterfaceSourceBuilder localizerInterfaceSourceBuilder = new LocalizerInterfaceSourceBuilder(resxBuildData);
+ sourceContext.AddSource($"{resxBuildData.TargetLocalizerNamespace}.I{resxBuildData.ResourceName}Localizer.g.cs", SourceText.From(localizerInterfaceSourceBuilder.BuildSource(), Encoding.UTF8));
+
+ // generate localizer implementation (ie. Homepage.resx => HomepageLocalizer)
+ LocalizerImplementationSourceBuilder localizerImplementationSourceBuilder = new LocalizerImplementationSourceBuilder(resxBuildData);
+ sourceContext.AddSource($"{resxBuildData.TargetLocalizerNamespace}.{resxBuildData.ResourceName}Localizer.g.cs", SourceText.From(localizerImplementationSourceBuilder.BuildSource(), Encoding.UTF8));
+ }
+ });
+
+ initializationContext.RegisterSourceOutput(serviceCollectionExtensionsDataProvider, static (sourceContext, serviceCollectionExtensionsData) =>
+ {
+ if (serviceCollectionExtensionsData.Resources.Count > 0)
+ {
+ // generate service collection externsion to register all generated localizers
+ var serviceRegistrationsSourceBuilder = new ServiceRegistrationsSourceBuilder(serviceCollectionExtensionsData);
+ sourceContext.AddSource($"{serviceCollectionExtensionsData.RootNamespace}.ServiceCollectionExtensions.g.cs", SourceText.From(serviceRegistrationsSourceBuilder.BuildSource(), Encoding.UTF8));
+ }
+ });
+ }
+
+ private static string GetTargetNamespace(BuildConfiguration buildConfiguration, string path)
+ {
+ return (buildConfiguration.RootNamespace + "." + GetResourceNamespace(buildConfiguration, path)).Trim('.');
+ }
+
+ private static string GetResourceNamespace(BuildConfiguration buildConfiguration, string path)
+ {
+ string localPath = path.StartsWith(buildConfiguration.ProjectDirectory, StringComparison.InvariantCultureIgnoreCase)
+ ? path.Substring(buildConfiguration.ProjectDirectory.Length).Trim(Path.DirectorySeparatorChar)
+ : path;
+
+ return Path.GetDirectoryName(localPath).Replace(Path.DirectorySeparatorChar, '.');
+ }
+
+
+ private static List GetResxPropertiesSafe(AdditionalText resx, CancellationToken cancellationToken)
+ {
+ try
+ {
+ SourceText resxContent = resx.GetText(cancellationToken);
+ var resxXmlDoc = XDocument.Load(new SourceTextReader(resxContent));
+
+ var result = new List();
+ foreach (var item in resxXmlDoc.Root.Elements("data"))
+ {
+ var nameAttribute = item.Attribute("name");
+ if (nameAttribute == null)
+ {
+ continue;
+ }
+
+ string comment = item.Element("comment")?.Value;
+ string value = item.Element("value")?.Value;
+
+ result.Add(new ResourcePropertyItem
+ {
+ Name = nameAttribute.Value,
+ Comment = comment ?? value,
+ });
+ }
+ return result.OrderBy(item => item.Name, StringComparer.InvariantCultureIgnoreCase).ToList();
+ }
+ catch (XmlException)
+ {
+ return null;
+ }
+ }
+}