diff --git a/Flow.Launcher.Localization.Attributes/EnumLocalizeAttribute.cs b/Flow.Launcher.Localization.Attributes/EnumLocalizeAttribute.cs
new file mode 100644
index 0000000..21f2a06
--- /dev/null
+++ b/Flow.Launcher.Localization.Attributes/EnumLocalizeAttribute.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Flow.Launcher.Localization.Attributes
+{
+ ///
+ /// Attribute to mark an enum for localization.
+ ///
+ [AttributeUsage(AttributeTargets.Enum)]
+ public class EnumLocalizeAttribute : Attribute
+ {
+ }
+}
diff --git a/Flow.Launcher.Localization.Attributes/EnumLocalizeKeyAttribute.cs b/Flow.Launcher.Localization.Attributes/EnumLocalizeKeyAttribute.cs
new file mode 100644
index 0000000..39d75eb
--- /dev/null
+++ b/Flow.Launcher.Localization.Attributes/EnumLocalizeKeyAttribute.cs
@@ -0,0 +1,33 @@
+using System;
+
+namespace Flow.Launcher.Localization.Attributes
+{
+ ///
+ /// Attribute to mark a localization key for an enum field.
+ ///
+ [AttributeUsage(AttributeTargets.Field)]
+ public class EnumLocalizeKeyAttribute : Attribute
+ {
+ public static readonly EnumLocalizeKeyAttribute Default = new EnumLocalizeKeyAttribute();
+
+ public EnumLocalizeKeyAttribute() : this(string.Empty)
+ {
+ }
+
+ public EnumLocalizeKeyAttribute(string enumLocalizeKey)
+ {
+ EnumLocalizeKey = enumLocalizeKey;
+ }
+
+ public virtual string LocalizeKey => EnumLocalizeKey;
+
+ protected string EnumLocalizeKey { get; set; }
+
+ public override bool Equals(object obj) =>
+ obj is EnumLocalizeKeyAttribute other && other.LocalizeKey == LocalizeKey;
+
+ public override int GetHashCode() => LocalizeKey?.GetHashCode() ?? 0;
+
+ public override bool IsDefaultAttribute() => Equals(Default);
+ }
+}
diff --git a/Flow.Launcher.Localization.Attributes/EnumLocalizeValueAttribute.cs b/Flow.Launcher.Localization.Attributes/EnumLocalizeValueAttribute.cs
new file mode 100644
index 0000000..75c3859
--- /dev/null
+++ b/Flow.Launcher.Localization.Attributes/EnumLocalizeValueAttribute.cs
@@ -0,0 +1,33 @@
+using System;
+
+namespace Flow.Launcher.Localization.Attributes
+{
+ ///
+ /// Attribute to mark a localization value for an enum field.
+ ///
+ [AttributeUsage(AttributeTargets.Field)]
+ public class EnumLocalizeValueAttribute : Attribute
+ {
+ public static readonly EnumLocalizeValueAttribute Default = new EnumLocalizeValueAttribute();
+
+ public EnumLocalizeValueAttribute() : this(string.Empty)
+ {
+ }
+
+ public EnumLocalizeValueAttribute(string enumLocalizeValue)
+ {
+ EnumLocalizeValue = enumLocalizeValue;
+ }
+
+ public virtual string LocalizeValue => EnumLocalizeValue;
+
+ protected string EnumLocalizeValue { get; set; }
+
+ public override bool Equals(object obj) =>
+ obj is EnumLocalizeValueAttribute other && other.LocalizeValue == LocalizeValue;
+
+ public override int GetHashCode() => LocalizeValue?.GetHashCode() ?? 0;
+
+ public override bool IsDefaultAttribute() => Equals(Default);
+ }
+}
diff --git a/Flow.Launcher.Localization.Attributes/Flow.Launcher.Localization.Attributes.csproj b/Flow.Launcher.Localization.Attributes/Flow.Launcher.Localization.Attributes.csproj
new file mode 100644
index 0000000..dd973d9
--- /dev/null
+++ b/Flow.Launcher.Localization.Attributes/Flow.Launcher.Localization.Attributes.csproj
@@ -0,0 +1,8 @@
+
+
+
+ netstandard2.0
+ Flow.Launcher.Localization.Attributes
+
+
+
diff --git a/Flow.Launcher.Localization.Shared/Constants.cs b/Flow.Launcher.Localization.Shared/Constants.cs
index 568da5d..8932aa9 100644
--- a/Flow.Launcher.Localization.Shared/Constants.cs
+++ b/Flow.Launcher.Localization.Shared/Constants.cs
@@ -20,6 +20,14 @@ public static class Constants
public const string OldLocalizationMethodName = "GetTranslation";
public const string StringFormatMethodName = "Format";
public const string StringFormatTypeName = "string";
+ public const string EnumLocalizeClassSuffix = "Data";
+ public const string EnumLocalizeAttributeName = "EnumLocalizeAttribute";
+ public const string EnumLocalizeKeyAttributeName = "EnumLocalizeKeyAttribute";
+ public const string EnumLocalizeValueAttributeName = "EnumLocalizeValueAttribute";
+ // Use PublicApi instead of PublicAPI for possible ambiguity with Flow.Launcher.Plugin.IPublicAPI
+ public const string PublicApiClassName = "PublicApi";
+ public const string PublicApiPrivatePropertyName = "instance";
+ public const string PublicApiInternalPropertyName = "Instance";
public static readonly Regex LanguagesXamlRegex = new Regex(@"\\Languages\\[^\\]+\.xaml$", RegexOptions.IgnoreCase);
public static readonly string[] OldLocalizationClasses = { "IPublicAPI", "Internationalization" };
diff --git a/Flow.Launcher.Localization.Shared/Helper.cs b/Flow.Launcher.Localization.Shared/Helper.cs
index d9502ac..0807cef 100644
--- a/Flow.Launcher.Localization.Shared/Helper.cs
+++ b/Flow.Launcher.Localization.Shared/Helper.cs
@@ -1,9 +1,9 @@
-using Microsoft.CodeAnalysis;
+using System.Linq;
+using System.Threading;
+using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
-using System.Linq;
-using System.Threading;
namespace Flow.Launcher.Localization.Shared
{
@@ -81,5 +81,14 @@ private static Location GetCodeFixLocation(PropertyDeclarationSyntax property, S
}
#endregion
+
+ #region Tab String
+
+ public static string Spacing(int n)
+ {
+ return new string(' ', n * 4);
+ }
+
+ #endregion
}
}
diff --git a/Flow.Launcher.Localization.SourceGenerators/AnalyzerReleases.Unshipped.md b/Flow.Launcher.Localization.SourceGenerators/AnalyzerReleases.Unshipped.md
index f60d258..c1ad7a3 100644
--- a/Flow.Launcher.Localization.SourceGenerators/AnalyzerReleases.Unshipped.md
+++ b/Flow.Launcher.Localization.SourceGenerators/AnalyzerReleases.Unshipped.md
@@ -11,3 +11,4 @@ FLSG0004 | Localization | Warning | FLSG0004_ContextPropertyNotStatic
FLSG0005 | Localization | Warning | FLSG0005_ContextPropertyIsPrivate
FLSG0006 | Localization | Warning | FLSG0006_ContextPropertyIsProtected
FLSG0007 | Localization | Warning | FLSG0007_LocalizationKeyUnused
+FLSG0008 | Localization | Warning | FLSG0008_EnumFieldLocalizationKeyValueInvalid
diff --git a/Flow.Launcher.Localization.SourceGenerators/Flow.Launcher.Localization.SourceGenerators.csproj b/Flow.Launcher.Localization.SourceGenerators/Flow.Launcher.Localization.SourceGenerators.csproj
index 1a95f4d..be6dda3 100644
--- a/Flow.Launcher.Localization.SourceGenerators/Flow.Launcher.Localization.SourceGenerators.csproj
+++ b/Flow.Launcher.Localization.SourceGenerators/Flow.Launcher.Localization.SourceGenerators.csproj
@@ -4,6 +4,8 @@
netstandard2.0
true
Flow.Launcher.Localization.SourceGenerators
+
+ 0.0.2
diff --git a/Flow.Launcher.Localization.SourceGenerators/Localize/EnumSourceGenerator.cs b/Flow.Launcher.Localization.SourceGenerators/Localize/EnumSourceGenerator.cs
new file mode 100644
index 0000000..76a218f
--- /dev/null
+++ b/Flow.Launcher.Localization.SourceGenerators/Localize/EnumSourceGenerator.cs
@@ -0,0 +1,358 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using Flow.Launcher.Localization.Shared;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Flow.Launcher.Localization.SourceGenerators.Localize
+{
+ [Generator]
+ public partial class EnumSourceGenerator : IIncrementalGenerator
+ {
+ #region Fields
+
+ private static readonly Version PackageVersion = typeof(EnumSourceGenerator).Assembly.GetName().Version;
+
+ private static readonly ImmutableArray _emptyEnumFields = ImmutableArray.Empty;
+
+ #endregion
+
+ #region Incremental Generator
+
+ ///
+ /// Initializes the generator and registers source output based on enum declarations.
+ ///
+ /// The initialization context.
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var enumDeclarations = context.SyntaxProvider
+ .CreateSyntaxProvider(
+ predicate: (s, _) => s is EnumDeclarationSyntax,
+ transform: (ctx, _) => (EnumDeclarationSyntax)ctx.Node)
+ .Where(ed => ed.AttributeLists.Count > 0)
+ .Collect();
+
+ var pluginClasses = context.SyntaxProvider
+ .CreateSyntaxProvider(
+ predicate: (n, _) => n is ClassDeclarationSyntax,
+ transform: (c, t) => Helper.GetPluginClassInfo((ClassDeclarationSyntax)c.Node, c.SemanticModel, t))
+ .Where(info => info != null)
+ .Collect();
+
+ var compilation = context.CompilationProvider;
+
+ var configOptions = context.AnalyzerConfigOptionsProvider;
+
+ var compilationEnums = enumDeclarations.Combine(pluginClasses).Combine(configOptions).Combine(compilation);
+
+ context.RegisterSourceOutput(compilationEnums, Execute);
+ }
+
+ ///
+ /// Executes the generation of enum data classes based on the provided data.
+ ///
+ /// The source production context.
+ /// The provided data.
+ private void Execute(SourceProductionContext spc,
+ (((ImmutableArray EnumsDeclarations,
+ ImmutableArray PluginClassInfos),
+ AnalyzerConfigOptionsProvider ConfigOptionsProvider),
+ Compilation Compilation) data)
+ {
+ var compilation = data.Compilation;
+ var configOptions = data.Item1.ConfigOptionsProvider;
+ var pluginClasses = data.Item1.Item1.PluginClassInfos;
+ var enumsDeclarations = data.Item1.Item1.EnumsDeclarations;
+
+ var assemblyNamespace = compilation.AssemblyName ?? Constants.DefaultNamespace;
+ var useDI = configOptions.GetFLLUseDependencyInjection();
+
+ PluginClassInfo pluginInfo;
+ if (useDI)
+ {
+ // If we use dependency injection, we do not need to check if there is a valid plugin context
+ pluginInfo = null;
+ }
+ else
+ {
+ pluginInfo = PluginInfoHelper.GetValidPluginInfoAndReportDiagnostic(pluginClasses, spc);
+ if (pluginInfo == null)
+ {
+ // If we cannot find a valid plugin info, we do not need to generate the source
+ return;
+ }
+ }
+
+ foreach (var enumDeclaration in enumsDeclarations.Distinct())
+ {
+ var semanticModel = compilation.GetSemanticModel(enumDeclaration.SyntaxTree);
+ var enumSymbol = semanticModel.GetDeclaredSymbol(enumDeclaration) as INamedTypeSymbol;
+
+ // Check if the enum has the EnumLocalize attribute
+ if (enumSymbol?.GetAttributes().Any(ad =>
+ ad.AttributeClass?.Name == Constants.EnumLocalizeAttributeName) ?? false)
+ {
+ GenerateSource(spc, enumSymbol, useDI, pluginInfo, assemblyNamespace);
+ }
+ }
+ }
+
+ #endregion
+
+ #region Get Enum Fields
+
+ private static ImmutableArray GetEnumFields(SourceProductionContext spc, INamedTypeSymbol enumSymbol, string enumFullName)
+ {
+ // Iterate through enum members and get enum fields
+ var enumFields = new List();
+ var enumError = false;
+ foreach (var member in enumSymbol.GetMembers().Where(m => m.Kind == SymbolKind.Field))
+ {
+ if (member is IFieldSymbol fieldSymbol)
+ {
+ var enumFieldName = fieldSymbol.Name;
+
+ // Check if the field has the EnumLocalizeKey attribute
+ var keyAttr = fieldSymbol.GetAttributes()
+ .FirstOrDefault(a => a.AttributeClass?.Name == Constants.EnumLocalizeKeyAttributeName);
+ var keyAttrExist = keyAttr != null;
+
+ // Check if the field has the EnumLocalizeValue attribute
+ var valueAttr = fieldSymbol.GetAttributes()
+ .FirstOrDefault(a => a.AttributeClass?.Name == Constants.EnumLocalizeValueAttributeName);
+ var valueAttrExist = valueAttr != null;
+
+ // Get the key and value from the attributes
+ var key = keyAttr?.ConstructorArguments.FirstOrDefault().Value?.ToString() ?? string.Empty;
+ var value = valueAttr?.ConstructorArguments.FirstOrDefault().Value?.ToString() ?? string.Empty;
+
+ // Users may use " " as a key, so we need to check if the key is not empty and not whitespace
+ if (keyAttrExist && !string.IsNullOrWhiteSpace(key))
+ {
+ // If localization key exists and is valid, use it
+ enumFields.Add(new EnumField(enumFieldName, key, valueAttrExist ? value : null));
+ }
+ else if (valueAttrExist)
+ {
+ // If localization value exists, use it
+ enumFields.Add(new EnumField(enumFieldName, value));
+ }
+ else
+ {
+ // If localization key and value are not provided, do not generate the field and report a diagnostic
+ spc.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.EnumFieldLocalizationKeyValueInvalid,
+ Location.None,
+ $"{enumFullName}.{enumFieldName}"));
+ enumError = true;
+ }
+ }
+ }
+
+ // If there was an error, do not generate the class
+ if (enumError) return _emptyEnumFields;
+
+ return enumFields.ToImmutableArray();
+ }
+
+ #endregion
+
+ #region Generate Source
+
+ private void GenerateSource(
+ SourceProductionContext spc,
+ INamedTypeSymbol enumSymbol,
+ bool useDI,
+ PluginClassInfo pluginInfo,
+ string assemblyNamespace)
+ {
+ var enumFullName = enumSymbol.ToDisplayString(new SymbolDisplayFormat(
+ globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, // Remove global:: symbol
+ typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces));
+ var enumDataClassName = $"{enumSymbol.Name}{Constants.EnumLocalizeClassSuffix}";
+ var enumName = enumSymbol.Name;
+ var enumNamespace = enumSymbol.ContainingNamespace.ToDisplayString();
+ var tabString = Helper.Spacing(1);
+
+ var sourceBuilder = new StringBuilder();
+
+ // Generate header
+ GeneratedHeaderFromPath(sourceBuilder, enumFullName);
+ sourceBuilder.AppendLine();
+
+ // Generate using directives
+ sourceBuilder.AppendLine("using System.Collections.Generic;");
+ sourceBuilder.AppendLine();
+
+ // Generate namespace
+ sourceBuilder.AppendLine($"namespace {enumNamespace};");
+ sourceBuilder.AppendLine();
+
+ // Generate class
+ sourceBuilder.AppendLine($"/// ");
+ sourceBuilder.AppendLine($"/// Data class for ");
+ sourceBuilder.AppendLine($"/// ");
+ sourceBuilder.AppendLine($"[System.CodeDom.Compiler.GeneratedCode(\"{nameof(EnumSourceGenerator)}\", \"{PackageVersion}\")]");
+ sourceBuilder.AppendLine($"public class {enumDataClassName}");
+ sourceBuilder.AppendLine("{");
+
+ // Generate properties
+ sourceBuilder.AppendLine($"{tabString}/// ");
+ sourceBuilder.AppendLine($"{tabString}/// The value of the enum");
+ sourceBuilder.AppendLine($"{tabString}/// ");
+ sourceBuilder.AppendLine($"{tabString}public {enumName} Value {{ get; private init; }}");
+ sourceBuilder.AppendLine();
+
+ sourceBuilder.AppendLine($"{tabString}/// ");
+ sourceBuilder.AppendLine($"{tabString}/// The display text of the enum value");
+ sourceBuilder.AppendLine($"{tabString}/// ");
+ sourceBuilder.AppendLine($"{tabString}public string Display {{ get; set; }}");
+ sourceBuilder.AppendLine();
+
+ sourceBuilder.AppendLine($"{tabString}/// ");
+ sourceBuilder.AppendLine($"{tabString}/// The localization key of the enum value");
+ sourceBuilder.AppendLine($"{tabString}/// ");
+ sourceBuilder.AppendLine($"{tabString}public string LocalizationKey {{ get; set; }}");
+ sourceBuilder.AppendLine();
+
+ sourceBuilder.AppendLine($"{tabString}/// ");
+ sourceBuilder.AppendLine($"{tabString}/// The localization value of the enum value");
+ sourceBuilder.AppendLine($"{tabString}/// ");
+ sourceBuilder.AppendLine($"{tabString}public string LocalizationValue {{ get; set; }}");
+ sourceBuilder.AppendLine();
+
+ // Generate API instance
+ string getTranslation = null;
+ if (useDI)
+ {
+ // Use instance from PublicApiSourceGenerator
+ getTranslation = $"{assemblyNamespace}.{Constants.PublicApiClassName}.{Constants.PublicApiInternalPropertyName}.GetTranslation";
+ }
+ else if (pluginInfo?.IsValid == true)
+ {
+ getTranslation = $"{assemblyNamespace}.{pluginInfo.ContextAccessor}.API.GetTranslation";
+ }
+
+ // Generate GetValues method
+ sourceBuilder.AppendLine($"{tabString}/// ");
+ sourceBuilder.AppendLine($"{tabString}/// Get all values of ");
+ sourceBuilder.AppendLine($"{tabString}/// ");
+ sourceBuilder.AppendLine($"{tabString}public static List<{enumDataClassName}> GetValues()");
+ sourceBuilder.AppendLine($"{tabString}{{");
+ sourceBuilder.AppendLine($"{tabString}{tabString}return new List<{enumDataClassName}>");
+ sourceBuilder.AppendLine($"{tabString}{tabString}{{");
+ var enumFields = GetEnumFields(spc, enumSymbol, enumFullName);
+ if (enumFields.Length == 0) return;
+ foreach (var enumField in enumFields)
+ {
+ GenerateEnumField(sourceBuilder, getTranslation, enumField, enumName, tabString);
+ }
+ sourceBuilder.AppendLine($"{tabString}{tabString}}};");
+ sourceBuilder.AppendLine($"{tabString}}}");
+ sourceBuilder.AppendLine();
+
+ // Generate UpdateLabels method
+ GenerateUpdateLabelsMethod(sourceBuilder, getTranslation, enumDataClassName, tabString);
+
+ sourceBuilder.AppendLine($"}}");
+
+ // Add source to context
+ spc.AddSource($"{Constants.ClassName}.{assemblyNamespace}.{enumNamespace}.{enumDataClassName}.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
+ }
+
+ private static void GeneratedHeaderFromPath(StringBuilder sb, string enumFullName)
+ {
+ if (string.IsNullOrEmpty(enumFullName))
+ {
+ sb.AppendLine("/// ");
+ }
+ else
+ {
+ sb.AppendLine("/// ")
+ .AppendLine($"/// From: {enumFullName}")
+ .AppendLine("/// ");
+ }
+ }
+
+ private static void GenerateEnumField(
+ StringBuilder sb,
+ string getTranslation,
+ EnumField enumField,
+ string enumName,
+ string tabString)
+ {
+ sb.AppendLine($"{tabString}{tabString}{tabString}new()");
+ sb.AppendLine($"{tabString}{tabString}{tabString}{{");
+ sb.AppendLine($"{tabString}{tabString}{tabString}{tabString}Value = {enumName}.{enumField.EnumFieldName},");
+ if (enumField.UseLocalizationKey)
+ {
+ sb.AppendLine($"{tabString}{tabString}{tabString}{tabString}Display = {getTranslation}(\"{enumField.LocalizationKey}\"),");
+ sb.AppendLine($"{tabString}{tabString}{tabString}{tabString}LocalizationKey = \"{enumField.LocalizationKey}\",");
+ }
+ else
+ {
+ sb.AppendLine($"{tabString}{tabString}{tabString}{tabString}Display = \"{enumField.LocalizationValue}\",");
+ }
+ if (enumField.LocalizationValue != null)
+ {
+ sb.AppendLine($"{tabString}{tabString}{tabString}{tabString}LocalizationValue = \"{enumField.LocalizationValue}\",");
+ }
+ sb.AppendLine($"{tabString}{tabString}{tabString}}},");
+ }
+
+ private static void GenerateUpdateLabelsMethod(
+ StringBuilder sb,
+ string getTranslation,
+ string enumDataClassName,
+ string tabString)
+ {
+ sb.AppendLine($"{tabString}/// ");
+ sb.AppendLine($"{tabString}/// Update the labels of the enum values when culture info changes.");
+ sb.AppendLine($"{tabString}/// See for more details");
+ sb.AppendLine($"{tabString}/// ");
+ sb.AppendLine($"{tabString}public static void UpdateLabels(List<{enumDataClassName}> options)");
+ sb.AppendLine($"{tabString}{{");
+ sb.AppendLine($"{tabString}{tabString}foreach (var item in options)");
+ sb.AppendLine($"{tabString}{tabString}{{");
+ // Users may use " " as a key, so we need to check if the key is not empty and not whitespace
+ sb.AppendLine($"{tabString}{tabString}{tabString}if (!string.IsNullOrWhiteSpace(item.LocalizationKey))");
+ sb.AppendLine($"{tabString}{tabString}{tabString}{{");
+ sb.AppendLine($"{tabString}{tabString}{tabString}{tabString}item.Display = {getTranslation}(item.LocalizationKey);");
+ sb.AppendLine($"{tabString}{tabString}{tabString}}}");
+ sb.AppendLine($"{tabString}{tabString}}}");
+ sb.AppendLine($"{tabString}}}");
+ }
+
+ #endregion
+
+ #region Classes
+
+ public class EnumField
+ {
+ public string EnumFieldName { get; set; }
+ public string LocalizationKey { get; set; }
+ public string LocalizationValue { get; set; }
+
+ // Users may use " " as a key, so we need to check if the key is not empty and not whitespace
+ public bool UseLocalizationKey => !string.IsNullOrWhiteSpace(LocalizationKey);
+
+ public EnumField(string enumFieldName, string localizationValue) : this(enumFieldName, null, localizationValue)
+ {
+ }
+
+ public EnumField(string enumFieldName, string localizationKey, string localizationValue)
+ {
+ EnumFieldName = enumFieldName;
+ LocalizationKey = localizationKey;
+ LocalizationValue = localizationValue;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs b/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
index 9b2e0ca..43589ce 100644
--- a/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
+++ b/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
@@ -97,16 +97,30 @@ private void Execute(SourceProductionContext spc,
var usedKeys = data.Item1.Item1.Item1.Item1.InvocationKeys;
var localizedStrings = data.Item1.Item1.Item1.Item1.LocalizableStrings;
- var assemblyName = compilation.AssemblyName ?? Constants.DefaultNamespace;
+ var assemblyNamespace = compilation.AssemblyName ?? Constants.DefaultNamespace;
var useDI = configOptions.GetFLLUseDependencyInjection();
- var pluginInfo = GetValidPluginInfo(pluginClasses, spc, useDI);
+ PluginClassInfo pluginInfo;
+ if (useDI)
+ {
+ // If we use dependency injection, we do not need to check if there is a valid plugin context
+ pluginInfo = null;
+ }
+ else
+ {
+ pluginInfo = PluginInfoHelper.GetValidPluginInfoAndReportDiagnostic(pluginClasses, spc);
+ if (pluginInfo == null)
+ {
+ // If we cannot find a valid plugin info, we do not need to generate the source
+ return;
+ }
+ }
GenerateSource(
spc,
xamlFiles[0],
localizedStrings,
- assemblyName,
+ assemblyNamespace,
useDI,
pluginInfo,
usedKeys);
@@ -421,95 +435,13 @@ private static string GetLocalizationKeyFromInvocation(GeneratorSyntaxContext co
#endregion
- #region Get Plugin Class Info
-
- private static PluginClassInfo GetValidPluginInfo(
- ImmutableArray pluginClasses,
- SourceProductionContext context,
- bool useDI)
- {
- // If p is null, this class does not implement IPluginI18n
- var iPluginI18nClasses = pluginClasses.Where(p => p != null).ToArray();
- if (iPluginI18nClasses.Length == 0)
- {
- context.ReportDiagnostic(Diagnostic.Create(
- SourceGeneratorDiagnostics.CouldNotFindPluginEntryClass,
- Location.None
- ));
- return null;
- }
-
- // If we use dependency injection, we do not need to check if there is a valid plugin context
- // Also we do not need to return the plugin info
- if (useDI)
- {
- return null;
- }
-
- // If p.PropertyName is null, this class does not have PluginInitContext property
- var iPluginI18nClassesWithContext = iPluginI18nClasses.Where(p => p.PropertyName != null).ToArray();
- if (iPluginI18nClassesWithContext.Length == 0)
- {
- foreach (var pluginClass in iPluginI18nClasses)
- {
- context.ReportDiagnostic(Diagnostic.Create(
- SourceGeneratorDiagnostics.CouldNotFindContextProperty,
- pluginClass.Location,
- pluginClass.ClassName
- ));
- }
- return null;
- }
-
- // Rest classes have implemented IPluginI18n and have PluginInitContext property
- // Check if the property is valid
- foreach (var pluginClass in iPluginI18nClassesWithContext)
- {
- if (pluginClass.IsValid == true)
- {
- return pluginClass;
- }
-
- if (!pluginClass.IsStatic)
- {
- context.ReportDiagnostic(Diagnostic.Create(
- SourceGeneratorDiagnostics.ContextPropertyNotStatic,
- pluginClass.Location,
- pluginClass.PropertyName
- ));
- }
-
- if (pluginClass.IsPrivate)
- {
- context.ReportDiagnostic(Diagnostic.Create(
- SourceGeneratorDiagnostics.ContextPropertyIsPrivate,
- pluginClass.Location,
- pluginClass.PropertyName
- ));
- }
-
- if (pluginClass.IsProtected)
- {
- context.ReportDiagnostic(Diagnostic.Create(
- SourceGeneratorDiagnostics.ContextPropertyIsProtected,
- pluginClass.Location,
- pluginClass.PropertyName
- ));
- }
- }
-
- return null;
- }
-
- #endregion
-
#region Generate Source
private static void GenerateSource(
SourceProductionContext spc,
AdditionalText xamlFile,
ImmutableArray localizedStrings,
- string assemblyName,
+ string assemblyNamespace,
bool useDI,
PluginClassInfo pluginInfo,
IEnumerable usedKeys)
@@ -538,7 +470,7 @@ private static void GenerateSource(
sourceBuilder.AppendLine();
// Generate namespace
- sourceBuilder.AppendLine($"namespace {assemblyName};");
+ sourceBuilder.AppendLine($"namespace {assemblyNamespace};");
sourceBuilder.AppendLine();
// Uncomment them for debugging
@@ -572,16 +504,14 @@ private static void GenerateSource(
sourceBuilder.AppendLine($"public static class {Constants.ClassName}");
sourceBuilder.AppendLine("{");
- var tabString = Spacing(1);
+ var tabString = Helper.Spacing(1);
// Generate API instance
string getTranslation = null;
if (useDI)
{
- sourceBuilder.AppendLine($"{tabString}private static Flow.Launcher.Plugin.IPublicAPI? api = null;");
- sourceBuilder.AppendLine($"{tabString}private static Flow.Launcher.Plugin.IPublicAPI Api => api ??= CommunityToolkit.Mvvm.DependencyInjection.Ioc.Default.GetRequiredService();");
- sourceBuilder.AppendLine();
- getTranslation = "Api.GetTranslation";
+ // Use instance from PublicApiSourceGenerator
+ getTranslation = $"{assemblyNamespace}.{Constants.PublicApiClassName}.{Constants.PublicApiInternalPropertyName}.GetTranslation";
}
else if (pluginInfo?.IsValid == true)
{
@@ -591,14 +521,15 @@ private static void GenerateSource(
// Generate localization methods
foreach (var ls in localizedStrings)
{
+ var isLast = ls.Equals(localizedStrings.Last());
GenerateDocComments(sourceBuilder, ls, tabString);
- GenerateLocalizationMethod(sourceBuilder, ls, getTranslation, tabString);
+ GenerateLocalizationMethod(sourceBuilder, ls, getTranslation, tabString, isLast);
}
sourceBuilder.AppendLine("}");
// Add source to context
- spc.AddSource($"{Constants.ClassName}.{assemblyName}.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
+ spc.AddSource($"{Constants.ClassName}.{assemblyNamespace}.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
}
private static void GeneratedHeaderFromPath(StringBuilder sb, string xamlFilePath)
@@ -649,7 +580,8 @@ private static void GenerateLocalizationMethod(
StringBuilder sb,
LocalizableString ls,
string getTranslation,
- string tabString)
+ string tabString,
+ bool last)
{
sb.Append($"{tabString}public static string {ls.Key}(");
@@ -674,21 +606,10 @@ private static void GenerateLocalizationMethod(
sb.AppendLine("\"LOCALIZATION_ERROR\";");
}
- sb.AppendLine();
- }
-
- private static string Spacing(int n)
- {
- Span spaces = stackalloc char[n * 4];
- spaces.Fill(' ');
-
- var sb = new StringBuilder(n * 4);
- foreach (var c in spaces)
+ if (!last)
{
- _ = sb.Append(c);
+ sb.AppendLine();
}
-
- return sb.ToString();
}
#endregion
diff --git a/Flow.Launcher.Localization.SourceGenerators/Localize/PublicApiSourceGenerator.cs b/Flow.Launcher.Localization.SourceGenerators/Localize/PublicApiSourceGenerator.cs
new file mode 100644
index 0000000..92593b7
--- /dev/null
+++ b/Flow.Launcher.Localization.SourceGenerators/Localize/PublicApiSourceGenerator.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Text;
+using Flow.Launcher.Localization.Shared;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Flow.Launcher.Localization.SourceGenerators.Localize
+{
+ [Generator]
+ public partial class PublicApiSourceGenerator : IIncrementalGenerator
+ {
+ #region Fields
+
+ private static readonly Version PackageVersion = typeof(PublicApiSourceGenerator).Assembly.GetName().Version;
+
+ #endregion
+
+ #region Incremental Generator
+
+ ///
+ /// Initializes the generator and registers source output based on build property FLLUseDependencyInjection.
+ ///
+ /// The initialization context.
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var compilation = context.CompilationProvider;
+
+ var configOptions = context.AnalyzerConfigOptionsProvider;
+
+ var compilationEnums = configOptions.Combine(compilation);
+
+ context.RegisterSourceOutput(compilationEnums, Execute);
+ }
+
+ ///
+ /// Executes the generation of public api property based on the provided data.
+ ///
+ /// The source production context.
+ /// The provided data.
+ private void Execute(SourceProductionContext spc,
+ (AnalyzerConfigOptionsProvider ConfigOptionsProvider, Compilation Compilation) data)
+ {
+ var compilation = data.Compilation;
+ var configOptions = data.ConfigOptionsProvider;
+
+ var assemblyNamespace = compilation.AssemblyName ?? Constants.DefaultNamespace;
+ var useDI = configOptions.GetFLLUseDependencyInjection();
+
+ // If we do not use dependency injection, we do not need to generate the public api property
+ if (!useDI) return;
+
+ GenerateSource(spc, assemblyNamespace);
+ }
+
+ #endregion
+
+ #region Generate Source
+
+ private void GenerateSource(
+ SourceProductionContext spc,
+ string assemblyNamespace)
+ {
+ var tabString = Helper.Spacing(1);
+
+ var sourceBuilder = new StringBuilder();
+
+ // Generate header
+ GeneratedHeaderFromPath(sourceBuilder);
+ sourceBuilder.AppendLine();
+
+ // Generate nullable enable
+ sourceBuilder.AppendLine("#nullable enable");
+ sourceBuilder.AppendLine();
+
+ // Generate namespace
+ sourceBuilder.AppendLine($"namespace {assemblyNamespace};");
+ sourceBuilder.AppendLine();
+
+ // Generate class
+ sourceBuilder.AppendLine($"[System.CodeDom.Compiler.GeneratedCode(\"{nameof(PublicApiSourceGenerator)}\", \"{PackageVersion}\")]");
+ sourceBuilder.AppendLine($"internal static class {Constants.PublicApiClassName}");
+ sourceBuilder.AppendLine("{");
+
+ // Generate properties
+ sourceBuilder.AppendLine($"{tabString}private static Flow.Launcher.Plugin.IPublicAPI? {Constants.PublicApiPrivatePropertyName} = null;");
+ sourceBuilder.AppendLine();
+ sourceBuilder.AppendLine($"{tabString}/// ");
+ sourceBuilder.AppendLine($"{tabString}/// Get instance");
+ sourceBuilder.AppendLine($"{tabString}/// ");
+ sourceBuilder.AppendLine($"{tabString}internal static Flow.Launcher.Plugin.IPublicAPI {Constants.PublicApiInternalPropertyName} =>" +
+ $"{Constants.PublicApiPrivatePropertyName} ??= CommunityToolkit.Mvvm.DependencyInjection.Ioc.Default.GetRequiredService();");
+ sourceBuilder.AppendLine($"}}");
+
+ // Add source to context
+ spc.AddSource($"{Constants.PublicApiClassName}.{assemblyNamespace}.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
+ }
+
+ private static void GeneratedHeaderFromPath(StringBuilder sb)
+ {
+ sb.AppendLine("/// ");
+ }
+
+ #endregion
+ }
+}
diff --git a/Flow.Launcher.Localization.SourceGenerators/PluginInfoHelper.cs b/Flow.Launcher.Localization.SourceGenerators/PluginInfoHelper.cs
new file mode 100644
index 0000000..a26ee57
--- /dev/null
+++ b/Flow.Launcher.Localization.SourceGenerators/PluginInfoHelper.cs
@@ -0,0 +1,80 @@
+using System.Collections.Immutable;
+using System.Linq;
+using Flow.Launcher.Localization.Shared;
+using Microsoft.CodeAnalysis;
+
+namespace Flow.Launcher.Localization.SourceGenerators
+{
+ internal class PluginInfoHelper
+ {
+ public static PluginClassInfo GetValidPluginInfoAndReportDiagnostic(
+ ImmutableArray pluginClasses,
+ SourceProductionContext context)
+ {
+ // If p is null, this class does not implement IPluginI18n
+ var iPluginI18nClasses = pluginClasses.Where(p => p != null).ToArray();
+ if (iPluginI18nClasses.Length == 0)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.CouldNotFindPluginEntryClass,
+ Location.None
+ ));
+ return null;
+ }
+
+ // If p.PropertyName is null, this class does not have PluginInitContext property
+ var iPluginI18nClassesWithContext = iPluginI18nClasses.Where(p => p.PropertyName != null).ToArray();
+ if (iPluginI18nClassesWithContext.Length == 0)
+ {
+ foreach (var pluginClass in iPluginI18nClasses)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.CouldNotFindContextProperty,
+ pluginClass.Location,
+ pluginClass.ClassName
+ ));
+ }
+ return null;
+ }
+
+ // Rest classes have implemented IPluginI18n and have PluginInitContext property
+ // Check if the property is valid
+ foreach (var pluginClass in iPluginI18nClassesWithContext)
+ {
+ if (pluginClass.IsValid == true)
+ {
+ return pluginClass;
+ }
+
+ if (!pluginClass.IsStatic)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.ContextPropertyNotStatic,
+ pluginClass.Location,
+ pluginClass.PropertyName
+ ));
+ }
+
+ if (pluginClass.IsPrivate)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.ContextPropertyIsPrivate,
+ pluginClass.Location,
+ pluginClass.PropertyName
+ ));
+ }
+
+ if (pluginClass.IsProtected)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.ContextPropertyIsProtected,
+ pluginClass.Location,
+ pluginClass.PropertyName
+ ));
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Flow.Launcher.Localization.SourceGenerators/SourceGeneratorDiagnostics.cs b/Flow.Launcher.Localization.SourceGenerators/SourceGeneratorDiagnostics.cs
index d162cdb..f8cde85 100644
--- a/Flow.Launcher.Localization.SourceGenerators/SourceGeneratorDiagnostics.cs
+++ b/Flow.Launcher.Localization.SourceGenerators/SourceGeneratorDiagnostics.cs
@@ -67,5 +67,14 @@ public static class SourceGeneratorDiagnostics
DiagnosticSeverity.Warning,
isEnabledByDefault: true
);
+
+ public static readonly DiagnosticDescriptor EnumFieldLocalizationKeyValueInvalid = new DiagnosticDescriptor(
+ "FLSG0008",
+ "Enum field localization key and value invalid",
+ $"Enum field `{{0}}` does not have a valid localization key or value",
+ "Localization",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true
+ );
}
}
diff --git a/Flow.Launcher.Localization.slnx b/Flow.Launcher.Localization.slnx
index 27c8f44..779a1ca 100644
--- a/Flow.Launcher.Localization.slnx
+++ b/Flow.Launcher.Localization.slnx
@@ -1,5 +1,6 @@
+
diff --git a/Flow.Launcher.Localization/Flow.Launcher.Localization.csproj b/Flow.Launcher.Localization/Flow.Launcher.Localization.csproj
index b9ee6bc..5cf25ff 100644
--- a/Flow.Launcher.Localization/Flow.Launcher.Localization.csproj
+++ b/Flow.Launcher.Localization/Flow.Launcher.Localization.csproj
@@ -6,7 +6,7 @@
Flow.Launcher.Localization
Flow.Launcher.Localization
false
- true
+ false
true
true
@@ -33,6 +33,9 @@
All
+
+ runtime
+
All
@@ -47,6 +50,11 @@
analyzers/dotnet/cs
false
+
+ true
+ lib/$(TargetFramework)
+ true
+
true
analyzers/dotnet/cs