diff --git a/Flow.Launcher.Localization.Analyzers/Flow.Launcher.Localization.Analyzers.csproj b/Flow.Launcher.Localization.Analyzers/Flow.Launcher.Localization.Analyzers.csproj
index e9ff31a..ce219d0 100644
--- a/Flow.Launcher.Localization.Analyzers/Flow.Launcher.Localization.Analyzers.csproj
+++ b/Flow.Launcher.Localization.Analyzers/Flow.Launcher.Localization.Analyzers.csproj
@@ -8,12 +8,12 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/Flow.Launcher.Localization.SourceGenerators/Flow.Launcher.Localization.SourceGenerators.csproj b/Flow.Launcher.Localization.SourceGenerators/Flow.Launcher.Localization.SourceGenerators.csproj
index 6c83304..40d585d 100644
--- a/Flow.Launcher.Localization.SourceGenerators/Flow.Launcher.Localization.SourceGenerators.csproj
+++ b/Flow.Launcher.Localization.SourceGenerators/Flow.Launcher.Localization.SourceGenerators.csproj
@@ -8,11 +8,11 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs b/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
index 4c97db5..1d48f6e 100644
--- a/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
+++ b/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
+using System.Threading;
using System.Xml.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
@@ -11,10 +13,13 @@
namespace Flow.Launcher.Localization.SourceGenerators.Localize
{
+ ///
+ /// Generates properties for strings based on resource files.
+ ///
[Generator]
- public partial class LocalizeSourceGenerator : ISourceGenerator
+ public partial class LocalizeSourceGenerator : IIncrementalGenerator
{
- private OptimizationLevel _optimizationLevel;
+ #region Fields
private const string CoreNamespace1 = "Flow.Launcher";
private const string CoreNamespace2 = "Flow.Launcher.Core";
@@ -22,477 +27,580 @@ public partial class LocalizeSourceGenerator : ISourceGenerator
private const string ClassName = "Localize";
private const string PluginInterfaceName = "IPluginI18n";
private const string PluginContextTypeName = "PluginInitContext";
- private const string KeywordStatic = "static";
- private const string KeywordPrivate = "private";
- private const string KeywordProtected = "protected";
private const string XamlPrefix = "system";
private const string XamlTag = "String";
- private const string DefaultLanguageFilePathEndsWith = @"\Languages\en.xaml";
- private const string XamlCustomPathPropertyKey = "build_property.localizegeneratorlangfiles";
- private readonly char[] _xamlCustomPathPropertyDelimiters = { '\n', ';' };
private readonly Regex _languagesXamlRegex = new Regex(@"\\Languages\\[^\\]+\.xaml$", RegexOptions.IgnoreCase);
- public void Initialize(GeneratorInitializationContext context)
+ private static readonly Version PackageVersion = typeof(LocalizeSourceGenerator).Assembly.GetName().Version;
+
+ #endregion
+
+ #region Incremental Generator
+
+ ///
+ /// Initializes the generator and registers source output based on resource files.
+ ///
+ /// The initialization context.
+ public void Initialize(IncrementalGeneratorInitializationContext context)
{
+ var xamlFiles = context.AdditionalTextsProvider
+ .Where(file => _languagesXamlRegex.IsMatch(file.Path));
+
+ var localizedStrings = xamlFiles
+ .Select((file, ct) => ParseXamlFile(file, ct))
+ .Collect()
+ .SelectMany((files, _) => files);
+
+ // TODO: Add support for usedKeys
+ var invocationKeys = context.SyntaxProvider
+ .CreateSyntaxProvider(
+ predicate: (n, _) => n is InvocationExpressionSyntax,
+ transform: GetLocalizationKeyFromInvocation)
+ .Where(key => !string.IsNullOrEmpty(key))
+ .Collect()
+ .Select((keys, _) => keys.Distinct().ToImmutableHashSet());
+
+ var pluginClasses = context.SyntaxProvider
+ .CreateSyntaxProvider(
+ predicate: (n, _) => n is ClassDeclarationSyntax,
+ transform: GetPluginClassInfo)
+ .Where(info => info != null)
+ .Collect();
+
+ var compilation = context.CompilationProvider;
+
+ var combined = localizedStrings.Combine(invocationKeys).Combine(pluginClasses).Combine(compilation).Combine(xamlFiles.Collect());
+
+ context.RegisterSourceOutput(combined, Execute);
}
- public void Execute(GeneratorExecutionContext context)
+ ///
+ /// Executes the generation of string properties based on the provided data.
+ ///
+ /// The source production context.
+ /// The provided data.
+ private void Execute(SourceProductionContext spc,
+ ((((ImmutableArray LocalizableStrings,
+ ImmutableHashSet InvocationKeys),
+ ImmutableArray PluginClassInfos),
+ Compilation Compilation),
+ ImmutableArray AdditionalTexts) data)
{
- _optimizationLevel = context.Compilation.Options.OptimizationLevel;
-
- context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(
- XamlCustomPathPropertyKey,
- out var langFilePathEndsWithStr
- );
-
- var allLanguageKeys = new List();
- context.Compilation.SyntaxTrees
- .SelectMany(v => v.GetRoot().DescendantNodes().OfType())
- .ToList()
- .ForEach(
- v =>
- {
- var split = v.Expression.ToString().Split('.');
- if (split.Length < 2) return;
- if (split[0] != ClassName) return;
- allLanguageKeys.Add(split[1]);
- });
-
- var allXamlFiles = context.AdditionalFiles
- .Where(v => _languagesXamlRegex.IsMatch(v.Path))
- .ToArray();
- AdditionalText[] resourceDictionaries;
- if (allXamlFiles.Length is 0)
+ var xamlFiles = data.AdditionalTexts;
+ if (xamlFiles.Length == 0)
{
- context.ReportDiagnostic(Diagnostic.Create(
+ spc.ReportDiagnostic(Diagnostic.Create(
SourceGeneratorDiagnostics.CouldNotFindResourceDictionaries,
Location.None
));
return;
}
- if (string.IsNullOrEmpty(langFilePathEndsWithStr))
+ var compilation = data.Item1.Compilation;
+ var pluginClasses = data.Item1.Item1.PluginClassInfos;
+ var usedKeys = data.Item1.Item1.Item1.InvocationKeys;
+ var localizedStrings = data.Item1.Item1.Item1.LocalizableStrings;
+
+ var assemblyName = compilation.AssemblyName ?? DefaultNamespace;
+ var optimizationLevel = compilation.Options.OptimizationLevel;
+
+ var pluginInfo = GetValidPluginInfo(pluginClasses, spc);
+ var isCoreAssembly = assemblyName == CoreNamespace1 || assemblyName == CoreNamespace2;
+
+ GenerateSource(
+ spc,
+ xamlFiles[0],
+ localizedStrings,
+ optimizationLevel,
+ assemblyName,
+ isCoreAssembly,
+ pluginInfo,
+ usedKeys);
+ }
+
+ #endregion
+
+ #region Parse Xaml File
+
+ private static ImmutableArray ParseXamlFile(AdditionalText file, CancellationToken ct)
+ {
+ var content = file.GetText(ct)?.ToString();
+ if (content is null)
{
- if (allXamlFiles.Length is 1)
- {
- resourceDictionaries = allXamlFiles;
- }
- else
- {
- resourceDictionaries = allXamlFiles.Where(v => v.Path.EndsWith(DefaultLanguageFilePathEndsWith)).ToArray();
- if (resourceDictionaries.Length is 0)
- {
- context.ReportDiagnostic(Diagnostic.Create(
- SourceGeneratorDiagnostics.CouldNotFindResourceDictionaries,
- Location.None
- ));
- return;
- }
- }
+ return ImmutableArray.Empty;
}
- else
+
+ var doc = XDocument.Parse(content);
+ var systemNs = doc.Root?.GetNamespaceOfPrefix(XamlPrefix); // Should be "system"
+ var xNs = doc.Root?.GetNamespaceOfPrefix("x");
+ if (systemNs is null || xNs is null)
{
- var langFilePathEndings = langFilePathEndsWithStr
- .Trim()
- .Split(_xamlCustomPathPropertyDelimiters)
- .Select(v => v.Trim())
- .ToArray();
- resourceDictionaries = allXamlFiles.Where(v => langFilePathEndings.Any(v.Path.EndsWith)).ToArray();
- if (resourceDictionaries.Length is 0)
- {
- context.ReportDiagnostic(Diagnostic.Create(
- SourceGeneratorDiagnostics.CouldNotFindResourceDictionaries,
- Location.None
- ));
- return;
- }
+ return ImmutableArray.Empty;
}
- var ns = context.Compilation.AssemblyName ?? DefaultNamespace;
-
- var localizedStrings = LoadLocalizedStrings(resourceDictionaries);
-
- var unusedLocalizationKeys = localizedStrings.Keys.Except(allLanguageKeys).ToArray();
+ var localizableStrings = new List();
+ foreach (var element in doc.Descendants(systemNs + XamlTag)) // "String" elements in system namespace
+ {
+ if (ct.IsCancellationRequested)
+ {
+ return ImmutableArray.Empty;
+ }
- foreach (var key in unusedLocalizationKeys)
- context.ReportDiagnostic(Diagnostic.Create(
- SourceGeneratorDiagnostics.LocalizationKeyUnused,
- Location.None,
- key
- ));
+ var key = element.Attribute(xNs + "Key")?.Value; // Correctly get x:Key
+ var value = element.Value;
+ var comment = element.PreviousNode as XComment;
- var sourceCode = GenerateSourceCode(localizedStrings, context, unusedLocalizationKeys);
+ if (key != null)
+ {
+ localizableStrings.Add(ParseLocalizableString(key, value, comment));
+ }
+ }
- context.AddSource($"{ClassName}.{ns}.g.cs", SourceText.From(sourceCode, Encoding.UTF8));
+ return localizableStrings.ToImmutableArray();
}
- private static Dictionary LoadLocalizedStrings(AdditionalText[] files)
+ private static LocalizableString ParseLocalizableString(string key, string value, XComment comment)
{
- var result = new Dictionary();
+ var (summary, parameters) = ParseComment(comment);
+ return new LocalizableString(key, value, summary, parameters);
+ }
- foreach (var file in files)
+ private static (string Summary, ImmutableArray Parameters) ParseComment(XComment comment)
+ {
+ if (comment == null || comment.Value == null)
{
- ProcessXamlFile(file, result);
+ return (null, ImmutableArray.Empty);
}
- return result;
+ try
+ {
+ var doc = XDocument.Parse($"{comment.Value}");
+ var summary = doc.Descendants("summary").FirstOrDefault()?.Value.Trim();
+ var parameters = doc.Descendants("param")
+ .Select(p => new LocalizableStringParam(
+ int.Parse(p.Attribute("index").Value),
+ p.Attribute("name").Value,
+ p.Attribute("type").Value))
+ .ToImmutableArray();
+ return (summary, parameters);
+ }
+ catch
+ {
+ return (null, ImmutableArray.Empty);
+ }
}
- private static void ProcessXamlFile(AdditionalText file, Dictionary result) {
- var content = file.GetText()?.ToString();
- if (content is null) return;
- var doc = XDocument.Parse(content);
- var ns = doc.Root?.GetNamespaceOfPrefix(XamlPrefix);
- if (ns is null) return;
- foreach (var element in doc.Descendants(ns + XamlTag))
- {
- var name = element.FirstAttribute?.Value;
- var value = element.Value;
+ #endregion
- if (name is null) continue;
+ #region Get Used Localization Keys
- string summary = null;
- var paramsList = new List();
- var commentNode = element.PreviousNode;
+ // TODO: Add support for usedKeys
+ private static string GetLocalizationKeyFromInvocation(GeneratorSyntaxContext context, CancellationToken ct)
+ {
+ if (ct.IsCancellationRequested)
+ {
+ return null;
+ }
- if (commentNode is XComment comment)
- summary = ProcessXamlFileComment(comment, paramsList);
+ var invocation = (InvocationExpressionSyntax)context.Node;
+ var expression = invocation.Expression;
+ var parts = new List();
- result[name] = new LocalizableString(name, value, summary, paramsList);
+ // Traverse the member access hierarchy
+ while (expression is MemberAccessExpressionSyntax memberAccess)
+ {
+ parts.Add(memberAccess.Name.Identifier.Text);
+ expression = memberAccess.Expression;
}
- }
- private static string ProcessXamlFileComment(XComment comment, List paramsList) {
- string summary = null;
- try
+ // Add the leftmost identifier
+ if (expression is IdentifierNameSyntax identifier)
{
- if (CommentIncludesDocumentationMarkup(comment))
- {
- var commentDoc = XDocument.Parse($"{comment.Value}");
- summary = ExtractDocumentationCommentSummary(commentDoc);
- foreach (var param in commentDoc.Descendants("param"))
- {
- if (!int.TryParse(param.Attribute("index")?.Value, out var index))
- {
- index = -1;
- }
- var paramName = param.Attribute("name")?.Value;
- var paramType = param.Attribute("type")?.Value;
- if (index < 0 || paramName is null || paramType is null) continue;
- paramsList.Add(new LocalizableStringParam(index, paramName, paramType));
- }
- }
+ parts.Add(identifier.Identifier.Text);
+ }
+ else
+ {
+ return null;
}
- catch (Exception ex)
+
+ // Reverse to get [ClassName, SubClass, Method] from [Method, SubClass, ClassName]
+ parts.Reverse();
+
+ // Check if the first part is ClassName and there's at least one more part
+ if (parts.Count < 2 || parts[0] != ClassName)
{
- // ignore
- Console.WriteLine($"Exception in ProcessXamlFileComment: {ex.Message}");
+ return null;
}
- return summary;
+ return parts[1];
}
- private static string ExtractDocumentationCommentSummary(XDocument commentDoc) {
- return commentDoc.Descendants("summary").FirstOrDefault()?.Value.Trim();
- }
+ #endregion
- private static bool CommentIncludesDocumentationMarkup(XComment comment) {
- return comment.Value.Contains("") || comment.Value.Contains(" localizedStrings,
- GeneratorExecutionContext context,
- string[] unusedLocalizationKeys
- )
+ private static PluginClassInfo GetPluginClassInfo(GeneratorSyntaxContext context, CancellationToken ct)
{
- var ns = context.Compilation.AssemblyName;
-
- var sb = new StringBuilder();
- if (ns is CoreNamespace1 || ns is CoreNamespace2)
+ var classDecl = (ClassDeclarationSyntax)context.Node;
+ var location = GetLocation(context.SemanticModel.SyntaxTree, classDecl);
+ if (!classDecl.BaseList?.Types.Any(t => t.Type.ToString() == PluginInterfaceName) ?? true)
{
- GenerateFileHeader(sb, context);
- GenerateClass(sb, localizedStrings, unusedLocalizationKeys);
- return sb.ToString();
+ // Cannot find class that implements IPluginI18n
+ return null;
}
- string contextPropertyName = null;
- var mainClassFound = false;
- foreach (var (syntaxTree, classDeclaration) in GetClasses(context))
+ var property = classDecl.Members
+ .OfType()
+ .FirstOrDefault(p => p.Type.ToString() == PluginContextTypeName);
+ if (property is null)
{
- if (!DoesClassImplementInterface(classDeclaration, PluginInterfaceName))
- continue;
+ // Cannot find context
+ return new PluginClassInfo(location, classDecl.Identifier.Text, null, false, false, false);
+ }
- mainClassFound = true;
+ var modifiers = property.Modifiers;
+ return new PluginClassInfo(
+ location,
+ classDecl.Identifier.Text,
+ property.Identifier.Text,
+ modifiers.Any(SyntaxKind.StaticKeyword),
+ modifiers.Any(SyntaxKind.PrivateKeyword),
+ modifiers.Any(SyntaxKind.ProtectedKeyword));
+ }
+
+ private static PluginClassInfo GetValidPluginInfo(
+ ImmutableArray pluginClasses,
+ SourceProductionContext context)
+ {
+ var nonNullExistClasses = pluginClasses.Where(p => p != null && p.PropertyName != null).ToArray();
+ if (nonNullExistClasses.Length == 0)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.CouldNotFindPluginEntryClass,
+ Location.None
+ ));
+ return null;
+ }
- var property = GetPluginContextProperty(classDeclaration);
- if (property is null)
+ foreach (var pluginClass in nonNullExistClasses)
+ {
+ if (pluginClass.IsValid == true)
{
- context.ReportDiagnostic(Diagnostic.Create(
- SourceGeneratorDiagnostics.CouldNotFindContextProperty,
- GetLocation(syntaxTree, classDeclaration),
- classDeclaration.Identifier
- ));
- return string.Empty;
+ return pluginClass;
}
- var propertyModifiers = GetPropertyModifiers(property);
-
- if (!propertyModifiers.Static)
+ if (!pluginClass.IsStatic)
{
context.ReportDiagnostic(Diagnostic.Create(
SourceGeneratorDiagnostics.ContextPropertyNotStatic,
- GetLocation(syntaxTree, property),
- property.Identifier
+ pluginClass.Location,
+ pluginClass.PropertyName
));
- return string.Empty;
}
- if (propertyModifiers.Private)
+ if (pluginClass.IsPrivate)
{
context.ReportDiagnostic(Diagnostic.Create(
SourceGeneratorDiagnostics.ContextPropertyIsPrivate,
- GetLocation(syntaxTree, property),
- property.Identifier
+ pluginClass.Location,
+ pluginClass.PropertyName
));
- return string.Empty;
}
- if (propertyModifiers.Protected)
+ if (pluginClass.IsProtected)
{
context.ReportDiagnostic(Diagnostic.Create(
SourceGeneratorDiagnostics.ContextPropertyIsProtected,
- GetLocation(syntaxTree, property),
- property.Identifier
+ pluginClass.Location,
+ pluginClass.PropertyName
));
- return string.Empty;
}
+ }
+
+ return null;
+ }
+
+ private static Location GetLocation(SyntaxTree syntaxTree, CSharpSyntaxNode classDeclaration)
+ {
+ return Location.Create(syntaxTree, classDeclaration.GetLocation().SourceSpan);
+ }
+
+#endregion
+
+ #region Generate Source
+
+ private static void GenerateSource(
+ SourceProductionContext spc,
+ AdditionalText xamlFile,
+ ImmutableArray localizedStrings,
+ OptimizationLevel optimizationLevel,
+ string assemblyName,
+ bool isCoreAssembly,
+ PluginClassInfo pluginInfo,
+ IEnumerable usedKeys)
+ {
+ // Get unusedKeys if we need to optimize
+ IEnumerable unusedKeys = new List();
+ if (optimizationLevel == OptimizationLevel.Release)
+ {
+ unusedKeys = localizedStrings
+ .Select(ls => ls.Key)
+ .ToImmutableHashSet()
+ .Except(usedKeys);
- contextPropertyName = $"{classDeclaration.Identifier}.{property.Identifier}";
- break;
+ foreach (var key in unusedKeys)
+ {
+ spc.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.LocalizationKeyUnused,
+ Location.None,
+ key));
+ }
}
- if (mainClassFound is false)
+ var sourceBuilder = new StringBuilder();
+
+ // Generate header
+ GeneratedHeaderFromPath(sourceBuilder, xamlFile.Path);
+ sourceBuilder.AppendLine();
+
+ // Generate usings
+ if (isCoreAssembly)
{
- context.ReportDiagnostic(Diagnostic.Create(
- SourceGeneratorDiagnostics.CouldNotFindPluginEntryClass,
- Location.None
- ));
- return string.Empty;
+ sourceBuilder.AppendLine("using Flow.Launcher.Core.Resource;");
+ sourceBuilder.AppendLine();
}
- GenerateFileHeader(sb, context, true);
- GenerateClass(sb, localizedStrings, unusedLocalizationKeys, contextPropertyName);
- return sb.ToString();
- }
+ // Generate nullable enable
+ sourceBuilder.AppendLine("#nullable enable");
+ sourceBuilder.AppendLine();
- private static void GenerateFileHeader(StringBuilder sb, GeneratorExecutionContext context, bool isPlugin = false)
- {
- var rootNamespace = context.Compilation.AssemblyName;
- sb.AppendLine("// ");
- sb.AppendLine("#nullable enable");
+ // Generate namespace
+ sourceBuilder.AppendLine($"namespace {assemblyName};");
+ sourceBuilder.AppendLine();
- if (!isPlugin)
- sb.AppendLine("using Flow.Launcher.Core.Resource;");
+ // Uncomment them for debugging
+ //sourceBuilder.AppendLine("/*");
+ /*// Generate all localization strings
+ sourceBuilder.AppendLine("localizedStrings");
+ foreach (var ls in localizedStrings)
+ {
+ sourceBuilder.AppendLine($"{ls.Key} - {ls.Value}");
+ }
+ sourceBuilder.AppendLine();
- sb.AppendLine($"namespace {rootNamespace};");
- }
+ // Generate all unused keys
+ sourceBuilder.AppendLine("unusedKeys");
+ foreach (var key in unusedKeys)
+ {
+ sourceBuilder.AppendLine($"{key}");
+ }
+ sourceBuilder.AppendLine();
- private void GenerateClass(
- StringBuilder sb,
- Dictionary localizedStrings,
- string[] unusedLocalizationKeys,
- string propertyName = null
- ) {
- const string name = nameof(LocalizeSourceGenerator);
- var version = typeof(LocalizeSourceGenerator).Assembly.GetName().Version;
- sb.AppendLine();
- sb.AppendLine($"[System.CodeDom.Compiler.GeneratedCode(\"{name}\", \"{version}\")]");
- sb.AppendLine($"public static class {ClassName}");
- sb.AppendLine("{");
- foreach (var localizedString in localizedStrings)
+ // Generate all used keys
+ sourceBuilder.AppendLine("usedKeys");
+ foreach (var key in usedKeys)
{
- if (_optimizationLevel == OptimizationLevel.Release && unusedLocalizationKeys.Contains(localizedString.Key))
+ sourceBuilder.AppendLine($"{key}");
+ }*/
+ //sourceBuilder.AppendLine("*/");
+
+ // Generate class
+ sourceBuilder.AppendLine($"[System.CodeDom.Compiler.GeneratedCode(\"{nameof(LocalizeSourceGenerator)}\", \"{PackageVersion}\")]");
+ sourceBuilder.AppendLine($"public static class {ClassName}");
+ sourceBuilder.AppendLine("{");
+
+ // Generate localization methods
+ var tabString = Spacing(1);
+ foreach (var ls in localizedStrings)
+ {
+ // TODO: Add support for usedKeys
+ /*if (unusedKeys.Contains(ls.Key))
+ {
continue;
+ }*/
- GenerateDocCommentForMethod(sb, localizedString.Value);
- GenerateMethod(sb, localizedString.Value, propertyName);
+ GenerateDocComments(sourceBuilder, ls, tabString);
+ GenerateLocalizationMethod(sourceBuilder, ls, isCoreAssembly, pluginInfo, tabString);
}
- sb.AppendLine("}");
+ sourceBuilder.AppendLine("}");
+
+ // Add source to context
+ spc.AddSource($"{ClassName}.{assemblyName}.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
}
- private static void GenerateDocCommentForMethod(StringBuilder sb, LocalizableString localizableString)
+ private static void GeneratedHeaderFromPath(StringBuilder sb, string xamlFilePath)
{
- sb.AppendLine("/// ");
- if (!(localizableString.Summary is null))
+ if (string.IsNullOrEmpty(xamlFilePath))
{
- sb.AppendLine(string.Join("\n", localizableString.Summary.Trim().Split('\n').Select(v => $"/// {v}")));
+ sb.AppendLine("/// ");
}
-
- sb.AppendLine("/// ");
- var value = localizableString.Value;
- foreach (var p in localizableString.Params)
+ else
{
- value = value.Replace($"{{{p.Index}}}", $"{{{p.Name}}}");
+ sb.AppendLine("/// ")
+ .AppendLine($"/// From: {xamlFilePath}")
+ .AppendLine("/// ");
}
- sb.AppendLine(string.Join("\n", value.Split('\n').Select(v => $"/// {v}")));
- sb.AppendLine("///
");
- sb.AppendLine("/// ");
}
- private static void GenerateMethod(StringBuilder sb, LocalizableString localizableString, string contextPropertyName)
+ private static void GenerateDocComments(StringBuilder sb, LocalizableString ls, string tabString)
{
- sb.Append($"public static string {localizableString.Key}(");
- var declarationArgs = new List();
- var callArgs = new List();
- for (var i = 0; i < 10; i++)
+ if (ls.Summary != null)
{
- if (localizableString.Value.Contains($"{{{i}}}"))
- {
- var param = localizableString.Params.FirstOrDefault(v => v.Index == i);
- if (!(param is null))
- {
- declarationArgs.Add($"{param.Type} {param.Name}");
- callArgs.Add(param.Name);
- }
- else
- {
- declarationArgs.Add($"object? arg{i}");
- callArgs.Add($"arg{i}");
- }
- }
- else
+ sb.AppendLine($"{tabString}/// ");
+ foreach (var line in ls.Summary.Split('\n'))
{
- break;
+ sb.AppendLine($"{tabString}/// {line.Trim()}");
}
+ sb.AppendLine($"{tabString}/// ");
}
- string callArray;
- switch (callArgs.Count)
+ sb.AppendLine($"{tabString}/// ");
+ foreach (var line in ls.Value.Split('\n'))
{
- case 0:
- callArray = "";
- break;
- case 1:
- callArray = callArgs[0];
- break;
- default:
- callArray = $"new object?[] {{ {string.Join(", ", callArgs)} }}";
- break;
+ sb.AppendLine($"{tabString}/// {line.Trim()}");
}
+ sb.AppendLine($"{tabString}///
");
+ }
- sb.Append(string.Join(", ", declarationArgs));
+ private static void GenerateLocalizationMethod(
+ StringBuilder sb,
+ LocalizableString ls,
+ bool isCoreAssembly,
+ PluginClassInfo pluginInfo,
+ string tabString)
+ {
+ sb.Append($"{tabString}public static string {ls.Key}(");
+ var parameters = BuildParameters(ls);
+ sb.Append(string.Join(", ", parameters.Select(p => $"{p.Type} {p.Name}")));
sb.Append(") => ");
- if (contextPropertyName is null)
+
+ var formatArgs = parameters.Count > 0
+ ? $", {string.Join(", ", parameters.Select(p => p.Name))}"
+ : string.Empty;
+
+ if (isCoreAssembly)
{
- if (string.IsNullOrEmpty(callArray))
- {
- sb.AppendLine($"InternationalizationManager.Instance.GetTranslation(\"{localizableString.Key}\");");
- }
- else
- {
- sb.AppendLine(
- $"string.Format(InternationalizationManager.Instance.GetTranslation(\"{localizableString.Key}\"), {callArray});"
- );
- }
+ sb.AppendLine(parameters.Count > 0
+ ? $"string.Format(InternationalizationManager.Instance.GetTranslation(\"{ls.Key}\"){formatArgs});"
+ : $"InternationalizationManager.Instance.GetTranslation(\"{ls.Key}\");");
+ }
+ else if (pluginInfo?.IsValid == true)
+ {
+ sb.AppendLine(parameters.Count > 0
+ ? $"string.Format({pluginInfo.ContextAccessor}.API.GetTranslation(\"{ls.Key}\"){formatArgs});"
+ : $"{pluginInfo.ContextAccessor}.API.GetTranslation(\"{ls.Key}\");");
}
else
{
- if (string.IsNullOrEmpty(callArray))
- {
- sb.AppendLine($"{contextPropertyName}.API.GetTranslation(\"{localizableString.Key}\");");
- }
- else
- {
- sb.AppendLine($"string.Format({contextPropertyName}.API.GetTranslation(\"{localizableString.Key}\"), {callArray});");
- }
+ sb.AppendLine("\"LOCALIZATION_ERROR\";");
}
sb.AppendLine();
}
- private static Location GetLocation(SyntaxTree syntaxTree, CSharpSyntaxNode classDeclaration)
- {
- return Location.Create(syntaxTree, classDeclaration.GetLocation().SourceSpan);
- }
-
- private static IEnumerable<(SyntaxTree, ClassDeclarationSyntax)> GetClasses(GeneratorExecutionContext context)
+ private static List BuildParameters(LocalizableString ls)
{
- foreach (var syntaxTree in context.Compilation.SyntaxTrees)
+ var parameters = new List();
+ for (var i = 0; i < 10; i++)
{
- var classDeclarations = syntaxTree.GetRoot().DescendantNodes().OfType();
- foreach (var classDeclaration in classDeclarations)
+ if (!ls.Value.Contains($"{{{i}}}"))
{
- yield return (syntaxTree, classDeclaration);
+ continue;
}
+
+ var param = ls.Params.FirstOrDefault(p => p.Index == i);
+ parameters.Add(param is null
+ ? new MethodParameter($"arg{i}", "object?")
+ : new MethodParameter(param.Name, param.Type));
}
+ return parameters;
}
- private static bool DoesClassImplementInterface(ClassDeclarationSyntax classDeclaration, string interfaceName)
+ private static string Spacing(int n)
{
- return classDeclaration.BaseList?.Types.Any(v => interfaceName == v.ToString()) is true;
+ Span spaces = stackalloc char[n * 4];
+ spaces.Fill(' ');
+
+ var sb = new StringBuilder(n * 4);
+ foreach (var c in spaces)
+ {
+ _ = sb.Append(c);
+ }
+
+ return sb.ToString();
}
- private static PropertyDeclarationSyntax GetPluginContextProperty(ClassDeclarationSyntax classDeclaration)
+ #endregion
+
+ #region Classes
+
+ public class MethodParameter
{
- return classDeclaration.Members
- .OfType()
- .FirstOrDefault(v => v.Type.ToString() is PluginContextTypeName);
+ public string Name { get; }
+ public string Type { get; }
+
+ public MethodParameter(string name, string type)
+ {
+ Name = name;
+ Type = type;
+ }
}
- private static Modifiers GetPropertyModifiers(PropertyDeclarationSyntax property)
+ public class LocalizableStringParam
{
- var isStatic = property.Modifiers.Any(v => v.Text is KeywordStatic);
- var isPrivate = property.Modifiers.Any(v => v.Text is KeywordPrivate);
- var isProtected = property.Modifiers.Any(v => v.Text is KeywordProtected);
+ public int Index { get; }
+ public string Name { get; }
+ public string Type { get; }
- return new Modifiers(isStatic, isPrivate, isProtected);
+ public LocalizableStringParam(int index, string name, string type)
+ {
+ Index = index;
+ Name = name;
+ Type = type;
+ }
}
- private class Modifiers
+ public class LocalizableString
{
- public bool Static { get; }
- public bool Private { get; }
- public bool Protected { get; }
+ public string Key { get; }
+ public string Value { get; }
+ public string Summary { get; }
+ public IEnumerable Params { get; }
- public Modifiers(bool isStatic = false, bool isPrivate = false, bool isProtected = false)
+ public LocalizableString(string key, string value, string summary, IEnumerable @params)
{
- Static = isStatic;
- Private = isPrivate;
- Protected = isProtected;
+ Key = key;
+ Value = value;
+ Summary = summary;
+ Params = @params;
}
}
- }
- public class LocalizableStringParam
- {
- public int Index { get; }
- public string Name { get; }
- public string Type { get; }
-
- public LocalizableStringParam(int index, string name, string type)
+ public class PluginClassInfo
{
- Index = index;
- Name = name;
- Type = type;
- }
- }
+ public Location Location { get; }
+ public string ClassName { get; }
+ public string PropertyName { get; }
+ public bool IsStatic { get; }
+ public bool IsPrivate { get; }
+ public bool IsProtected { get; }
- public class LocalizableString
- {
- public string Key { get; }
- public string Value { get; }
- public string Summary { get; }
- public IEnumerable Params { get; }
+ public string ContextAccessor => $"{ClassName}.{PropertyName}";
+ public bool IsValid => PropertyName != null && IsStatic && (!IsPrivate) && (!IsProtected);
- public LocalizableString(string key, string value, string summary, IEnumerable @params)
- {
- Key = key;
- Value = value;
- Summary = summary;
- Params = @params;
+ public PluginClassInfo(Location location, string className, string propertyName, bool isStatic, bool isPrivate, bool isProtected)
+ {
+ Location = location;
+ ClassName = className;
+ PropertyName = propertyName;
+ IsStatic = isStatic;
+ IsPrivate = isPrivate;
+ IsProtected = isProtected;
+ }
}
+
+ #endregion
}
}