diff --git a/Flow.Launcher.Analyzers/AnalyzerDiagnostics.cs b/Flow.Launcher.Analyzers/AnalyzerDiagnostics.cs
new file mode 100644
index 00000000000..f381aa31ec0
--- /dev/null
+++ b/Flow.Launcher.Analyzers/AnalyzerDiagnostics.cs
@@ -0,0 +1,52 @@
+using Microsoft.CodeAnalysis;
+
+namespace Flow.Launcher.Analyzers
+{
+ public static class AnalyzerDiagnostics
+ {
+ public static readonly DiagnosticDescriptor OldLocalizationApiUsed = new DiagnosticDescriptor(
+ "FLAN0001",
+ "Old localization API used",
+ "Use `Localize.{0}({1})` instead",
+ "Localization",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true
+ );
+
+ public static readonly DiagnosticDescriptor ContextIsAField = new DiagnosticDescriptor(
+ "FLAN0002",
+ "Plugin context is a field",
+ "Plugin context must be at least internal static property",
+ "Localization",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+
+ public static readonly DiagnosticDescriptor ContextIsNotStatic = new DiagnosticDescriptor(
+ "FLAN0003",
+ "Plugin context is not static",
+ "Plugin context must be at least internal static property",
+ "Localization",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+
+ public static readonly DiagnosticDescriptor ContextAccessIsTooRestrictive = new DiagnosticDescriptor(
+ "FLAN0004",
+ "Plugin context property access modifier is too restrictive",
+ "Plugin context property must be at least internal static property",
+ "Localization",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+
+ public static readonly DiagnosticDescriptor ContextIsNotDeclared = new DiagnosticDescriptor(
+ "FLAN0005",
+ "Plugin context is not declared",
+ "Plugin context must be at least internal static property of type `PluginInitContext`",
+ "Localization",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+ }
+}
diff --git a/Flow.Launcher.Analyzers/AnalyzerReleases.Shipped.md b/Flow.Launcher.Analyzers/AnalyzerReleases.Shipped.md
new file mode 100644
index 00000000000..5ccc9f037f6
--- /dev/null
+++ b/Flow.Launcher.Analyzers/AnalyzerReleases.Shipped.md
@@ -0,0 +1,2 @@
+; Shipped analyzer releases
+; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
diff --git a/Flow.Launcher.Analyzers/AnalyzerReleases.Unshipped.md b/Flow.Launcher.Analyzers/AnalyzerReleases.Unshipped.md
new file mode 100644
index 00000000000..d5f177c6c1a
--- /dev/null
+++ b/Flow.Launcher.Analyzers/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,11 @@
+; 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
+--------|----------|----------|-------
+FLAN0001 | Localization | Warning | FLAN0001_OldLocalizationApiUsed
+FLAN0002 | Localization | Error | FLAN0002_ContextIsAField
+FLAN0003 | Localization | Error | FLAN0003_ContextIsNotStatic
+FLAN0004 | Localization | Error | FLAN0004_ContextAccessIsTooRestrictive
+FLAN0005 | Localization | Error | FLAN0005_ContextIsNotDeclared
diff --git a/Flow.Launcher.Analyzers/Flow.Launcher.Analyzers.csproj b/Flow.Launcher.Analyzers/Flow.Launcher.Analyzers.csproj
new file mode 100644
index 00000000000..231d8d1afdd
--- /dev/null
+++ b/Flow.Launcher.Analyzers/Flow.Launcher.Analyzers.csproj
@@ -0,0 +1,18 @@
+
+
+
+ 1.0.0
+ netstandard2.0
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
diff --git a/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzer.cs b/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzer.cs
new file mode 100644
index 00000000000..eede68bfe07
--- /dev/null
+++ b/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzer.cs
@@ -0,0 +1,94 @@
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Flow.Launcher.Analyzers.Localize
+{
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public class ContextAvailabilityAnalyzer : DiagnosticAnalyzer
+ {
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(
+ AnalyzerDiagnostics.ContextIsAField,
+ AnalyzerDiagnostics.ContextIsNotStatic,
+ AnalyzerDiagnostics.ContextAccessIsTooRestrictive,
+ AnalyzerDiagnostics.ContextIsNotDeclared
+ );
+
+ private const string PluginContextTypeName = "PluginInitContext";
+
+ private const string PluginInterfaceName = "IPluginI18n";
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ClassDeclaration);
+ }
+
+ private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
+ {
+ var classDeclaration = (ClassDeclarationSyntax)context.Node;
+ var semanticModel = context.SemanticModel;
+ var classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration);
+
+ if (!IsPluginEntryClass(classSymbol)) return;
+
+ var contextProperty = classDeclaration.Members.OfType()
+ .Select(p => semanticModel.GetDeclaredSymbol(p))
+ .FirstOrDefault(p => p?.Type.Name is PluginContextTypeName);
+
+ if (contextProperty != null)
+ {
+ if (!contextProperty.IsStatic)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ AnalyzerDiagnostics.ContextIsNotStatic,
+ contextProperty.DeclaringSyntaxReferences[0].GetSyntax().GetLocation()
+ ));
+ return;
+ }
+
+ if (contextProperty.DeclaredAccessibility is Accessibility.Private || contextProperty.DeclaredAccessibility is Accessibility.Protected)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ AnalyzerDiagnostics.ContextAccessIsTooRestrictive,
+ contextProperty.DeclaringSyntaxReferences[0].GetSyntax().GetLocation()
+ ));
+ return;
+ }
+
+ return;
+ }
+
+ var fieldDeclaration = classDeclaration.Members
+ .OfType()
+ .SelectMany(f => f.Declaration.Variables)
+ .Select(f => semanticModel.GetDeclaredSymbol(f))
+ .FirstOrDefault(f => f is IFieldSymbol fs && fs.Type.Name is PluginContextTypeName);
+ var parentSyntax = fieldDeclaration
+ ?.DeclaringSyntaxReferences[0]
+ .GetSyntax()
+ .FirstAncestorOrSelf();
+
+ if (parentSyntax != null)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ AnalyzerDiagnostics.ContextIsAField,
+ parentSyntax.GetLocation()
+ ));
+ return;
+ }
+
+ context.ReportDiagnostic(Diagnostic.Create(
+ AnalyzerDiagnostics.ContextIsNotDeclared,
+ classDeclaration.Identifier.GetLocation()
+ ));
+ }
+
+ private static bool IsPluginEntryClass(INamedTypeSymbol namedTypeSymbol) =>
+ namedTypeSymbol?.Interfaces.Any(i => i.Name == PluginInterfaceName) ?? false;
+ }
+}
diff --git a/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs b/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs
new file mode 100644
index 00000000000..9b798018a21
--- /dev/null
+++ b/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs
@@ -0,0 +1,188 @@
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Formatting;
+using Microsoft.CodeAnalysis.Simplification;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Flow.Launcher.Analyzers.Localize
+{
+ [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ContextAvailabilityAnalyzerCodeFixProvider)), Shared]
+ public class ContextAvailabilityAnalyzerCodeFixProvider : CodeFixProvider
+ {
+ public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(
+ AnalyzerDiagnostics.ContextIsAField.Id,
+ AnalyzerDiagnostics.ContextIsNotStatic.Id,
+ AnalyzerDiagnostics.ContextAccessIsTooRestrictive.Id,
+ AnalyzerDiagnostics.ContextIsNotDeclared.Id
+ );
+
+ public sealed override FixAllProvider GetFixAllProvider()
+ {
+ return WellKnownFixAllProviders.BatchFixer;
+ }
+
+ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+
+ var diagnostic = context.Diagnostics.First();
+ var diagnosticSpan = diagnostic.Location.SourceSpan;
+
+ if (diagnostic.Id == AnalyzerDiagnostics.ContextIsAField.Id)
+ {
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: "Replace with static property",
+ createChangedDocument: _ => Task.FromResult(FixContextIsAFieldError(context, root, diagnosticSpan)),
+ equivalenceKey: AnalyzerDiagnostics.ContextIsAField.Id
+ ),
+ diagnostic
+ );
+ }
+ else if (diagnostic.Id == AnalyzerDiagnostics.ContextIsNotStatic.Id)
+ {
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: "Make static",
+ createChangedDocument: _ => Task.FromResult(FixContextIsNotStaticError(context, root, diagnosticSpan)),
+ equivalenceKey: AnalyzerDiagnostics.ContextIsNotStatic.Id
+ ),
+ diagnostic
+ );
+ }
+ else if (diagnostic.Id == AnalyzerDiagnostics.ContextAccessIsTooRestrictive.Id)
+ {
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: "Make internal",
+ createChangedDocument: _ => Task.FromResult(FixContextIsTooRestricted(context, root, diagnosticSpan)),
+ equivalenceKey: AnalyzerDiagnostics.ContextAccessIsTooRestrictive.Id
+ ),
+ diagnostic
+ );
+ }
+ else if (diagnostic.Id == AnalyzerDiagnostics.ContextIsNotDeclared.Id)
+ {
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: "Declare context property",
+ createChangedDocument: _ => Task.FromResult(FixContextNotDeclared(context, root, diagnosticSpan)),
+ equivalenceKey: AnalyzerDiagnostics.ContextIsNotDeclared.Id
+ ),
+ diagnostic
+ );
+ }
+ }
+
+ private static MemberDeclarationSyntax GetStaticContextPropertyDeclaration(string propertyName = "Context") =>
+ SyntaxFactory.ParseMemberDeclaration(
+ $"internal static PluginInitContext {propertyName} {{ get; private set; }} = null!;"
+ );
+
+ private static Document GetFormattedDocument(CodeFixContext context, SyntaxNode root)
+ {
+ var formattedRoot = Formatter.Format(
+ root,
+ Formatter.Annotation,
+ context.Document.Project.Solution.Workspace
+ );
+
+ return context.Document.WithSyntaxRoot(formattedRoot);
+ }
+
+ private static Document FixContextNotDeclared(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan)
+ {
+ var classDeclaration = GetDeclarationSyntax(root, diagnosticSpan);
+ if (classDeclaration?.BaseList is null) return context.Document;
+
+ var newPropertyDeclaration = GetStaticContextPropertyDeclaration();
+ if (newPropertyDeclaration is null) return context.Document;
+
+ var annotatedNewPropertyDeclaration = newPropertyDeclaration
+ .WithLeadingTrivia(SyntaxFactory.ElasticLineFeed)
+ .WithTrailingTrivia(SyntaxFactory.ElasticLineFeed)
+ .WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation);
+
+ var newMembers = classDeclaration.Members.Insert(0, annotatedNewPropertyDeclaration);
+ var newClassDeclaration = classDeclaration.WithMembers(newMembers);
+
+ var newRoot = root.ReplaceNode(classDeclaration, newClassDeclaration);
+
+ return GetFormattedDocument(context, newRoot);
+ }
+
+ private static Document FixContextIsNotStaticError(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan)
+ {
+ var propertyDeclaration = GetDeclarationSyntax(root, diagnosticSpan);
+ if (propertyDeclaration is null) return context.Document;
+
+ var newPropertyDeclaration = FixRestrictivePropertyModifiers(propertyDeclaration).AddModifiers(SyntaxFactory.Token(SyntaxKind.StaticKeyword));
+
+ var newRoot = root.ReplaceNode(propertyDeclaration, newPropertyDeclaration);
+ return context.Document.WithSyntaxRoot(newRoot);
+ }
+
+ private static Document FixContextIsTooRestricted(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan)
+ {
+ var propertyDeclaration = GetDeclarationSyntax(root, diagnosticSpan);
+ if (propertyDeclaration is null) return context.Document;
+
+ var newPropertyDeclaration = FixRestrictivePropertyModifiers(propertyDeclaration);
+
+ var newRoot = root.ReplaceNode(propertyDeclaration, newPropertyDeclaration);
+ return context.Document.WithSyntaxRoot(newRoot);
+ }
+
+ private static Document FixContextIsAFieldError(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan) {
+ var fieldDeclaration = GetDeclarationSyntax(root, diagnosticSpan);
+ if (fieldDeclaration is null) return context.Document;
+
+ var field = fieldDeclaration.Declaration.Variables.First();
+ var fieldIdentifier = field.Identifier.ToString();
+
+ var propertyDeclaration = GetStaticContextPropertyDeclaration(fieldIdentifier);
+ if (propertyDeclaration is null) return context.Document;
+
+ var annotatedNewPropertyDeclaration = propertyDeclaration
+ .WithTriviaFrom(fieldDeclaration)
+ .WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation);
+
+ var newRoot = root.ReplaceNode(fieldDeclaration, annotatedNewPropertyDeclaration);
+
+ return GetFormattedDocument(context, newRoot);
+ }
+
+ private static PropertyDeclarationSyntax FixRestrictivePropertyModifiers(PropertyDeclarationSyntax propertyDeclaration)
+ {
+ var newModifiers = SyntaxFactory.TokenList();
+ foreach (var modifier in propertyDeclaration.Modifiers)
+ {
+ if (modifier.IsKind(SyntaxKind.PrivateKeyword) || modifier.IsKind(SyntaxKind.ProtectedKeyword))
+ {
+ newModifiers = newModifiers.Add(SyntaxFactory.Token(SyntaxKind.InternalKeyword));
+ }
+ else
+ {
+ newModifiers = newModifiers.Add(modifier);
+ }
+ }
+
+ return propertyDeclaration.WithModifiers(newModifiers);
+ }
+
+ private static T GetDeclarationSyntax(SyntaxNode root, TextSpan diagnosticSpan) where T : SyntaxNode =>
+ root
+ .FindToken(diagnosticSpan.Start)
+ .Parent
+ ?.AncestorsAndSelf()
+ .OfType()
+ .First();
+ }
+}
diff --git a/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzer.cs b/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzer.cs
new file mode 100644
index 00000000000..25f0f6ef36b
--- /dev/null
+++ b/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzer.cs
@@ -0,0 +1,100 @@
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Flow.Launcher.Analyzers.Localize
+{
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public class OldGetTranslateAnalyzer : DiagnosticAnalyzer
+ {
+ public override ImmutableArray SupportedDiagnostics =>
+ ImmutableArray.Create(AnalyzerDiagnostics.OldLocalizationApiUsed);
+
+ private static readonly string[] oldLocalizationClasses = { "IPublicAPI", "Internationalization" };
+ private const string OldLocalizationMethodName = "GetTranslation";
+
+ private const string StringFormatMethodName = "Format";
+ private const string StringFormatTypeName = "string";
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.InvocationExpression);
+ }
+
+ private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
+ {
+ var invocationExpr = (InvocationExpressionSyntax)context.Node;
+ var semanticModel = context.SemanticModel;
+ var symbolInfo = semanticModel.GetSymbolInfo(invocationExpr);
+
+ if (!(symbolInfo.Symbol is IMethodSymbol methodSymbol)) return;
+
+ if (IsFormatStringCall(methodSymbol) &&
+ GetFirstArgumentInvocationExpression(invocationExpr) is InvocationExpressionSyntax innerInvocationExpr)
+ {
+ if (!IsTranslateCall(semanticModel.GetSymbolInfo(innerInvocationExpr)) ||
+ !(GetFirstArgumentStringValue(innerInvocationExpr) is string translationKey))
+ return;
+
+ var diagnostic = Diagnostic.Create(
+ AnalyzerDiagnostics.OldLocalizationApiUsed,
+ invocationExpr.GetLocation(),
+ translationKey,
+ GetInvocationArguments(invocationExpr)
+ );
+ context.ReportDiagnostic(diagnostic);
+ }
+ else if (IsTranslateCall(methodSymbol) && GetFirstArgumentStringValue(invocationExpr) is string translationKey)
+ {
+ if (IsParentFormatStringCall(semanticModel, invocationExpr)) return;
+
+ var diagnostic = Diagnostic.Create(
+ AnalyzerDiagnostics.OldLocalizationApiUsed,
+ invocationExpr.GetLocation(),
+ translationKey,
+ string.Empty
+ );
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+
+ private static string GetInvocationArguments(InvocationExpressionSyntax invocationExpr) =>
+ string.Join(", ", invocationExpr.ArgumentList.Arguments.Skip(1));
+
+ private static bool IsParentFormatStringCall(SemanticModel semanticModel, SyntaxNode syntaxNode) =>
+ syntaxNode is InvocationExpressionSyntax invocationExpressionSyntax &&
+ invocationExpressionSyntax.Parent?.Parent?.Parent is SyntaxNode parent &&
+ IsFormatStringCall(semanticModel?.GetSymbolInfo(parent));
+
+ private static bool IsFormatStringCall(SymbolInfo? symbolInfo) =>
+ symbolInfo is SymbolInfo info && IsFormatStringCall(info.Symbol as IMethodSymbol);
+
+ private static bool IsFormatStringCall(IMethodSymbol methodSymbol) =>
+ methodSymbol?.Name is StringFormatMethodName &&
+ methodSymbol.ContainingType.ToDisplayString() is StringFormatTypeName;
+
+ private static InvocationExpressionSyntax GetFirstArgumentInvocationExpression(InvocationExpressionSyntax invocationExpr) =>
+ invocationExpr.ArgumentList.Arguments.FirstOrDefault()?.Expression as InvocationExpressionSyntax;
+
+ private static bool IsTranslateCall(SymbolInfo symbolInfo) =>
+ symbolInfo.Symbol is IMethodSymbol innerMethodSymbol &&
+ innerMethodSymbol.Name is OldLocalizationMethodName &&
+ oldLocalizationClasses.Contains(innerMethodSymbol.ContainingType.Name);
+
+ private static bool IsTranslateCall(IMethodSymbol methodSymbol) =>
+ methodSymbol?.Name is OldLocalizationMethodName &&
+ oldLocalizationClasses.Contains(methodSymbol.ContainingType.Name);
+
+ private static string GetFirstArgumentStringValue(InvocationExpressionSyntax invocationExpr)
+ {
+ if (invocationExpr.ArgumentList.Arguments.FirstOrDefault()?.Expression is LiteralExpressionSyntax syntax)
+ return syntax.Token.ValueText;
+ return null;
+ }
+ }
+}
diff --git a/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs b/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs
new file mode 100644
index 00000000000..87fb37a8355
--- /dev/null
+++ b/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs
@@ -0,0 +1,114 @@
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Flow.Launcher.Analyzers.Localize
+{
+ [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(OldGetTranslateAnalyzerCodeFixProvider)), Shared]
+ public class OldGetTranslateAnalyzerCodeFixProvider : CodeFixProvider
+ {
+ public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AnalyzerDiagnostics.OldLocalizationApiUsed.Id);
+
+ public sealed override FixAllProvider GetFixAllProvider()
+ {
+ return WellKnownFixAllProviders.BatchFixer;
+ }
+
+ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+
+ var diagnostic = context.Diagnostics.First();
+
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: "Replace with 'Localize.localization_key(...args)'",
+ createChangedDocument: _ => Task.FromResult(FixOldTranslation(context, root, diagnostic)),
+ equivalenceKey: AnalyzerDiagnostics.OldLocalizationApiUsed.Id
+ ),
+ diagnostic
+ );
+ }
+
+ private static Document FixOldTranslation(CodeFixContext context, SyntaxNode root, Diagnostic diagnostic)
+ {
+ var diagnosticSpan = diagnostic.Location.SourceSpan;
+
+ var invocationExpr = root
+ ?.FindToken(diagnosticSpan.Start).Parent
+ ?.AncestorsAndSelf()
+ .OfType()
+ .First();
+
+ if (invocationExpr is null || root is null) return context.Document;
+
+ var argumentList = invocationExpr.ArgumentList.Arguments;
+ var argument = argumentList.First().Expression;
+
+ if (GetTranslationKey(argument) is string translationKey)
+ return FixOldTranslationWithoutStringFormat(context, translationKey, root, invocationExpr);
+
+ if (GetTranslationKeyFromInnerInvocation(argument) is string translationKeyInside)
+ return FixOldTranslationWithStringFormat(context, argumentList, translationKeyInside, root, invocationExpr);
+
+ return context.Document;
+ }
+
+
+ private static string GetTranslationKey(ExpressionSyntax syntax)
+ {
+ if (
+ syntax is LiteralExpressionSyntax literalExpressionSyntax &&
+ literalExpressionSyntax.Token.Value is string translationKey
+ )
+ return translationKey;
+ return null;
+ }
+
+ private static Document FixOldTranslationWithoutStringFormat(
+ CodeFixContext context, string translationKey, SyntaxNode root, InvocationExpressionSyntax invocationExpr
+ ) {
+ var newInvocationExpr = SyntaxFactory.ParseExpression(
+ $"Localize.{translationKey}()"
+ );
+
+ var newRoot = root.ReplaceNode(invocationExpr, newInvocationExpr);
+ var newDocument = context.Document.WithSyntaxRoot(newRoot);
+ return newDocument;
+ }
+
+ private static string GetTranslationKeyFromInnerInvocation(ExpressionSyntax syntax)
+ {
+ if (
+ syntax is InvocationExpressionSyntax invocationExpressionSyntax &&
+ invocationExpressionSyntax.ArgumentList.Arguments.Count is 1
+ )
+ {
+ var firstArgument = invocationExpressionSyntax.ArgumentList.Arguments.First().Expression;
+ return GetTranslationKey(firstArgument);
+ }
+ return null;
+ }
+
+ private static Document FixOldTranslationWithStringFormat(
+ CodeFixContext context,
+ SeparatedSyntaxList argumentList,
+ string translationKey2,
+ SyntaxNode root,
+ InvocationExpressionSyntax invocationExpr
+ ) {
+ var newArguments = string.Join(", ", argumentList.Skip(1).Select(a => a.Expression));
+ var newInnerInvocationExpr = SyntaxFactory.ParseExpression($"Localize.{translationKey2}({newArguments})");
+
+ var newRoot = root.ReplaceNode(invocationExpr, newInnerInvocationExpr);
+ return context.Document.WithSyntaxRoot(newRoot);
+ }
+
+ }
+}
diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj
index 18101ccf04e..c7c9193df82 100644
--- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj
+++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj
@@ -13,7 +13,7 @@
false
en
-
+
true
portable
@@ -24,7 +24,7 @@
4
false
-
+
pdbonly
true
@@ -39,11 +39,11 @@
-
+
-
+
@@ -51,7 +51,7 @@
-
+
@@ -60,10 +60,22 @@
-
+
-
\ No newline at end of file
+
+
+
+
+
+
+
diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs
index 96338cf6a1a..feeb978b328 100644
--- a/Flow.Launcher.Core/Resource/Theme.cs
+++ b/Flow.Launcher.Core/Resource/Theme.cs
@@ -79,14 +79,14 @@ public bool ChangeTheme(string theme)
{
if (string.IsNullOrEmpty(path))
throw new DirectoryNotFoundException("Theme path can't be found <{path}>");
-
+
// reload all resources even if the theme itself hasn't changed in order to pickup changes
// to things like fonts
UpdateResourceDictionary(GetResourceDictionary(theme));
-
+
Settings.Theme = theme;
-
+
//always allow re-loading default theme, in case of failure of switching to a new theme from default theme
if (_oldTheme != theme || theme == defaultTheme)
{
@@ -105,7 +105,7 @@ public bool ChangeTheme(string theme)
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found");
if (theme != defaultTheme)
{
- MessageBox.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_path_not_exists"), theme));
+ MessageBox.Show(Localize.theme_load_failure_path_not_exists(theme));
ChangeTheme(defaultTheme);
}
return false;
@@ -115,7 +115,7 @@ public bool ChangeTheme(string theme)
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse");
if (theme != defaultTheme)
{
- MessageBox.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_parse_error"), theme));
+ MessageBox.Show(Localize.theme_load_failure_parse_error(theme));
ChangeTheme(defaultTheme);
}
return false;
@@ -148,7 +148,7 @@ private ResourceDictionary GetThemeResourceDictionary(string theme)
public ResourceDictionary GetResourceDictionary(string theme)
{
var dict = GetThemeResourceDictionary(theme);
-
+
if (dict["QueryBoxStyle"] is Style queryBoxStyle &&
dict["QuerySuggestionBoxStyle"] is Style querySuggestionBoxStyle)
{
@@ -189,7 +189,7 @@ public ResourceDictionary GetResourceDictionary(string theme)
Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch };
Array.ForEach(
- new[] { resultItemStyle, resultSubItemStyle, resultItemSelectedStyle, resultSubItemSelectedStyle, resultHotkeyItemStyle, resultHotkeyItemSelectedStyle }, o
+ new[] { resultItemStyle, resultSubItemStyle, resultItemSelectedStyle, resultSubItemSelectedStyle, resultHotkeyItemStyle, resultHotkeyItemSelectedStyle }, o
=> Array.ForEach(setters, p => o.Setters.Add(p)));
}
/* Ignore Theme Window Width and use setting */
diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs
index 3f64b273e4c..c197347d23a 100644
--- a/Flow.Launcher.Core/Updater.cs
+++ b/Flow.Launcher.Core/Updater.cs
@@ -37,8 +37,7 @@ public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true)
try
{
if (!silentUpdate)
- api.ShowMsg(api.GetTranslation("pleaseWait"),
- api.GetTranslation("update_flowlauncher_update_check"));
+ api.ShowMsg(Localize.pleaseWait(), Localize.update_flowlauncher_update_check());
using var updateManager = await GitHubUpdateManagerAsync(GitHubRepository).ConfigureAwait(false);
@@ -53,13 +52,12 @@ public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true)
if (newReleaseVersion <= currentVersion)
{
if (!silentUpdate)
- MessageBox.Show(api.GetTranslation("update_flowlauncher_already_on_latest"));
+ MessageBox.Show(Localize.update_flowlauncher_already_on_latest());
return;
}
if (!silentUpdate)
- api.ShowMsg(api.GetTranslation("update_flowlauncher_update_found"),
- api.GetTranslation("update_flowlauncher_updating"));
+ api.ShowMsg(Localize.update_flowlauncher_update_found(), Localize.update_flowlauncher_updating());
await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false);
@@ -70,20 +68,22 @@ public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true)
var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{DataLocation.PortableFolderName}";
FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination);
if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination))
- MessageBox.Show(string.Format(api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"),
- DataLocation.PortableDataPath,
- targetDestination));
+ MessageBox.Show(
+ Localize.update_flowlauncher_fail_moving_portable_user_profile_data(
+ DataLocation.PortableDataPath, targetDestination
+ )
+ );
}
else
{
await updateManager.CreateUninstallerRegistryEntry().ConfigureAwait(false);
}
- var newVersionTips = NewVersionTips(newReleaseVersion.ToString());
+ var newVersionTips = Localize.newVersionTips(newReleaseVersion.ToString());
Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}");
- if (MessageBox.Show(newVersionTips, api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes)
+ if (MessageBox.Show(newVersionTips, Localize.update_flowlauncher_new_update(), MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
UpdateManager.RestartApp(Constant.ApplicationFileName);
}
@@ -94,10 +94,9 @@ public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true)
Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e);
else
Log.Exception($"|Updater.UpdateApp|Error Occurred", e);
-
+
if (!silentUpdate)
- api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"),
- api.GetTranslation("update_flowlauncher_check_connection"));
+ api.ShowMsg(Localize.update_flowlauncher_fail(), Localize.update_flowlauncher_check_connection());
}
finally
{
@@ -140,13 +139,5 @@ private async Task GitHubUpdateManagerAsync(string repository)
return manager;
}
-
- public string NewVersionTips(string version)
- {
- var translator = InternationalizationManager.Instance;
- var tips = string.Format(translator.GetTranslation("newVersionTips"), version);
-
- return tips;
- }
}
}
diff --git a/Flow.Launcher.SourceGenerators/AnalyzerReleases.Shipped.md b/Flow.Launcher.SourceGenerators/AnalyzerReleases.Shipped.md
new file mode 100644
index 00000000000..5ccc9f037f6
--- /dev/null
+++ b/Flow.Launcher.SourceGenerators/AnalyzerReleases.Shipped.md
@@ -0,0 +1,2 @@
+; Shipped analyzer releases
+; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
diff --git a/Flow.Launcher.SourceGenerators/AnalyzerReleases.Unshipped.md b/Flow.Launcher.SourceGenerators/AnalyzerReleases.Unshipped.md
new file mode 100644
index 00000000000..f60d25850da
--- /dev/null
+++ b/Flow.Launcher.SourceGenerators/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,13 @@
+; Shipped analyzer releases
+; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|-------
+FLSG0001 | Localization | Warning | FLSG0001_CouldNotFindResourceDictionaries
+FLSG0002 | Localization | Warning | FLSG0002_CouldNotFindPluginEntryClass
+FLSG0003 | Localization | Warning | FLSG0003_CouldNotFindContextProperty
+FLSG0004 | Localization | Warning | FLSG0004_ContextPropertyNotStatic
+FLSG0005 | Localization | Warning | FLSG0005_ContextPropertyIsPrivate
+FLSG0006 | Localization | Warning | FLSG0006_ContextPropertyIsProtected
+FLSG0007 | Localization | Warning | FLSG0007_LocalizationKeyUnused
diff --git a/Flow.Launcher.SourceGenerators/Flow.Launcher.SourceGenerators.csproj b/Flow.Launcher.SourceGenerators/Flow.Launcher.SourceGenerators.csproj
new file mode 100644
index 00000000000..9679baa17e0
--- /dev/null
+++ b/Flow.Launcher.SourceGenerators/Flow.Launcher.SourceGenerators.csproj
@@ -0,0 +1,17 @@
+
+
+
+ 1.0.0
+ netstandard2.0
+ true
+ Flow.Launcher.SourceGenerators
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
diff --git a/Flow.Launcher.SourceGenerators/Localize/LocalizeSourceGenerator.cs b/Flow.Launcher.SourceGenerators/Localize/LocalizeSourceGenerator.cs
new file mode 100644
index 00000000000..ca8d6d5a01d
--- /dev/null
+++ b/Flow.Launcher.SourceGenerators/Localize/LocalizeSourceGenerator.cs
@@ -0,0 +1,491 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Xml.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Flow.Launcher.SourceGenerators.Localize
+{
+ [Generator]
+ public partial class LocalizeSourceGenerator : ISourceGenerator
+ {
+ private OptimizationLevel _optimizationLevel;
+
+ private const string CoreNamespace1 = "Flow.Launcher";
+ private const string CoreNamespace2 = "Flow.Launcher.Core";
+ private const string DefaultNamespace = "Flow.Launcher";
+ 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)
+ {
+ }
+
+ public void Execute(GeneratorExecutionContext context)
+ {
+ _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] is ClassName)) return;
+ allLanguageKeys.Add(split[1]);
+ });
+
+ var allXamlFiles = context.AdditionalFiles
+ .Where(v => _languagesXamlRegex.IsMatch(v.Path))
+ .ToArray();
+ AdditionalText[] resourceDictionaries;
+ if (allXamlFiles.Length is 0)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.CouldNotFindResourceDictionaries,
+ Location.None
+ ));
+ return;
+ }
+
+ if (string.IsNullOrEmpty(langFilePathEndsWithStr))
+ {
+ 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;
+ }
+ }
+ }
+ else
+ {
+ 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;
+ }
+ }
+
+ var ns = context.Compilation.AssemblyName ?? DefaultNamespace;
+
+ var localizedStrings = LoadLocalizedStrings(resourceDictionaries);
+
+ var unusedLocalizationKeys = localizedStrings.Keys.Except(allLanguageKeys).ToArray();
+
+ foreach (var key in unusedLocalizationKeys)
+ context.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.LocalizationKeyUnused,
+ Location.None,
+ key
+ ));
+
+ var sourceCode = GenerateSourceCode(localizedStrings, context, unusedLocalizationKeys);
+
+ context.AddSource($"{ClassName}.{ns}.g.cs", SourceText.From(sourceCode, Encoding.UTF8));
+ }
+
+ private static Dictionary LoadLocalizedStrings(AdditionalText[] files)
+ {
+ var result = new Dictionary();
+
+ foreach (var file in files)
+ {
+ ProcessXamlFile(file, result);
+ }
+
+ return result;
+ }
+
+ 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;
+
+ if (name is null) continue;
+
+ string summary = null;
+ var paramsList = new List();
+ var commentNode = element.PreviousNode;
+
+ if (commentNode is XComment comment)
+ summary = ProcessXamlFileComment(comment, paramsList);
+
+ result[name] = new LocalizableString(name, value, summary, paramsList);
+ }
+ }
+
+ private static string ProcessXamlFileComment(XComment comment, List paramsList) {
+ string summary = null;
+ try
+ {
+ if (CommentIncludesDocumentationMarkup(comment))
+ {
+ var commentDoc = XDocument.Parse($"{comment.Value}");
+ summary = ExtractDocumentationCommentSummary(commentDoc);
+ foreach (var param in commentDoc.Descendants("param"))
+ {
+ var index = int.Parse(param.Attribute("index")?.Value ?? "-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));
+ }
+ }
+ }
+ catch
+ {
+ // ignore
+ }
+
+ return summary;
+ }
+
+ private static string ExtractDocumentationCommentSummary(XDocument commentDoc) {
+ return commentDoc.Descendants("summary").FirstOrDefault()?.Value.Trim();
+ }
+
+ private static bool CommentIncludesDocumentationMarkup(XComment comment) {
+ return comment.Value.Contains("") || comment.Value.Contains(" localizedStrings,
+ GeneratorExecutionContext context,
+ string[] unusedLocalizationKeys
+ )
+ {
+ var ns = context.Compilation.AssemblyName;
+
+ var sb = new StringBuilder();
+ if (ns is CoreNamespace1 || ns is CoreNamespace2)
+ {
+ GenerateFileHeader(sb, context);
+ GenerateClass(sb, localizedStrings, unusedLocalizationKeys);
+ return sb.ToString();
+ }
+
+ string contextPropertyName = null;
+ var mainClassFound = false;
+ foreach (var (syntaxTree, classDeclaration) in GetClasses(context))
+ {
+ if (!DoesClassImplementInterface(classDeclaration, PluginInterfaceName))
+ continue;
+
+ mainClassFound = true;
+
+ var property = GetPluginContextProperty(classDeclaration);
+ if (property is null)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.CouldNotFindContextProperty,
+ GetLocation(syntaxTree, classDeclaration),
+ classDeclaration.Identifier
+ ));
+ return string.Empty;
+ }
+
+ var propertyModifiers = GetPropertyModifiers(property);
+
+ if (!propertyModifiers.Static)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.ContextPropertyNotStatic,
+ GetLocation(syntaxTree, property),
+ property.Identifier
+ ));
+ return string.Empty;
+ }
+
+ if (propertyModifiers.Private)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.ContextPropertyIsPrivate,
+ GetLocation(syntaxTree, property),
+ property.Identifier
+ ));
+ return string.Empty;
+ }
+
+ if (propertyModifiers.Protected)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.ContextPropertyIsProtected,
+ GetLocation(syntaxTree, property),
+ property.Identifier
+ ));
+ return string.Empty;
+ }
+
+ contextPropertyName = $"{classDeclaration.Identifier}.{property.Identifier}";
+ break;
+ }
+
+ if (mainClassFound is false)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.CouldNotFindPluginEntryClass,
+ Location.None
+ ));
+ return string.Empty;
+ }
+
+ GenerateFileHeader(sb, context, true);
+ GenerateClass(sb, localizedStrings, unusedLocalizationKeys, contextPropertyName);
+ return sb.ToString();
+ }
+
+ private static void GenerateFileHeader(StringBuilder sb, GeneratorExecutionContext context, bool isPlugin = false)
+ {
+ var rootNamespace = context.Compilation.AssemblyName;
+ sb.AppendLine("// ");
+ sb.AppendLine("#nullable enable");
+
+ if (!isPlugin)
+ sb.AppendLine("using Flow.Launcher.Core.Resource;");
+
+ sb.AppendLine($"namespace {rootNamespace};");
+ }
+
+ private void GenerateClass(
+ StringBuilder sb,
+ Dictionary localizedStrings,
+ string[] unusedLocalizationKeys,
+ string propertyName = null
+ )
+ {
+ sb.AppendLine();
+ sb.AppendLine($"public static class {ClassName}");
+ sb.AppendLine("{");
+ foreach (var localizedString in localizedStrings)
+ {
+ if (_optimizationLevel == OptimizationLevel.Release && unusedLocalizationKeys.Contains(localizedString.Key))
+ continue;
+
+ GenerateDocCommentForMethod(sb, localizedString.Value);
+ GenerateMethod(sb, localizedString.Value, propertyName);
+ }
+
+ sb.AppendLine("}");
+ }
+
+ private static void GenerateDocCommentForMethod(StringBuilder sb, LocalizableString localizableString)
+ {
+ sb.AppendLine("/// ");
+ if (!(localizableString.Summary is null))
+ {
+ sb.AppendLine(string.Join("\n", localizableString.Summary.Trim().Split('\n').Select(v => $"/// {v}")));
+ }
+
+ sb.AppendLine("/// ");
+ var value = localizableString.Value;
+ foreach (var p in localizableString.Params)
+ {
+ value = value.Replace($"{{{p.Index}}}", $"{{{p.Name}}}");
+ }
+ 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)
+ {
+ sb.Append($"public static string {localizableString.Key}(");
+ var declarationArgs = new List();
+ var callArgs = new List();
+ for (var i = 0; i < 10; i++)
+ {
+ 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
+ {
+ break;
+ }
+ }
+
+ string callArray;
+ switch (callArgs.Count)
+ {
+ case 0:
+ callArray = "";
+ break;
+ case 1:
+ callArray = callArgs[0];
+ break;
+ default:
+ callArray = $"new object?[] {{ {string.Join(", ", callArgs)} }}";
+ break;
+ }
+
+ sb.Append(string.Join(", ", declarationArgs));
+ sb.Append(") => ");
+ if (contextPropertyName is null)
+ {
+ if (string.IsNullOrEmpty(callArray))
+ {
+ sb.AppendLine($"InternationalizationManager.Instance.GetTranslation(\"{localizableString.Key}\");");
+ }
+ else
+ {
+ sb.AppendLine(
+ $"string.Format(InternationalizationManager.Instance.GetTranslation(\"{localizableString.Key}\"), {callArray});"
+ );
+ }
+ }
+ 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();
+ }
+
+ private static Location GetLocation(SyntaxTree syntaxTree, CSharpSyntaxNode classDeclaration)
+ {
+ return Location.Create(syntaxTree, classDeclaration.GetLocation().SourceSpan);
+ }
+
+ private static IEnumerable<(SyntaxTree, ClassDeclarationSyntax)> GetClasses(GeneratorExecutionContext context)
+ {
+ foreach (var syntaxTree in context.Compilation.SyntaxTrees)
+ {
+ var classDeclarations = syntaxTree.GetRoot().DescendantNodes().OfType();
+ foreach (var classDeclaration in classDeclarations)
+ {
+ yield return (syntaxTree, classDeclaration);
+ }
+ }
+ }
+
+ private static bool DoesClassImplementInterface(ClassDeclarationSyntax classDeclaration, string interfaceName)
+ {
+ return classDeclaration.BaseList?.Types.Any(v => interfaceName == v.ToString()) is true;
+ }
+
+ private static PropertyDeclarationSyntax GetPluginContextProperty(ClassDeclarationSyntax classDeclaration)
+ {
+ return classDeclaration.Members
+ .OfType()
+ .FirstOrDefault(v => v.Type.ToString() is PluginContextTypeName);
+ }
+
+ private static Modifiers GetPropertyModifiers(PropertyDeclarationSyntax property)
+ {
+ 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);
+
+ return new Modifiers(isStatic, isPrivate, isProtected);
+ }
+
+ private class Modifiers
+ {
+ public bool Static { get; }
+ public bool Private { get; }
+ public bool Protected { get; }
+
+ public Modifiers(bool isStatic = false, bool isPrivate = false, bool isProtected = false)
+ {
+ Static = isStatic;
+ Private = isPrivate;
+ Protected = isProtected;
+ }
+ }
+ }
+
+ public class LocalizableStringParam
+ {
+ public int Index { get; }
+ public string Name { get; }
+ public string Type { get; }
+
+ public LocalizableStringParam(int index, string name, string type)
+ {
+ Index = index;
+ Name = name;
+ Type = type;
+ }
+ }
+
+ public class LocalizableString
+ {
+ public string Key { get; }
+ public string Value { get; }
+ public string Summary { get; }
+ public IEnumerable Params { get; }
+
+ public LocalizableString(string key, string value, string summary, IEnumerable @params)
+ {
+ Key = key;
+ Value = value;
+ Summary = summary;
+ Params = @params;
+ }
+ }
+}
diff --git a/Flow.Launcher.SourceGenerators/SourceGeneratorDiagnostics.cs b/Flow.Launcher.SourceGenerators/SourceGeneratorDiagnostics.cs
new file mode 100644
index 00000000000..296419bd3e1
--- /dev/null
+++ b/Flow.Launcher.SourceGenerators/SourceGeneratorDiagnostics.cs
@@ -0,0 +1,70 @@
+using Microsoft.CodeAnalysis;
+
+namespace Flow.Launcher.SourceGenerators
+{
+ public static class SourceGeneratorDiagnostics
+ {
+ public static readonly DiagnosticDescriptor CouldNotFindResourceDictionaries = new DiagnosticDescriptor(
+ "FLSG0001",
+ "Could not find resource dictionaries",
+ "Could not find resource dictionaries. There must be a file named [LANG].xaml file (for example, en.xaml), and it must be specified in in your .csproj file.",
+ "Localization",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true
+ );
+
+ public static readonly DiagnosticDescriptor CouldNotFindPluginEntryClass = new DiagnosticDescriptor(
+ "FLSG0002",
+ "Could not find the main class of plugin",
+ "Could not find the main class of your plugin. It must implement IPluginI18n.",
+ "Localization",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true
+ );
+
+ public static readonly DiagnosticDescriptor CouldNotFindContextProperty = new DiagnosticDescriptor(
+ "FLSG0003",
+ "Could not find plugin context property",
+ "Could not find a property of type PluginInitContext in {0}. It must be a public static or internal static property of the main class of your plugin.",
+ "Localization",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true
+ );
+
+ public static readonly DiagnosticDescriptor ContextPropertyNotStatic = new DiagnosticDescriptor(
+ "FLSG0004",
+ "Plugin context property is not static",
+ "Context property {0} is not static. It must be static.",
+ "Localization",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true
+ );
+
+ public static readonly DiagnosticDescriptor ContextPropertyIsPrivate = new DiagnosticDescriptor(
+ "FLSG0005",
+ "Plugin context property is private",
+ "Context property {0} is private. It must be either internal or public.",
+ "Localization",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true
+ );
+
+ public static readonly DiagnosticDescriptor ContextPropertyIsProtected = new DiagnosticDescriptor(
+ "FLSG0006",
+ "Plugin context property is protected",
+ "Context property {0} is protected. It must be either internal or public.",
+ "Localization",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true
+ );
+
+ public static readonly DiagnosticDescriptor LocalizationKeyUnused = new DiagnosticDescriptor(
+ "FLSG0007",
+ "Localization key is unused",
+ "Method 'Localize.{0}' is never used",
+ "Localization",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true
+ );
+ }
+}
diff --git a/Flow.Launcher.sln b/Flow.Launcher.sln
index e44b23232fb..bdbcdc4d7db 100644
--- a/Flow.Launcher.sln
+++ b/Flow.Launcher.sln
@@ -71,6 +71,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Plugin
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.WindowsSettings", "Plugins\Flow.Launcher.Plugin.WindowsSettings\Flow.Launcher.Plugin.WindowsSettings.csproj", "{5043CECE-E6A7-4867-9CBE-02D27D83747A}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flow.Launcher.SourceGenerators", "Flow.Launcher.SourceGenerators\Flow.Launcher.SourceGenerators.csproj", "{2CB3B152-6147-48E2-B497-CEFA3331E9B4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flow.Launcher.Analyzers", "Flow.Launcher.Analyzers\Flow.Launcher.Analyzers.csproj", "{58A54F2B-772F-4724-861D-F3A191441DF1}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -82,7 +86,7 @@ Global
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x64.ActiveCfg = Debug|Any CPU
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x64.Build.0 = Debug|Any CPU
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x86.ActiveCfg = Debug|Any CPU
@@ -286,6 +290,30 @@ Global
{5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|x64.Build.0 = Release|Any CPU
{5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|x86.ActiveCfg = Release|Any CPU
{5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|x86.Build.0 = Release|Any CPU
+ {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Debug|x64.Build.0 = Debug|Any CPU
+ {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Debug|x86.Build.0 = Debug|Any CPU
+ {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Release|x64.ActiveCfg = Release|Any CPU
+ {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Release|x64.Build.0 = Release|Any CPU
+ {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Release|x86.ActiveCfg = Release|Any CPU
+ {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Release|x86.Build.0 = Release|Any CPU
+ {58A54F2B-772F-4724-861D-F3A191441DF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {58A54F2B-772F-4724-861D-F3A191441DF1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {58A54F2B-772F-4724-861D-F3A191441DF1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {58A54F2B-772F-4724-861D-F3A191441DF1}.Debug|x64.Build.0 = Debug|Any CPU
+ {58A54F2B-772F-4724-861D-F3A191441DF1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {58A54F2B-772F-4724-861D-F3A191441DF1}.Debug|x86.Build.0 = Debug|Any CPU
+ {58A54F2B-772F-4724-861D-F3A191441DF1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {58A54F2B-772F-4724-861D-F3A191441DF1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {58A54F2B-772F-4724-861D-F3A191441DF1}.Release|x64.ActiveCfg = Release|Any CPU
+ {58A54F2B-772F-4724-861D-F3A191441DF1}.Release|x64.Build.0 = Release|Any CPU
+ {58A54F2B-772F-4724-861D-F3A191441DF1}.Release|x86.ActiveCfg = Release|Any CPU
+ {58A54F2B-772F-4724-861D-F3A191441DF1}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml
index 070c290cdfb..cab0da722c1 100644
--- a/Flow.Launcher/Languages/en.xaml
+++ b/Flow.Launcher/Languages/en.xaml
@@ -146,7 +146,9 @@
Result Item Font
Window Mode
Opacity
+
Theme {0} not exists, fallback to default theme
+
Fail to load theme {0}, fallback to default theme
Theme Folder
Open Theme Folder
@@ -254,6 +256,7 @@
You have activated Flow Launcher {0} times
Check for Updates
Become A Sponsor
+
New version {0} is available, would you like to restart Flow Launcher to use the update?
Check updates failed, please check your connection and proxy settings to api.github.com.
@@ -358,6 +361,8 @@
You already have the latest Flow Launcher version
Update found
Updating...
+
Flow Launcher was not able to move your user profile data to the new update version.
Please manually move your profile data folder from {0} to {1}
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
index fe118c2c3bf..0ce9d6d458f 100644
--- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
+++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
@@ -59,4 +59,16 @@
-
\ No newline at end of file
+
+
+
+
+
+
+
diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj b/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj
index 415f852f4c8..81170d95f85 100644
--- a/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj
+++ b/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj
@@ -13,7 +13,7 @@
false
en
-
+
true
portable
@@ -24,7 +24,7 @@
4
false
-
+
pdbonly
true
@@ -34,13 +34,13 @@
4
false
-
+
PreserveNewest
-
+
@@ -64,5 +64,17 @@
-
-
\ No newline at end of file
+
+
+
+
+
+
+
+