diff --git a/Flow.Launcher.Localization/.github/workflows/build.yml b/Flow.Launcher.Localization/.github/workflows/build.yml
new file mode 100644
index 00000000000..d23ee423d5d
--- /dev/null
+++ b/Flow.Launcher.Localization/.github/workflows/build.yml
@@ -0,0 +1,54 @@
+name: build
+
+on:
+ workflow_dispatch:
+ push:
+ branches: [ "main" ]
+ pull_request:
+
+jobs:
+
+ build:
+
+ runs-on: windows-latest
+
+ env:
+ Dotnet_Version: 8.0.x
+ Project_Path: Flow.Launcher.Localization\Flow.Launcher.Localization.csproj
+
+ steps:
+
+ # Checkout codes
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ # Install the .NET Core workload
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: ${{ env.Dotnet_Version }}
+
+ # Restore dependencies
+ - name: Restore dependencies
+ run: dotnet restore ${{ env.Project_Path }}
+
+ # Build the project
+ - name: Build
+ run: dotnet build ${{ env.Project_Path }} --configuration Release --no-restore
+
+ # Execute all unit tests in the solution
+ - name: Execute unit tests
+ if: github.event_name == 'push' && github.ref != 'refs/heads/main'
+ run: dotnet test --configuration Release --no-build
+
+ # Pack the NuGet package
+ - name: Create NuGet package
+ run: dotnet pack ${{ env.Project_Path }} --configuration Release --no-build --output nupkgs
+
+ # Upload the NuGet package
+ - name: Upload NuGet package
+ uses: actions/upload-artifact@v4
+ with:
+ name: Full nupkg
+ path: nupkgs/Flow.Launcher.Localization.*.nupkg
+ compression-level: 0
diff --git a/Flow.Launcher.Localization/.github/workflows/pr_assignee.yml b/Flow.Launcher.Localization/.github/workflows/pr_assignee.yml
new file mode 100644
index 00000000000..368932293d2
--- /dev/null
+++ b/Flow.Launcher.Localization/.github/workflows/pr_assignee.yml
@@ -0,0 +1,15 @@
+name: Assign PR to creator
+
+on:
+ pull_request_target:
+ types: [opened]
+
+permissions:
+ pull-requests: write
+
+jobs:
+ automation:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Assign PR to creator
+ uses: toshimaru/auto-author-assign@v2.1.1
diff --git a/Flow.Launcher.Localization/.github/workflows/publish.yml b/Flow.Launcher.Localization/.github/workflows/publish.yml
new file mode 100644
index 00000000000..70c7809dd77
--- /dev/null
+++ b/Flow.Launcher.Localization/.github/workflows/publish.yml
@@ -0,0 +1,61 @@
+name: publish
+
+on:
+ workflow_dispatch:
+ push:
+ tags:
+ - '*'
+
+jobs:
+
+ publish:
+
+ runs-on: windows-latest
+
+ env:
+ Dotnet_Version: 8.0.x
+ Project_Path: Flow.Launcher.Localization\Flow.Launcher.Localization.csproj
+
+ steps:
+
+ # Checkout codes
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ # Install the .NET Core workload
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: ${{ env.Dotnet_Version }}
+
+ # Restore dependencies
+ - name: Restore dependencies
+ run: dotnet restore ${{ env.Project_Path }}
+
+ # Build the project
+ - name: Build
+ run: dotnet build ${{ env.Project_Path }} --configuration Release --no-restore
+
+ # Pack the NuGet package
+ - name: Create NuGet package
+ run: dotnet pack ${{ env.Project_Path }} --configuration Release --no-build --output nupkgs
+
+ # Publish to NuGet.org
+ - name: Push to NuGet
+ # if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+ run: nuget push nupkgs\*.nupkg -source 'https://api.nuget.org/v3/index.json' -apikey ${{ secrets.NUGET_API_KEY }}
+
+ # Get package version
+ - name: Get Package Version
+ # if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+ run: |
+ $version = [system.diagnostics.fileversioninfo]::getversioninfo("Flow.Launcher.Localization\bin\Release\netstandard2.0\Flow.Launcher.Localization.dll").fileversion
+ echo "release_version=$version" | out-file -filepath $env:github_env -encoding utf-8 -append
+
+ # Publish to GitHub releases
+ - name: Publish GitHub releases
+ # if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+ uses: softprops/action-gh-release@v1
+ with:
+ files: "nupkgs/*.nupkg"
+ tag_name: "v${{ env.release_version }}"
diff --git a/Flow.Launcher.Localization/.gitignore b/Flow.Launcher.Localization/.gitignore
new file mode 100644
index 00000000000..a5e6adf01a3
--- /dev/null
+++ b/Flow.Launcher.Localization/.gitignore
@@ -0,0 +1,7 @@
+.idea/
+.vs/
+bin/
+obj/
+/packages/
+riderModule.iml
+/_ReSharper.Caches/
diff --git a/Flow.Launcher.Localization/Flow.Launcher.Localization.Analyzers/AnalyzerDiagnostics.cs b/Flow.Launcher.Localization/Flow.Launcher.Localization.Analyzers/AnalyzerDiagnostics.cs
new file mode 100644
index 00000000000..83174541351
--- /dev/null
+++ b/Flow.Launcher.Localization/Flow.Launcher.Localization.Analyzers/AnalyzerDiagnostics.cs
@@ -0,0 +1,53 @@
+using Flow.Launcher.Localization.Shared;
+using Microsoft.CodeAnalysis;
+
+namespace Flow.Launcher.Localization.Analyzers
+{
+ public static class AnalyzerDiagnostics
+ {
+ public static readonly DiagnosticDescriptor OldLocalizationApiUsed = new DiagnosticDescriptor(
+ "FLAN0001",
+ "Old localization API used",
+ $"Use `{Constants.ClassName}.{{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 `{Constants.PluginContextTypeName}`",
+ "Localization",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+ }
+}
diff --git a/Flow.Launcher.Localization/Flow.Launcher.Localization.Analyzers/AnalyzerReleases.Shipped.md b/Flow.Launcher.Localization/Flow.Launcher.Localization.Analyzers/AnalyzerReleases.Shipped.md
new file mode 100644
index 00000000000..5ccc9f037f6
--- /dev/null
+++ b/Flow.Launcher.Localization/Flow.Launcher.Localization.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.Localization/Flow.Launcher.Localization.Analyzers/AnalyzerReleases.Unshipped.md b/Flow.Launcher.Localization/Flow.Launcher.Localization.Analyzers/AnalyzerReleases.Unshipped.md
new file mode 100644
index 00000000000..d5f177c6c1a
--- /dev/null
+++ b/Flow.Launcher.Localization/Flow.Launcher.Localization.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.Localization/Flow.Launcher.Localization.Analyzers/Flow.Launcher.Localization.Analyzers.csproj b/Flow.Launcher.Localization/Flow.Launcher.Localization.Analyzers/Flow.Launcher.Localization.Analyzers.csproj
new file mode 100644
index 00000000000..b70b72e5b9b
--- /dev/null
+++ b/Flow.Launcher.Localization/Flow.Launcher.Localization.Analyzers/Flow.Launcher.Localization.Analyzers.csproj
@@ -0,0 +1,22 @@
+
+
+
+ netstandard2.0
+ true
+ Flow.Launcher.Localization.Analyzers
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher.Localization/Flow.Launcher.Localization.Analyzers/Localize/ContextAvailabilityAnalyzer.cs b/Flow.Launcher.Localization/Flow.Launcher.Localization.Analyzers/Localize/ContextAvailabilityAnalyzer.cs
new file mode 100644
index 00000000000..fa9da7604a2
--- /dev/null
+++ b/Flow.Launcher.Localization/Flow.Launcher.Localization.Analyzers/Localize/ContextAvailabilityAnalyzer.cs
@@ -0,0 +1,106 @@
+using System.Collections.Immutable;
+using System.Linq;
+using Flow.Launcher.Localization.Shared;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Flow.Launcher.Localization.Analyzers.Localize
+{
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public class ContextAvailabilityAnalyzer : DiagnosticAnalyzer
+ {
+ #region DiagnosticAnalyzer
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(
+ AnalyzerDiagnostics.ContextIsAField,
+ AnalyzerDiagnostics.ContextIsNotStatic,
+ AnalyzerDiagnostics.ContextAccessIsTooRestrictive,
+ AnalyzerDiagnostics.ContextIsNotDeclared
+ );
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ClassDeclaration);
+ }
+
+ #endregion
+
+ #region Analyze Methods
+
+ private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
+ {
+ var configOptions = context.Options.AnalyzerConfigOptionsProvider;
+ var useDI = configOptions.GetFLLUseDependencyInjection();
+ if (useDI)
+ {
+ // If we use dependency injection, we don't need to check for this context property
+ return;
+ }
+
+ var classDeclaration = (ClassDeclarationSyntax)context.Node;
+ var semanticModel = context.SemanticModel;
+ var pluginClassInfo = Helper.GetPluginClassInfo(classDeclaration, semanticModel, context.CancellationToken);
+ if (pluginClassInfo == null)
+ {
+ // Cannot find class that implements IPluginI18n
+ return;
+ }
+
+ // Context property is found, check if it's a valid property
+ if (pluginClassInfo.PropertyName != null)
+ {
+ if (!pluginClassInfo.IsStatic)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ AnalyzerDiagnostics.ContextIsNotStatic,
+ pluginClassInfo.CodeFixLocation
+ ));
+ return;
+ }
+
+ if (pluginClassInfo.IsPrivate || pluginClassInfo.IsProtected)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ AnalyzerDiagnostics.ContextAccessIsTooRestrictive,
+ pluginClassInfo.CodeFixLocation
+ ));
+ return;
+ }
+
+ // If the context property is valid, we don't need to check for anything else
+ return;
+ }
+
+ // Context property is not found, check if it's declared as a field
+ 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 Constants.PluginContextTypeName);
+ var parentSyntax = fieldDeclaration
+ ?.DeclaringSyntaxReferences[0]
+ .GetSyntax()
+ .FirstAncestorOrSelf();
+ if (parentSyntax != null)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ AnalyzerDiagnostics.ContextIsAField,
+ parentSyntax.GetLocation()
+ ));
+ return;
+ }
+
+ // Context property is not found, report an error
+ context.ReportDiagnostic(Diagnostic.Create(
+ AnalyzerDiagnostics.ContextIsNotDeclared,
+ classDeclaration.Identifier.GetLocation()
+ ));
+ }
+
+ #endregion
+ }
+}
diff --git a/Flow.Launcher.Localization/Flow.Launcher.Localization.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs b/Flow.Launcher.Localization/Flow.Launcher.Localization.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs
new file mode 100644
index 00000000000..8ef920c37cc
--- /dev/null
+++ b/Flow.Launcher.Localization/Flow.Launcher.Localization.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs
@@ -0,0 +1,201 @@
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading.Tasks;
+using Flow.Launcher.Localization.Shared;
+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.Localization.Analyzers.Localize
+{
+ [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ContextAvailabilityAnalyzerCodeFixProvider)), Shared]
+ public class ContextAvailabilityAnalyzerCodeFixProvider : CodeFixProvider
+ {
+ #region 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
+ );
+ }
+ }
+
+ #endregion
+
+ #region Fix Methods
+
+ 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);
+ }
+
+ #region Utils
+
+ private static MemberDeclarationSyntax GetStaticContextPropertyDeclaration(string propertyName = "Context") =>
+ SyntaxFactory.ParseMemberDeclaration(
+ $"internal static {Constants.PluginContextTypeName} {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 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();
+
+ #endregion
+
+ #endregion
+ }
+}
diff --git a/Flow.Launcher.Localization/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzer.cs b/Flow.Launcher.Localization/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzer.cs
new file mode 100644
index 00000000000..9577a8cc314
--- /dev/null
+++ b/Flow.Launcher.Localization/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzer.cs
@@ -0,0 +1,130 @@
+using System.Collections.Immutable;
+using System.Linq;
+using Flow.Launcher.Localization.Shared;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Flow.Launcher.Localization.Analyzers.Localize
+{
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public class OldGetTranslateAnalyzer : DiagnosticAnalyzer
+ {
+ #region DiagnosticAnalyzer
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(
+ AnalyzerDiagnostics.OldLocalizationApiUsed
+ );
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.InvocationExpression);
+ }
+
+ #endregion
+
+ #region Analyze Methods
+
+ private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
+ {
+ var invocationExpr = (InvocationExpressionSyntax)context.Node;
+ var semanticModel = context.SemanticModel;
+ var symbolInfo = semanticModel.GetSymbolInfo(invocationExpr);
+
+ // Check if the method is a format string call
+ if (!(symbolInfo.Symbol is IMethodSymbol methodSymbol)) return;
+
+ // First branch: detect a call to string.Format containing a translate call anywhere in its arguments.
+ if (IsFormatStringCall(methodSymbol))
+ {
+ var arguments = invocationExpr.ArgumentList.Arguments;
+ // Check all arguments is an invocation (i.e. a candidate for Context.API.GetTranslation(…))
+ for (int i = 0; i < arguments.Count; i++)
+ {
+ if (GetArgumentInvocationExpression(invocationExpr, i) is InvocationExpressionSyntax innerInvocationExpr &&
+ IsTranslateCall(semanticModel.GetSymbolInfo(innerInvocationExpr)) &&
+ GetFirstArgumentStringValue(innerInvocationExpr) is string translationKey)
+ {
+ var diagnostic = Diagnostic.Create(
+ AnalyzerDiagnostics.OldLocalizationApiUsed,
+ invocationExpr.GetLocation(),
+ translationKey,
+ GetInvocationArguments(invocationExpr, i)
+ );
+ context.ReportDiagnostic(diagnostic);
+ return;
+ }
+ }
+ }
+ // Second branch: direct translate call (outside of a Format call)
+ 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);
+ }
+ }
+
+ #region Utils
+
+ private static string GetInvocationArguments(InvocationExpressionSyntax invocationExpr, int translateArgIndex) =>
+ string.Join(", ", invocationExpr.ArgumentList.Arguments.Skip(translateArgIndex + 1));
+
+ ///
+ /// Walk up the tree to see if we're already inside a Format call
+ ///
+ private static bool IsParentFormatStringCall(SemanticModel semanticModel, SyntaxNode syntaxNode)
+ {
+ var parent = syntaxNode.Parent;
+ while (parent != null)
+ {
+ if (parent is InvocationExpressionSyntax parentInvocation)
+ {
+ var symbol = semanticModel.GetSymbolInfo(parentInvocation).Symbol as IMethodSymbol;
+ if (IsFormatStringCall(symbol))
+ {
+ return true;
+ }
+ }
+ parent = parent.Parent;
+ }
+ return false;
+ }
+
+ private static bool IsFormatStringCall(IMethodSymbol methodSymbol) =>
+ methodSymbol?.Name == Constants.StringFormatMethodName &&
+ methodSymbol.ContainingType.ToDisplayString() == Constants.StringFormatTypeName;
+
+ private static InvocationExpressionSyntax GetArgumentInvocationExpression(InvocationExpressionSyntax invocationExpr, int index) =>
+ invocationExpr.ArgumentList.Arguments[index].Expression as InvocationExpressionSyntax;
+
+ private static bool IsTranslateCall(SymbolInfo symbolInfo) =>
+ symbolInfo.Symbol is IMethodSymbol innerMethodSymbol &&
+ innerMethodSymbol.Name == Constants.OldLocalizationMethodName &&
+ Constants.OldLocalizationClasses.Contains(innerMethodSymbol.ContainingType.Name);
+
+ private static bool IsTranslateCall(IMethodSymbol methodSymbol) =>
+ methodSymbol?.Name is Constants.OldLocalizationMethodName &&
+ Constants.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;
+ }
+
+ #endregion
+
+ #endregion
+ }
+}
diff --git a/Flow.Launcher.Localization/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs b/Flow.Launcher.Localization/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs
new file mode 100644
index 00000000000..7e73b87962b
--- /dev/null
+++ b/Flow.Launcher.Localization/Flow.Launcher.Localization.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs
@@ -0,0 +1,141 @@
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading.Tasks;
+using Flow.Launcher.Localization.Shared;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Flow.Launcher.Localization.Analyzers.Localize
+{
+ [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(OldGetTranslateAnalyzerCodeFixProvider)), Shared]
+ public class OldGetTranslateAnalyzerCodeFixProvider : CodeFixProvider
+ {
+ #region 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 '{Constants.ClassName}.localization_key(...args)'",
+ createChangedDocument: _ => Task.FromResult(FixOldTranslation(context, root, diagnostic)),
+ equivalenceKey: AnalyzerDiagnostics.OldLocalizationApiUsed.Id
+ ),
+ diagnostic
+ );
+ }
+
+ #endregion
+
+ #region Fix Methods
+
+ private static Document FixOldTranslation(CodeFixContext context, SyntaxNode root, Diagnostic diagnostic)
+ {
+ var diagnosticSpan = diagnostic.Location.SourceSpan;
+
+ if (root is null) return context.Document;
+
+ var invocationExpr = root
+ .FindToken(diagnosticSpan.Start).Parent
+ .AncestorsAndSelf()
+ .OfType()
+ .FirstOrDefault();
+
+ if (invocationExpr is null) return context.Document;
+
+ var argumentList = invocationExpr.ArgumentList.Arguments;
+
+ // Loop through the arguments to find the translation key.
+ for (int i = 0; i < argumentList.Count; i++)
+ {
+ var argument = argumentList[i].Expression;
+
+ // Case 1: The argument is a literal (direct GetTranslation("key"))
+ if (GetTranslationKey(argument) is string translationKey)
+ return FixOldTranslationWithoutStringFormat(context, translationKey, root, invocationExpr);
+
+ // Case 2: The argument is itself an invocation (nested GetTranslation)
+ if (GetTranslationKeyFromInnerInvocation(argument) is string translationKeyInside)
+ {
+ // If there are arguments following this translation call, treat as a Format call.
+ if (i < argumentList.Count - 1)
+ return FixOldTranslationWithStringFormat(context, argumentList, translationKeyInside, root, invocationExpr, i);
+
+ // Otherwise, treat it as a direct translation call.
+ return FixOldTranslationWithoutStringFormat(context, translationKeyInside, root, invocationExpr);
+ }
+ }
+
+ return context.Document;
+ }
+
+ #region Utils
+
+ 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(
+ $"{Constants.ClassName}.{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 == 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,
+ int translationArgIndex)
+ {
+ // Skip all arguments before and including the translation call
+ var newArguments = string.Join(", ", argumentList.Skip(translationArgIndex + 1).Select(a => a.Expression));
+ var newInnerInvocationExpr = SyntaxFactory.ParseExpression($"{Constants.ClassName}.{translationKey2}({newArguments})");
+
+ var newRoot = root.ReplaceNode(invocationExpr, newInnerInvocationExpr);
+ return context.Document.WithSyntaxRoot(newRoot);
+ }
+
+ #endregion
+
+ #endregion
+ }
+}
diff --git a/Flow.Launcher.Localization/Flow.Launcher.Localization.Attributes/EnumLocalizeAttribute.cs b/Flow.Launcher.Localization/Flow.Launcher.Localization.Attributes/EnumLocalizeAttribute.cs
new file mode 100644
index 00000000000..21f2a06f0ef
--- /dev/null
+++ b/Flow.Launcher.Localization/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/Flow.Launcher.Localization.Attributes/EnumLocalizeKeyAttribute.cs b/Flow.Launcher.Localization/Flow.Launcher.Localization.Attributes/EnumLocalizeKeyAttribute.cs
new file mode 100644
index 00000000000..39d75eb14ab
--- /dev/null
+++ b/Flow.Launcher.Localization/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/Flow.Launcher.Localization.Attributes/EnumLocalizeValueAttribute.cs b/Flow.Launcher.Localization/Flow.Launcher.Localization.Attributes/EnumLocalizeValueAttribute.cs
new file mode 100644
index 00000000000..75c3859a761
--- /dev/null
+++ b/Flow.Launcher.Localization/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/Flow.Launcher.Localization.Attributes/Flow.Launcher.Localization.Attributes.csproj b/Flow.Launcher.Localization/Flow.Launcher.Localization.Attributes/Flow.Launcher.Localization.Attributes.csproj
new file mode 100644
index 00000000000..dd973d9abc9
--- /dev/null
+++ b/Flow.Launcher.Localization/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/Flow.Launcher.Localization.Shared/Classes.cs b/Flow.Launcher.Localization/Flow.Launcher.Localization.Shared/Classes.cs
new file mode 100644
index 00000000000..9c78d98a330
--- /dev/null
+++ b/Flow.Launcher.Localization/Flow.Launcher.Localization.Shared/Classes.cs
@@ -0,0 +1,29 @@
+using Microsoft.CodeAnalysis;
+
+namespace Flow.Launcher.Localization.Shared
+{
+ public class PluginClassInfo
+ {
+ 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 Location CodeFixLocation { get; }
+
+ public string ContextAccessor => $"{ClassName}.{PropertyName}";
+ public bool IsValid => PropertyName != null && IsStatic && (!IsPrivate) && (!IsProtected);
+
+ public PluginClassInfo(Location location, string className, string propertyName, bool isStatic, bool isPrivate, bool isProtected, Location codeFixLocation)
+ {
+ Location = location;
+ ClassName = className;
+ PropertyName = propertyName;
+ IsStatic = isStatic;
+ IsPrivate = isPrivate;
+ IsProtected = isProtected;
+ CodeFixLocation = codeFixLocation;
+ }
+ }
+}
diff --git a/Flow.Launcher.Localization/Flow.Launcher.Localization.Shared/Constants.cs b/Flow.Launcher.Localization/Flow.Launcher.Localization.Shared/Constants.cs
new file mode 100644
index 00000000000..3b3fe069609
--- /dev/null
+++ b/Flow.Launcher.Localization/Flow.Launcher.Localization.Shared/Constants.cs
@@ -0,0 +1,35 @@
+using System.Text.RegularExpressions;
+
+namespace Flow.Launcher.Localization.Shared
+{
+ public static class Constants
+ {
+ public const string DefaultNamespace = "Flow.Launcher";
+ public const string ClassName = "Localize";
+ public const string PluginInterfaceName = "IPluginI18n";
+ public const string PluginContextTypeName = "PluginInitContext";
+ public const string SystemPrefixUri = "clr-namespace:System;assembly=mscorlib";
+ public const string XamlPrefixUri = "http://schemas.microsoft.com/winfx/2006/xaml";
+ public const string XamlTag = "String";
+ public const string KeyAttribute = "Key";
+ public const string SummaryElementName = "summary";
+ public const string ParamElementName = "param";
+ public const string IndexAttribute = "index";
+ public const string NameAttribute = "name";
+ public const string TypeAttribute = "type";
+ public const string OldLocalizationMethodName = "GetTranslation";
+ public const string StringFormatMethodName = "Format";
+ public const string StringFormatTypeName = "string";
+ public const string EnumLocalizeClassSuffix = "Localized";
+ 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/Flow.Launcher.Localization.Shared/Flow.Launcher.Localization.Shared.csproj b/Flow.Launcher.Localization/Flow.Launcher.Localization.Shared/Flow.Launcher.Localization.Shared.csproj
new file mode 100644
index 00000000000..b8c2598eb6e
--- /dev/null
+++ b/Flow.Launcher.Localization/Flow.Launcher.Localization.Shared/Flow.Launcher.Localization.Shared.csproj
@@ -0,0 +1,13 @@
+
+
+
+ netstandard2.0
+ true
+ Flow.Launcher.Localization.Shared
+
+
+
+
+
+
+
diff --git a/Flow.Launcher.Localization/Flow.Launcher.Localization.Shared/Helper.cs b/Flow.Launcher.Localization/Flow.Launcher.Localization.Shared/Helper.cs
new file mode 100644
index 00000000000..0807ceff88a
--- /dev/null
+++ b/Flow.Launcher.Localization/Flow.Launcher.Localization.Shared/Helper.cs
@@ -0,0 +1,94 @@
+using System.Linq;
+using System.Threading;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Flow.Launcher.Localization.Shared
+{
+ public static class Helper
+ {
+ #region Build Properties
+
+ public static bool GetFLLUseDependencyInjection(this AnalyzerConfigOptionsProvider configOptions)
+ {
+ if (!configOptions.GlobalOptions.TryGetValue("build_property.FLLUseDependencyInjection", out var result) ||
+ !bool.TryParse(result, out var useDI))
+ {
+ return false; // Default to false
+ }
+ return useDI;
+ }
+
+ #endregion
+
+ #region Plugin Class Info
+
+ ///
+ /// If cannot find the class that implements IPluginI18n, return null.
+ /// If cannot find the context property, return PluginClassInfo with null context property name.
+ ///
+ public static PluginClassInfo GetPluginClassInfo(ClassDeclarationSyntax classDecl, SemanticModel semanticModel, CancellationToken ct)
+ {
+ var classSymbol = semanticModel.GetDeclaredSymbol(classDecl, ct);
+ if (!IsPluginEntryClass(classSymbol))
+ {
+ // Cannot find class that implements IPluginI18n
+ return null;
+ }
+
+ var property = GetContextProperty(classDecl);
+ var location = GetLocation(semanticModel.SyntaxTree, classDecl);
+ if (property is null)
+ {
+ // Cannot find context
+ return new PluginClassInfo(location, classDecl.Identifier.Text, null, false, false, false, null);
+ }
+
+ var modifiers = property.Modifiers;
+ var codeFixLocation = GetCodeFixLocation(property, semanticModel);
+ return new PluginClassInfo(
+ location,
+ classDecl.Identifier.Text,
+ property.Identifier.Text,
+ modifiers.Any(SyntaxKind.StaticKeyword),
+ modifiers.Any(SyntaxKind.PrivateKeyword),
+ modifiers.Any(SyntaxKind.ProtectedKeyword),
+ codeFixLocation);
+ }
+
+ private static bool IsPluginEntryClass(INamedTypeSymbol namedTypeSymbol)
+ {
+ return namedTypeSymbol?.Interfaces.Any(i => i.Name == Constants.PluginInterfaceName) ?? false;
+ }
+
+ private static PropertyDeclarationSyntax GetContextProperty(ClassDeclarationSyntax classDecl)
+ {
+ return classDecl.Members
+ .OfType()
+ .FirstOrDefault(p => p.Type.ToString() == Constants.PluginContextTypeName);
+ }
+
+ private static Location GetLocation(SyntaxTree syntaxTree, CSharpSyntaxNode classDeclaration)
+ {
+ return Location.Create(syntaxTree, classDeclaration.GetLocation().SourceSpan);
+ }
+
+ private static Location GetCodeFixLocation(PropertyDeclarationSyntax property, SemanticModel semanticModel)
+ {
+ return semanticModel.GetDeclaredSymbol(property).DeclaringSyntaxReferences[0].GetSyntax().GetLocation();
+ }
+
+ #endregion
+
+ #region Tab String
+
+ public static string Spacing(int n)
+ {
+ return new string(' ', n * 4);
+ }
+
+ #endregion
+ }
+}
diff --git a/Flow.Launcher.Localization/Flow.Launcher.Localization.SourceGenerators/AnalyzerReleases.Shipped.md b/Flow.Launcher.Localization/Flow.Launcher.Localization.SourceGenerators/AnalyzerReleases.Shipped.md
new file mode 100644
index 00000000000..5ccc9f037f6
--- /dev/null
+++ b/Flow.Launcher.Localization/Flow.Launcher.Localization.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.Localization/Flow.Launcher.Localization.SourceGenerators/AnalyzerReleases.Unshipped.md b/Flow.Launcher.Localization/Flow.Launcher.Localization.SourceGenerators/AnalyzerReleases.Unshipped.md
new file mode 100644
index 00000000000..c1ad7a3758c
--- /dev/null
+++ b/Flow.Launcher.Localization/Flow.Launcher.Localization.SourceGenerators/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,14 @@
+; 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
+FLSG0008 | Localization | Warning | FLSG0008_EnumFieldLocalizationKeyValueInvalid
diff --git a/Flow.Launcher.Localization/Flow.Launcher.Localization.SourceGenerators/Flow.Launcher.Localization.SourceGenerators.csproj b/Flow.Launcher.Localization/Flow.Launcher.Localization.SourceGenerators/Flow.Launcher.Localization.SourceGenerators.csproj
new file mode 100644
index 00000000000..6835526926a
--- /dev/null
+++ b/Flow.Launcher.Localization/Flow.Launcher.Localization.SourceGenerators/Flow.Launcher.Localization.SourceGenerators.csproj
@@ -0,0 +1,23 @@
+
+
+
+ netstandard2.0
+ true
+ Flow.Launcher.Localization.SourceGenerators
+
+ 0.0.3
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher.Localization/Flow.Launcher.Localization.SourceGenerators/Localize/EnumSourceGenerator.cs b/Flow.Launcher.Localization/Flow.Launcher.Localization.SourceGenerators/Localize/EnumSourceGenerator.cs
new file mode 100644
index 00000000000..76a218f2128
--- /dev/null
+++ b/Flow.Launcher.Localization/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/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs b/Flow.Launcher.Localization/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
new file mode 100644
index 00000000000..43589cea597
--- /dev/null
+++ b/Flow.Launcher.Localization/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
@@ -0,0 +1,647 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Xml.Linq;
+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
+{
+ ///
+ /// Generates properties for strings based on resource files.
+ ///
+ [Generator]
+ public partial class LocalizeSourceGenerator : IIncrementalGenerator
+ {
+ #region Fields
+
+ private static readonly Version PackageVersion = typeof(LocalizeSourceGenerator).Assembly.GetName().Version;
+
+ private static readonly ImmutableArray _emptyLocalizableStrings = ImmutableArray.Empty;
+ private static readonly ImmutableArray _emptyLocalizableStringParams = ImmutableArray.Empty;
+
+ #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 => Constants.LanguagesXamlRegex.IsMatch(file.Path));
+
+ var localizedStrings = xamlFiles
+ .Select((file, ct) => ParseXamlFile(file, ct))
+ .Collect()
+ .SelectMany((files, _) => files);
+
+ 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: (c, t) => Helper.GetPluginClassInfo((ClassDeclarationSyntax)c.Node, c.SemanticModel, t))
+ .Where(info => info != null)
+ .Collect();
+
+ var compilation = context.CompilationProvider;
+
+ var configOptions = context.AnalyzerConfigOptionsProvider;
+
+ var combined = localizedStrings.Combine(invocationKeys).Combine(pluginClasses).Combine(configOptions).Combine(compilation).Combine(xamlFiles.Collect());
+
+ context.RegisterSourceOutput(combined, Execute);
+ }
+
+ ///
+ /// 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),
+ AnalyzerConfigOptionsProvider ConfigOptionsProvider),
+ Compilation Compilation),
+ ImmutableArray AdditionalTexts) data)
+ {
+ var xamlFiles = data.AdditionalTexts;
+ if (xamlFiles.Length == 0)
+ {
+ spc.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.CouldNotFindResourceDictionaries,
+ Location.None
+ ));
+ return;
+ }
+
+ var compilation = data.Item1.Compilation;
+ var configOptions = data.Item1.Item1.ConfigOptionsProvider;
+ var pluginClasses = data.Item1.Item1.Item1.PluginClassInfos;
+ var usedKeys = data.Item1.Item1.Item1.Item1.InvocationKeys;
+ var localizedStrings = data.Item1.Item1.Item1.Item1.LocalizableStrings;
+
+ 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;
+ }
+ }
+
+ GenerateSource(
+ spc,
+ xamlFiles[0],
+ localizedStrings,
+ assemblyNamespace,
+ useDI,
+ 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)
+ {
+ return _emptyLocalizableStrings;
+ }
+
+ var doc = XDocument.Parse(content);
+ var root = doc.Root;
+ if (root is null)
+ {
+ return _emptyLocalizableStrings;
+ }
+
+ // Find prefixes for the target URIs
+ string systemPrefix = null;
+ string xamlPrefix = null;
+
+ foreach (var attr in root.Attributes())
+ {
+ // Check if the attribute is a namespace declaration (xmlns:...)
+ if (attr.Name.NamespaceName == XNamespace.Xmlns.NamespaceName)
+ {
+ string uri = attr.Value;
+ string prefix = attr.Name.LocalName;
+
+ if (uri == Constants.SystemPrefixUri)
+ {
+ systemPrefix = prefix;
+ }
+ else if (uri == Constants.XamlPrefixUri)
+ {
+ xamlPrefix = prefix;
+ }
+ }
+ }
+
+ if (systemPrefix is null || xamlPrefix is null)
+ {
+ return _emptyLocalizableStrings;
+ }
+
+ var systemNs = doc.Root?.GetNamespaceOfPrefix(systemPrefix);
+ var xNs = doc.Root?.GetNamespaceOfPrefix(xamlPrefix);
+ if (systemNs is null || xNs is null)
+ {
+ return _emptyLocalizableStrings;
+ }
+
+ var localizableStrings = new List();
+ foreach (var element in doc.Descendants(systemNs + Constants.XamlTag)) // "String" elements in system namespace
+ {
+ if (ct.IsCancellationRequested)
+ {
+ return _emptyLocalizableStrings;
+ }
+
+ var key = element.Attribute(xNs + Constants.KeyAttribute)?.Value; // "Key" attribute in xaml namespace
+ var value = element.Value;
+ var comment = element.PreviousNode as XComment;
+
+ if (key != null)
+ {
+ var formatParams = GetParameters(value);
+ var (summary, updatedFormatParams) = ParseCommentAndUpdateParameters(comment, formatParams);
+ localizableStrings.Add(new LocalizableString(key, value, summary, updatedFormatParams));
+ }
+ }
+
+ return localizableStrings.ToImmutableArray();
+ }
+
+ ///
+ /// Analyzes the format string and returns a list of its parameters.
+ ///
+ ///
+ ///
+ private static List GetParameters(string format)
+ {
+ var parameters = new Dictionary();
+ int maxIndex = -1;
+ int i = 0;
+ int len = format.Length;
+
+ while (i < len)
+ {
+ if (format[i] == '{')
+ {
+ if (i + 1 < len && format[i + 1] == '{')
+ {
+ // Escaped '{', skip both
+ i += 2;
+ continue;
+ }
+ else
+ {
+ // Start of a format item, parse index and format
+ i++; // Move past '{'
+ int index = 0;
+ bool hasIndex = false;
+
+ // Parse index
+ while (i < len && char.IsDigit(format[i]))
+ {
+ hasIndex = true;
+ index = index * 10 + (format[i] - '0');
+ i++;
+ }
+
+ if (!hasIndex)
+ {
+ // Skip invalid format item
+ while (i < len && format[i] != '}')
+ {
+ i++;
+ }
+ if (i < len)
+ {
+ i++; // Move past '}'
+ }
+ continue;
+ }
+
+ // Check for alignment (comma followed by optional sign and digits)
+ if (i < len && format[i] == ',')
+ {
+ i++; // Skip comma and optional sign
+ if (i < len && (format[i] == '+' || format[i] == '-'))
+ {
+ i++;
+ }
+ // Skip digits
+ while (i < len && char.IsDigit(format[i]))
+ {
+ i++;
+ }
+ }
+
+ string formatPart = null;
+
+ // Check for format (after colon)
+ if (i < len && format[i] == ':')
+ {
+ i++; // Move past ':'
+ int start = i;
+ while (i < len && format[i] != '}')
+ {
+ i++;
+ }
+ formatPart = i < len ? format.Substring(start, i - start) : format.Substring(start);
+ if (i < len)
+ {
+ i++; // Move past '}'
+ }
+ }
+ else if (i < len && format[i] == '}')
+ {
+ // No format part
+ i++; // Move past '}'
+ }
+ else
+ {
+ // Invalid characters after index, skip to '}'
+ while (i < len && format[i] != '}')
+ {
+ i++;
+ }
+ if (i < len)
+ {
+ i++; // Move past '}'
+ }
+ }
+
+ parameters[index] = formatPart;
+ if (index > maxIndex)
+ {
+ maxIndex = index;
+ }
+ }
+ }
+ else if (format[i] == '}')
+ {
+ // Handle possible escaped '}}'
+ if (i + 1 < len && format[i + 1] == '}')
+ {
+ i += 2; // Skip escaped '}}'
+ }
+ else
+ {
+ i++; // Move past '}'
+ }
+ }
+ else
+ {
+ i++;
+ }
+ }
+
+ // Generate the result list from 0 to maxIndex
+ var result = new List();
+ if (maxIndex == -1)
+ {
+ return result;
+ }
+
+ for (int idx = 0; idx <= maxIndex; idx++)
+ {
+ var formatValue = parameters.TryGetValue(idx, out var value) ? value : null;
+ result.Add(new LocalizableStringParam { Index = idx, Format = formatValue, Name = $"arg{idx}", Type = "object?" });
+ }
+
+ return result;
+ }
+
+ ///
+ /// Parses the comment and updates the format parameter names and types.
+ ///
+ ///
+ ///
+ ///
+ private static (string Summary, ImmutableArray Parameters) ParseCommentAndUpdateParameters(XComment comment, List parameters)
+ {
+ if (comment == null || comment.Value == null || parameters.Count == 0)
+ {
+ return (null, _emptyLocalizableStringParams);
+ }
+
+ try
+ {
+ var doc = XDocument.Parse($"{comment.Value}");
+ var summary = doc.Descendants(Constants.SummaryElementName).FirstOrDefault()?.Value.Trim();
+
+ // Update parameter names and types of the format string
+ foreach (var p in doc.Descendants(Constants.ParamElementName))
+ {
+ var index = int.TryParse(p.Attribute(Constants.IndexAttribute).Value, out var intValue) ? intValue : -1;
+ var name = p.Attribute(Constants.NameAttribute).Value;
+ var type = p.Attribute(Constants.TypeAttribute).Value;
+ if (index >= 0 && index < parameters.Count)
+ {
+ if (!string.IsNullOrEmpty(name))
+ {
+ parameters[index].Name = name;
+ }
+ if (!string.IsNullOrEmpty(type))
+ {
+ parameters[index].Type = type;
+ }
+ }
+ }
+ return (summary, parameters.ToImmutableArray());
+ }
+ catch
+ {
+ return (null, _emptyLocalizableStringParams);
+ }
+ }
+
+ #endregion
+
+ #region Get Used Localization Keys
+
+ private static string GetLocalizationKeyFromInvocation(GeneratorSyntaxContext context, CancellationToken ct)
+ {
+ if (ct.IsCancellationRequested)
+ {
+ return null;
+ }
+
+ var invocation = (InvocationExpressionSyntax)context.Node;
+ var expression = invocation.Expression;
+ var parts = new List();
+
+ // Traverse the member access hierarchy
+ while (expression is MemberAccessExpressionSyntax memberAccess)
+ {
+ parts.Add(memberAccess.Name.Identifier.Text);
+ expression = memberAccess.Expression;
+ }
+
+ // Add the leftmost identifier
+ if (expression is IdentifierNameSyntax identifier)
+ {
+ parts.Add(identifier.Identifier.Text);
+ }
+ else
+ {
+ return null;
+ }
+
+ // 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] != Constants.ClassName)
+ {
+ return null;
+ }
+
+ return parts[1];
+ }
+
+ #endregion
+
+ #region Generate Source
+
+ private static void GenerateSource(
+ SourceProductionContext spc,
+ AdditionalText xamlFile,
+ ImmutableArray localizedStrings,
+ string assemblyNamespace,
+ bool useDI,
+ PluginClassInfo pluginInfo,
+ IEnumerable usedKeys)
+ {
+ // Report unusedKeys
+ var unusedKeys = localizedStrings
+ .Select(ls => ls.Key)
+ .ToImmutableHashSet()
+ .Except(usedKeys);
+ foreach (var key in unusedKeys)
+ {
+ spc.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.LocalizationKeyUnused,
+ Location.None,
+ key));
+ }
+
+ var sourceBuilder = new StringBuilder();
+
+ // Generate header
+ GeneratedHeaderFromPath(sourceBuilder, xamlFile.Path);
+ sourceBuilder.AppendLine();
+
+ // Generate nullable enable
+ sourceBuilder.AppendLine("#nullable enable");
+ sourceBuilder.AppendLine();
+
+ // Generate namespace
+ sourceBuilder.AppendLine($"namespace {assemblyNamespace};");
+ sourceBuilder.AppendLine();
+
+ // 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();
+
+ // Generate all unused keys
+ sourceBuilder.AppendLine("unusedKeys");
+ foreach (var key in unusedKeys)
+ {
+ sourceBuilder.AppendLine($"{key}");
+ }
+ sourceBuilder.AppendLine();
+
+ // Generate all used keys
+ sourceBuilder.AppendLine("usedKeys");
+ foreach (var key in usedKeys)
+ {
+ sourceBuilder.AppendLine($"{key}");
+ }*/
+ //sourceBuilder.AppendLine("*/");
+
+ // Generate class
+ sourceBuilder.AppendLine($"[System.CodeDom.Compiler.GeneratedCode(\"{nameof(LocalizeSourceGenerator)}\", \"{PackageVersion}\")]");
+ sourceBuilder.AppendLine($"public static class {Constants.ClassName}");
+ sourceBuilder.AppendLine("{");
+
+ var tabString = Helper.Spacing(1);
+
+ // 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 = $"{pluginInfo.ContextAccessor}.API.GetTranslation";
+ }
+
+ // Generate localization methods
+ foreach (var ls in localizedStrings)
+ {
+ var isLast = ls.Equals(localizedStrings.Last());
+ GenerateDocComments(sourceBuilder, ls, tabString);
+ GenerateLocalizationMethod(sourceBuilder, ls, getTranslation, tabString, isLast);
+ }
+
+ sourceBuilder.AppendLine("}");
+
+ // Add source to context
+ spc.AddSource($"{Constants.ClassName}.{assemblyNamespace}.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
+ }
+
+ private static void GeneratedHeaderFromPath(StringBuilder sb, string xamlFilePath)
+ {
+ if (string.IsNullOrEmpty(xamlFilePath))
+ {
+ sb.AppendLine("/// ");
+ }
+ else
+ {
+ sb.AppendLine("/// ")
+ .AppendLine($"/// From: {xamlFilePath}")
+ .AppendLine("/// ");
+ }
+ }
+
+ private static void GenerateDocComments(StringBuilder sb, LocalizableString ls, string tabString)
+ {
+ if (!string.IsNullOrEmpty(ls.Summary))
+ {
+ var summaryLines = ls.Summary.Split('\n');
+ if (summaryLines.Length > 0)
+ {
+ sb.AppendLine($"{tabString}/// ");
+ foreach (var line in summaryLines)
+ {
+ sb.AppendLine($"{tabString}/// {line.Trim()}");
+ }
+ sb.AppendLine($"{tabString}/// ");
+ }
+ }
+
+ var lines = ls.Value.Split('\n');
+ if (lines.Length > 0)
+ {
+ sb.AppendLine($"{tabString}/// ");
+ sb.AppendLine($"{tabString}/// e.g.: ");
+ foreach (var line in lines)
+ {
+ sb.AppendLine($"{tabString}/// {line.Trim()}");
+ }
+ sb.AppendLine($"{tabString}///
");
+ sb.AppendLine($"{tabString}/// ");
+ }
+ }
+
+ private static void GenerateLocalizationMethod(
+ StringBuilder sb,
+ LocalizableString ls,
+ string getTranslation,
+ string tabString,
+ bool last)
+ {
+ sb.Append($"{tabString}public static string {ls.Key}(");
+
+ // Get parameter string
+ var parameters = ls.Params.ToList();
+ sb.Append(string.Join(", ", parameters.Select(p => $"{p.Type} {p.Name}")));
+ sb.Append(") => ");
+ var formatArgs = parameters.Count > 0
+ ? $", {string.Join(", ", parameters.Select(p => p.Name))}"
+ : string.Empty;
+
+ if (!(string.IsNullOrEmpty(getTranslation)))
+ {
+ sb.AppendLine(parameters.Count > 0
+ ? !ls.Format ?
+ $"string.Format({getTranslation}(\"{ls.Key}\"){formatArgs});"
+ : $"string.Format(System.Globalization.CultureInfo.CurrentCulture, {getTranslation}(\"{ls.Key}\"){formatArgs});"
+ : $"{getTranslation}(\"{ls.Key}\");");
+ }
+ else
+ {
+ sb.AppendLine("\"LOCALIZATION_ERROR\";");
+ }
+
+ if (!last)
+ {
+ sb.AppendLine();
+ }
+ }
+
+ #endregion
+
+ #region Classes
+
+ public class LocalizableStringParam
+ {
+ public int Index { get; set; }
+ public string Format { get; set; }
+ public string Name { get; set; }
+ public string Type { get; set; }
+ }
+
+ public class LocalizableString
+ {
+ public string Key { get; }
+ public string Value { get; }
+ public string Summary { get; }
+ public IEnumerable Params { get; }
+
+ public bool Format => Params.Any(p => !string.IsNullOrEmpty(p.Format));
+
+ public LocalizableString(string key, string value, string summary, IEnumerable @params)
+ {
+ Key = key;
+ Value = value;
+ Summary = summary;
+ Params = @params;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/Flow.Launcher.Localization/Flow.Launcher.Localization.SourceGenerators/Localize/PublicApiSourceGenerator.cs b/Flow.Launcher.Localization/Flow.Launcher.Localization.SourceGenerators/Localize/PublicApiSourceGenerator.cs
new file mode 100644
index 00000000000..92593b7522c
--- /dev/null
+++ b/Flow.Launcher.Localization/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/Flow.Launcher.Localization.SourceGenerators/PluginInfoHelper.cs b/Flow.Launcher.Localization/Flow.Launcher.Localization.SourceGenerators/PluginInfoHelper.cs
new file mode 100644
index 00000000000..a26ee5793e9
--- /dev/null
+++ b/Flow.Launcher.Localization/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/Flow.Launcher.Localization.SourceGenerators/SourceGeneratorDiagnostics.cs b/Flow.Launcher.Localization/Flow.Launcher.Localization.SourceGenerators/SourceGeneratorDiagnostics.cs
new file mode 100644
index 00000000000..f8cde858e8e
--- /dev/null
+++ b/Flow.Launcher.Localization/Flow.Launcher.Localization.SourceGenerators/SourceGeneratorDiagnostics.cs
@@ -0,0 +1,80 @@
+using Flow.Launcher.Localization.Shared;
+using Microsoft.CodeAnalysis;
+
+namespace Flow.Launcher.Localization.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 `en.xaml` file under `Language` folder.",
+ "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 `{Constants.PluginInterfaceName}`.",
+ "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 `{Constants.PluginContextTypeName}` 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 `{Constants.ClassName}.{{0}}` is never used",
+ "Localization",
+ 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/Flow.Launcher.Localization.slnx b/Flow.Launcher.Localization/Flow.Launcher.Localization.slnx
new file mode 100644
index 00000000000..779a1ca196c
--- /dev/null
+++ b/Flow.Launcher.Localization/Flow.Launcher.Localization.slnx
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/Flow.Launcher.Localization/Flow.Launcher.Localization/Flow.Launcher.Localization.csproj b/Flow.Launcher.Localization/Flow.Launcher.Localization/Flow.Launcher.Localization.csproj
new file mode 100644
index 00000000000..adc3ad72879
--- /dev/null
+++ b/Flow.Launcher.Localization/Flow.Launcher.Localization/Flow.Launcher.Localization.csproj
@@ -0,0 +1,75 @@
+
+
+
+ netstandard2.0
+ true
+ Flow.Launcher.Localization
+ Flow.Launcher.Localization
+ false
+ false
+ true
+ true
+
+
+
+ 0.0.3
+ 0.0.3
+ 0.0.3
+ 0.0.3
+ Flow.Launcher.Localization
+ Flow Launcher Localization Toolkit
+ Localization toolkit for Flow Launcher and its plugins
+ Flow-Launcher
+ MIT
+ https://github.com/Flow-Launcher/Flow.Launcher.Localization
+ Localization toolkit for Flow Launcher and its plugins
+ localization-tool; localization-toolkit; flow-launcher; flow-launcher-plugins; flowlauncher; localization-tools; flow-launcher-plugin
+ true
+ true
+ README.md
+
+
+
+
+ All
+
+
+ runtime
+
+
+ All
+
+
+ All
+
+
+
+
+
+ true
+ analyzers/dotnet/cs
+ false
+
+
+ true
+ lib/$(TargetFramework)
+ true
+
+
+ true
+ analyzers/dotnet/cs
+ false
+
+
+ true
+ analyzers/dotnet/cs
+ false
+
+
+ True
+ \
+
+
+
+
+
diff --git a/Flow.Launcher.Localization/Flow.Launcher.Localization/build/Flow.Launcher.Localization.props b/Flow.Launcher.Localization/Flow.Launcher.Localization/build/Flow.Launcher.Localization.props
new file mode 100644
index 00000000000..80b3641ded5
--- /dev/null
+++ b/Flow.Launcher.Localization/Flow.Launcher.Localization/build/Flow.Launcher.Localization.props
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/Flow.Launcher.Localization/LICENSE b/Flow.Launcher.Localization/LICENSE
new file mode 100644
index 00000000000..49c7c8c7e0a
--- /dev/null
+++ b/Flow.Launcher.Localization/LICENSE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2025 The Flow Launcher team
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Flow.Launcher.Localization/README.md b/Flow.Launcher.Localization/README.md
new file mode 100644
index 00000000000..750e0fb2d21
--- /dev/null
+++ b/Flow.Launcher.Localization/README.md
@@ -0,0 +1,8 @@
+# Flow Launcher Localization Toolkit
+
+Localization toolkit for Flow Launcher and its plugins.
+
+Useful links:
+
+* [Flow Launcher localization toolkit guide](https://www.flowlauncher.com/docs/#/localization-toolkit)
+* [.NET plugin development guide](https://www.flowlauncher.com/docs/#/develop-dotnet-plugins)
diff --git a/Flow.Launcher.Localization/global.json b/Flow.Launcher.Localization/global.json
new file mode 100644
index 00000000000..36e1a9e95f3
--- /dev/null
+++ b/Flow.Launcher.Localization/global.json
@@ -0,0 +1,7 @@
+{
+ "sdk": {
+ "version": "7.0.0",
+ "rollForward": "latestMajor",
+ "allowPrerelease": false
+ }
+}
\ No newline at end of file
diff --git a/Flow.Launcher.Localization/localization-toolkit.md b/Flow.Launcher.Localization/localization-toolkit.md
new file mode 100644
index 00000000000..247ac58d215
--- /dev/null
+++ b/Flow.Launcher.Localization/localization-toolkit.md
@@ -0,0 +1,118 @@
+The Localization Toolkit helps Flow Launcher C# plugin developers make the localization process easier.
+
+## Getting Started
+
+For C# plugins, install and reference [Flow.Launcher.Localization](www.nuget.org/packages/Flow.Launcher.Localization) via NuGet.
+
+## Build Properties
+
+These are properties you can configure in your `.csproj` file to customize the localization process. You can set them in the `` section. For example, to set the `FLLUseDependencyInjection` property to `true`, add the following lines:
+
+```xml
+
+ true
+
+```
+
+### `FLLUseDependencyInjection`
+
+This flag specifies whether to use dependency injection to obtain an `IPublicAPI` instance. The default is `false`.
+- If set to `false`, the Main class (which must implement **[IPlugin](/API-Reference/Flow.Launcher.Plugin/IPlugin.md)** or **[IAsyncPlugin](/API-Reference/Flow.Launcher.Plugin/IAsyncPlugin.md)**)
+ must have a [PluginInitContext](/API-Reference/Flow.Launcher.Plugin/PluginInitContext.md) property that is at least `internal static`.
+- If set to `true`, you can access the `IPublicAPI` instance via `PublicApi.Instance` using dependency injection, and the Main class does not need to include a [PluginInitContext](/API-Reference/Flow.Launcher.Plugin/PluginInitContext.md) property.
+ (Note: This approach is not recommended for plugin projects at the moment since it limits compatibility to Flow Launcher 1.20.0 or later.)
+
+## Usage
+
+### Main Class
+
+The Main class must implement [IPluginI18n](/API-Reference/Flow.Launcher.Plugin/IPluginI18n.md).
+
+If `FLLUseDependencyInjection` is `false`, include a [PluginInitContext](/API-Reference/Flow.Launcher.Plugin/PluginInitContext.md) property, for example:
+
+```csharp
+ // Must implement IPluginI18n
+public class Main : IPlugin, IPluginI18n
+{
+ // Must be at least internal static
+ internal static PluginInitContext Context { get; private set; } = null!;
+}
+```
+
+### Localized Strings
+
+You can simplify your code by replacing calls like:
+```csharp
+Context.API.GetTranslation("flowlauncher_plugin_localization_demo_plugin_name")
+```
+with:
+```csharp
+Localize.flowlauncher_plugin_localization_demo_plugin_name()
+```
+
+If your localization string uses variables, it becomes even simpler! From this:
+```csharp
+string.Format(Context.API.GetTranslation("flowlauncher_plugin_localization_demo_value_with_keys"), firstName, lastName);
+```
+To this:
+```csharp
+Localize.flowlauncher_plugin_localization_demo_value_with_keys(firstName, lastName);
+```
+
+### Localized Enums
+
+For enum types (e.g., `DemoEnum`) that need localization in UI controls such as combo boxes, use the `EnumLocalize` attribute to enable localization. For each enum field:
+- Use `EnumLocalizeKey` to provide a custom localization key.
+- Use `EnumLocalizeValue` to provide a constant localization string.
+
+Example:
+
+```csharp
+[EnumLocalize] // Enable localization support
+public enum DemoEnum
+{
+ // Specific localization key
+ [EnumLocalizeKey("localize_key_1")]
+ Value1,
+
+ // Specific localization value
+ [EnumLocalizeValue("This is my enum value localization")]
+ Value2,
+
+ // Key takes precedence if both are present
+ [EnumLocalizeKey("localize_key_3")]
+ [EnumLocalizeValue("Localization Value")]
+ Value3,
+
+ // Using the Localize class. This way, you can't misspell localization keys, and if you rename
+ // them in your .xaml file, you won't forget to rename them here as well because the build will fail.
+ [EnumLocalizeKey(nameof(Localize.flowlauncher_plugin_localization_demo_plugin_description))]
+ Value4,
+}
+```
+
+Then, use the generated `DemoEnumLocalized` class within your view model to bind to a combo box control:
+
+```csharp
+// ComboBox ItemSource
+public List AllDemoEnums { get; } = DemoEnumLocalized.GetValues();
+
+// ComboBox SelectedValue
+public DemoEnum SelectedDemoEnum { get; set; }
+```
+
+In your XAML, bind as follows:
+
+```xml
+
+```
+
+To update localization strings when the language changes, you can call:
+
+```csharp
+DemoEnumLocalize.UpdateLabels(AllDemoEnums);
+```
diff --git a/Flow.Launcher.sln b/Flow.Launcher.sln
index e44b23232fb..67c0f25bca7 100644
--- a/Flow.Launcher.sln
+++ b/Flow.Launcher.sln
@@ -53,8 +53,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
LICENSE = LICENSE
Scripts\post_build.ps1 = Scripts\post_build.ps1
README.md = README.md
- SolutionAssemblyInfo.cs = SolutionAssemblyInfo.cs
Settings.XamlStyler = Settings.XamlStyler
+ SolutionAssemblyInfo.cs = SolutionAssemblyInfo.cs
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Shell", "Plugins\Flow.Launcher.Plugin.Shell\Flow.Launcher.Plugin.Shell.csproj", "{C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}"
@@ -71,6 +71,18 @@ 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("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Flow.Launcher.Localization", "Flow.Launcher.Localization", "{966EEB8B-5109-1020-D93D-9CB99693248E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flow.Launcher.Localization", "Flow.Launcher.Localization\Flow.Launcher.Localization\Flow.Launcher.Localization.csproj", "{7D495A1A-D861-2434-9A9E-2147A71EF8FD}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flow.Launcher.Localization.Analyzers", "Flow.Launcher.Localization\Flow.Launcher.Localization.Analyzers\Flow.Launcher.Localization.Analyzers.csproj", "{FD6DA12A-F468-559A-D973-F2A20494028B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flow.Launcher.Localization.Attributes", "Flow.Launcher.Localization\Flow.Launcher.Localization.Attributes\Flow.Launcher.Localization.Attributes.csproj", "{5F306065-2D83-4FBE-2DDD-3BAF37B77BF3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flow.Launcher.Localization.Shared", "Flow.Launcher.Localization\Flow.Launcher.Localization.Shared\Flow.Launcher.Localization.Shared.csproj", "{A1A23E38-C825-0C1C-7919-E780289F927F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flow.Launcher.Localization.SourceGenerators", "Flow.Launcher.Localization\Flow.Launcher.Localization.SourceGenerators\Flow.Launcher.Localization.SourceGenerators.csproj", "{E846BB23-171C-FAD8-8774-437821CD6100}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -81,8 +93,20 @@ Global
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Debug|x64.Build.0 = Debug|Any CPU
+ {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Debug|x86.Build.0 = Debug|Any CPU
+ {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Release|x64.ActiveCfg = Release|Any CPU
+ {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Release|x64.Build.0 = Release|Any CPU
+ {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Release|x86.ActiveCfg = Release|Any CPU
+ {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Release|x86.Build.0 = Release|Any CPU
{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
@@ -105,18 +129,6 @@ Global
{8451ECDD-2EA4-4966-BB0A-7BBC40138E80}.Release|x64.Build.0 = Release|Any CPU
{8451ECDD-2EA4-4966-BB0A-7BBC40138E80}.Release|x86.ActiveCfg = Release|Any CPU
{8451ECDD-2EA4-4966-BB0A-7BBC40138E80}.Release|x86.Build.0 = Release|Any CPU
- {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Debug|x64.ActiveCfg = Debug|Any CPU
- {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Debug|x64.Build.0 = Debug|Any CPU
- {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Debug|x86.ActiveCfg = Debug|Any CPU
- {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Debug|x86.Build.0 = Debug|Any CPU
- {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Release|Any CPU.Build.0 = Release|Any CPU
- {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Release|x64.ActiveCfg = Release|Any CPU
- {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Release|x64.Build.0 = Release|Any CPU
- {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Release|x86.ActiveCfg = Release|Any CPU
- {DB90F671-D861-46BB-93A3-F1304F5BA1C5}.Release|x86.Build.0 = Release|Any CPU
{4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -286,6 +298,66 @@ 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
+ {7D495A1A-D861-2434-9A9E-2147A71EF8FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7D495A1A-D861-2434-9A9E-2147A71EF8FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7D495A1A-D861-2434-9A9E-2147A71EF8FD}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7D495A1A-D861-2434-9A9E-2147A71EF8FD}.Debug|x64.Build.0 = Debug|Any CPU
+ {7D495A1A-D861-2434-9A9E-2147A71EF8FD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7D495A1A-D861-2434-9A9E-2147A71EF8FD}.Debug|x86.Build.0 = Debug|Any CPU
+ {7D495A1A-D861-2434-9A9E-2147A71EF8FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7D495A1A-D861-2434-9A9E-2147A71EF8FD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7D495A1A-D861-2434-9A9E-2147A71EF8FD}.Release|x64.ActiveCfg = Release|Any CPU
+ {7D495A1A-D861-2434-9A9E-2147A71EF8FD}.Release|x64.Build.0 = Release|Any CPU
+ {7D495A1A-D861-2434-9A9E-2147A71EF8FD}.Release|x86.ActiveCfg = Release|Any CPU
+ {7D495A1A-D861-2434-9A9E-2147A71EF8FD}.Release|x86.Build.0 = Release|Any CPU
+ {FD6DA12A-F468-559A-D973-F2A20494028B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FD6DA12A-F468-559A-D973-F2A20494028B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FD6DA12A-F468-559A-D973-F2A20494028B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {FD6DA12A-F468-559A-D973-F2A20494028B}.Debug|x64.Build.0 = Debug|Any CPU
+ {FD6DA12A-F468-559A-D973-F2A20494028B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {FD6DA12A-F468-559A-D973-F2A20494028B}.Debug|x86.Build.0 = Debug|Any CPU
+ {FD6DA12A-F468-559A-D973-F2A20494028B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FD6DA12A-F468-559A-D973-F2A20494028B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FD6DA12A-F468-559A-D973-F2A20494028B}.Release|x64.ActiveCfg = Release|Any CPU
+ {FD6DA12A-F468-559A-D973-F2A20494028B}.Release|x64.Build.0 = Release|Any CPU
+ {FD6DA12A-F468-559A-D973-F2A20494028B}.Release|x86.ActiveCfg = Release|Any CPU
+ {FD6DA12A-F468-559A-D973-F2A20494028B}.Release|x86.Build.0 = Release|Any CPU
+ {5F306065-2D83-4FBE-2DDD-3BAF37B77BF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5F306065-2D83-4FBE-2DDD-3BAF37B77BF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5F306065-2D83-4FBE-2DDD-3BAF37B77BF3}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5F306065-2D83-4FBE-2DDD-3BAF37B77BF3}.Debug|x64.Build.0 = Debug|Any CPU
+ {5F306065-2D83-4FBE-2DDD-3BAF37B77BF3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5F306065-2D83-4FBE-2DDD-3BAF37B77BF3}.Debug|x86.Build.0 = Debug|Any CPU
+ {5F306065-2D83-4FBE-2DDD-3BAF37B77BF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5F306065-2D83-4FBE-2DDD-3BAF37B77BF3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5F306065-2D83-4FBE-2DDD-3BAF37B77BF3}.Release|x64.ActiveCfg = Release|Any CPU
+ {5F306065-2D83-4FBE-2DDD-3BAF37B77BF3}.Release|x64.Build.0 = Release|Any CPU
+ {5F306065-2D83-4FBE-2DDD-3BAF37B77BF3}.Release|x86.ActiveCfg = Release|Any CPU
+ {5F306065-2D83-4FBE-2DDD-3BAF37B77BF3}.Release|x86.Build.0 = Release|Any CPU
+ {A1A23E38-C825-0C1C-7919-E780289F927F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A1A23E38-C825-0C1C-7919-E780289F927F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A1A23E38-C825-0C1C-7919-E780289F927F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A1A23E38-C825-0C1C-7919-E780289F927F}.Debug|x64.Build.0 = Debug|Any CPU
+ {A1A23E38-C825-0C1C-7919-E780289F927F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A1A23E38-C825-0C1C-7919-E780289F927F}.Debug|x86.Build.0 = Debug|Any CPU
+ {A1A23E38-C825-0C1C-7919-E780289F927F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A1A23E38-C825-0C1C-7919-E780289F927F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A1A23E38-C825-0C1C-7919-E780289F927F}.Release|x64.ActiveCfg = Release|Any CPU
+ {A1A23E38-C825-0C1C-7919-E780289F927F}.Release|x64.Build.0 = Release|Any CPU
+ {A1A23E38-C825-0C1C-7919-E780289F927F}.Release|x86.ActiveCfg = Release|Any CPU
+ {A1A23E38-C825-0C1C-7919-E780289F927F}.Release|x86.Build.0 = Release|Any CPU
+ {E846BB23-171C-FAD8-8774-437821CD6100}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E846BB23-171C-FAD8-8774-437821CD6100}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E846BB23-171C-FAD8-8774-437821CD6100}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E846BB23-171C-FAD8-8774-437821CD6100}.Debug|x64.Build.0 = Debug|Any CPU
+ {E846BB23-171C-FAD8-8774-437821CD6100}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E846BB23-171C-FAD8-8774-437821CD6100}.Debug|x86.Build.0 = Debug|Any CPU
+ {E846BB23-171C-FAD8-8774-437821CD6100}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E846BB23-171C-FAD8-8774-437821CD6100}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E846BB23-171C-FAD8-8774-437821CD6100}.Release|x64.ActiveCfg = Release|Any CPU
+ {E846BB23-171C-FAD8-8774-437821CD6100}.Release|x64.Build.0 = Release|Any CPU
+ {E846BB23-171C-FAD8-8774-437821CD6100}.Release|x86.ActiveCfg = Release|Any CPU
+ {E846BB23-171C-FAD8-8774-437821CD6100}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -303,6 +375,11 @@ Global
{588088F4-3262-4F9F-9663-A05DE12534C3} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87}
{4792A74A-0CEA-4173-A8B2-30E6764C6217} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87}
{5043CECE-E6A7-4867-9CBE-02D27D83747A} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87}
+ {7D495A1A-D861-2434-9A9E-2147A71EF8FD} = {966EEB8B-5109-1020-D93D-9CB99693248E}
+ {FD6DA12A-F468-559A-D973-F2A20494028B} = {966EEB8B-5109-1020-D93D-9CB99693248E}
+ {5F306065-2D83-4FBE-2DDD-3BAF37B77BF3} = {966EEB8B-5109-1020-D93D-9CB99693248E}
+ {A1A23E38-C825-0C1C-7919-E780289F927F} = {966EEB8B-5109-1020-D93D-9CB99693248E}
+ {E846BB23-171C-FAD8-8774-437821CD6100} = {966EEB8B-5109-1020-D93D-9CB99693248E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F26ACB50-3F6C-4907-B0C9-1ADACC1D0DED}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj
index b444fb8f655..81e1e648327 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj
@@ -53,7 +53,16 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs
index fbaefa9d66e..6aa20186039 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs
@@ -1,4 +1,4 @@
-using Flow.Launcher.Plugin.Explorer.Helper;
+using Flow.Launcher.Plugin.Explorer.Helper;
using Flow.Launcher.Plugin.Explorer.Search;
using Flow.Launcher.Plugin.Explorer.Search.Everything;
using Flow.Launcher.Plugin.Explorer.ViewModels;
@@ -92,12 +92,12 @@ public async Task> QueryAsync(Query query, CancellationToken token)
public string GetTranslatedPluginTitle()
{
- return Context.API.GetTranslation("plugin_explorer_plugin_name");
+ return Localize.plugin_explorer_plugin_name();
}
public string GetTranslatedPluginDescription()
{
- return Context.API.GetTranslation("plugin_explorer_plugin_description");
+ return Localize.plugin_explorer_plugin_description();
}
private static void FillQuickAccessLinkNames()