Replace T4 with Source Generator (#459)
* Replace T4 template with source generator * remove space * Added summary --------- Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
This commit is contained in:
3091
Wino.Core.Domain/Translator.Designer.cs
generated
3091
Wino.Core.Domain/Translator.Designer.cs
generated
File diff suppressed because it is too large
Load Diff
10
Wino.Core.Domain/Translator.cs
Normal file
10
Wino.Core.Domain/Translator.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using Wino.Core.SourceGeneration.Translator;
|
||||||
|
|
||||||
|
namespace Wino.Core.Domain;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Translator class for translation.
|
||||||
|
/// All translations generated automatically by the source generator.
|
||||||
|
/// </summary>
|
||||||
|
[TranslatorGen]
|
||||||
|
public partial class Translator;
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
<#@ template debug="true" hostspecific="true" language="C#" #>
|
|
||||||
<#@ assembly name="System.Core" #>
|
|
||||||
<#@ assembly name="System.Text.Json" #>
|
|
||||||
<#@ assembly name="System.Memory" #>
|
|
||||||
<#@ assembly name="System" #>
|
|
||||||
<#@ import namespace="System.Text.Json" #>
|
|
||||||
<#@ import namespace="System" #>
|
|
||||||
<#@ assembly name="NetStandard" #>
|
|
||||||
<#@ import namespace="System.Linq" #>
|
|
||||||
<#@ import namespace="System.Text" #>
|
|
||||||
<#@ import namespace="System.Collections.Generic" #>
|
|
||||||
<#@ import namespace="System.IO" #>
|
|
||||||
<#@ output extension="Designer.cs" #>
|
|
||||||
<# string filename = this.Host.ResolvePath("Translations/en_US/resources.json");
|
|
||||||
var allText = File.ReadAllText(filename);
|
|
||||||
var resourceKeys = JsonSerializer.Deserialize<Dictionary<string, string>>(allText);
|
|
||||||
#>
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain
|
|
||||||
{
|
|
||||||
public class Translator
|
|
||||||
{
|
|
||||||
private static global::Wino.Core.Domain.Translations.WinoTranslationDictionary _dictionary;
|
|
||||||
|
|
||||||
public static global::Wino.Core.Domain.Translations.WinoTranslationDictionary Resources
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_dictionary == null)
|
|
||||||
{
|
|
||||||
_dictionary = new global::Wino.Core.Domain.Translations.WinoTranslationDictionary();
|
|
||||||
}
|
|
||||||
|
|
||||||
return _dictionary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
<#
|
|
||||||
|
|
||||||
string[] escapeChars = new string[] { " ", ";", "@", "$", "&", "(",")","-","#",":","!","'","?","{","}","," };
|
|
||||||
|
|
||||||
foreach (var key in resourceKeys)
|
|
||||||
{
|
|
||||||
// Generate proper allowed variable name by C#
|
|
||||||
var allowedPropertyName = escapeChars.Aggregate(key.Key, (c1, c2) => c1.Replace(c2, string.Empty));
|
|
||||||
|
|
||||||
// There might be null values for some keys. Those will display as (null string) in the Comment;
|
|
||||||
// The actual translation for the key will be the key itself at runtime.
|
|
||||||
var beautifiedValue = key.Value == null ? "(null string)" : key.Value;
|
|
||||||
|
|
||||||
// We need to trim the line ending literals for comments.
|
|
||||||
var beautifiedComment = beautifiedValue.Replace('\r',' ').Replace('\n',' ');
|
|
||||||
#>
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <#= beautifiedComment #>
|
|
||||||
/// </summary>
|
|
||||||
public static string <#= allowedPropertyName #> => Resources.GetTranslatedString(@"<#= key.Key #>");
|
|
||||||
<# } #>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -70,17 +70,14 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
|
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Update="Translator.Designer.cs">
|
<AdditionalFiles Include="Translations\en_US\resources.json" />
|
||||||
<DesignTime>True</DesignTime>
|
|
||||||
<AutoGen>True</AutoGen>
|
|
||||||
<DependentUpon>Translator.tt</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="Translator.tt">
|
<!-- Source Generators -->
|
||||||
<Generator>TextTemplatingFileGenerator</Generator>
|
<ProjectReference Include="..\Wino.SourceGenerators\Wino.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||||
<LastGenOutput>Translator.Designer.cs</LastGenOutput>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
165
Wino.SourceGenerators/Translator/TranslatorGenerator.cs
Normal file
165
Wino.SourceGenerators/Translator/TranslatorGenerator.cs
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Text;
|
||||||
|
|
||||||
|
namespace Wino.SourceGenerators.Translator;
|
||||||
|
|
||||||
|
[Generator]
|
||||||
|
public class TranslatorSourceGenerator : IIncrementalGenerator
|
||||||
|
{
|
||||||
|
private const string TranslatorAttributeName = "Wino.Core.SourceGeneration.Translator.TranslatorGenAttribute";
|
||||||
|
|
||||||
|
private const string AttributeText =
|
||||||
|
"""
|
||||||
|
using System;
|
||||||
|
namespace Wino.Core.SourceGeneration.Translator
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||||
|
public class TranslatorGenAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||||
|
{
|
||||||
|
// Register the attribute source
|
||||||
|
context.RegisterPostInitializationOutput(ctx => ctx.AddSource(
|
||||||
|
"TranslatorGenAttribute.g.cs",
|
||||||
|
SourceText.From(AttributeText, Encoding.UTF8)));
|
||||||
|
|
||||||
|
// Get all classes with the TranslatorGenAttribute
|
||||||
|
var classDeclarations = context.SyntaxProvider
|
||||||
|
.ForAttributeWithMetadataName(
|
||||||
|
TranslatorAttributeName,
|
||||||
|
predicate: static (node, _) => node is ClassDeclarationSyntax,
|
||||||
|
transform: static (context, _) => (ClassDeclarationSyntax)context.TargetNode);
|
||||||
|
|
||||||
|
// Get the JSON schema
|
||||||
|
var jsonSchema = context.AdditionalTextsProvider
|
||||||
|
.Where(static file => file.Path.EndsWith("en_US\\resources.json"))
|
||||||
|
.Select((text, _) => (text, text.GetText()))
|
||||||
|
.Collect();
|
||||||
|
|
||||||
|
// Combine the JSON schema with the marked classes
|
||||||
|
var combined = classDeclarations.Combine(jsonSchema);
|
||||||
|
|
||||||
|
// Generate the source
|
||||||
|
context.RegisterSourceOutput(combined,
|
||||||
|
static (spc, source) => Execute(source.Left, source.Right, spc));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Execute(
|
||||||
|
ClassDeclarationSyntax classDeclaration,
|
||||||
|
ImmutableArray<(AdditionalText, SourceText?)> jsonFiles,
|
||||||
|
SourceProductionContext context)
|
||||||
|
{
|
||||||
|
var (_, jsonContent) = jsonFiles.FirstOrDefault();
|
||||||
|
if (jsonContent == null) return;
|
||||||
|
|
||||||
|
// Parse JSON
|
||||||
|
var jsonString = jsonContent.ToString();
|
||||||
|
var translations = JsonSerializer.Deserialize<Dictionary<string, string>>(jsonString);
|
||||||
|
if (translations == null) return;
|
||||||
|
|
||||||
|
// Generate the class
|
||||||
|
var namespaceName = GetNamespaceName(classDeclaration);
|
||||||
|
var className = classDeclaration.Identifier.Text;
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine($$"""
|
||||||
|
namespace {{namespaceName}}
|
||||||
|
{
|
||||||
|
public partial class {{className}}
|
||||||
|
{
|
||||||
|
private static global::Wino.Core.Domain.Translations.WinoTranslationDictionary _dictionary;
|
||||||
|
|
||||||
|
public static global::Wino.Core.Domain.Translations.WinoTranslationDictionary Resources
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_dictionary == null)
|
||||||
|
{
|
||||||
|
_dictionary = new global::Wino.Core.Domain.Translations.WinoTranslationDictionary();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _dictionary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
string[] escapeChars = [" ", ";", "@", "$", "&", "(", ")", "-", "#", ":", "!", "'", "?", "{", "}", ","];
|
||||||
|
|
||||||
|
foreach (var translation in translations)
|
||||||
|
{
|
||||||
|
// Generate proper allowed variable name by C#
|
||||||
|
var allowedPropertyName = escapeChars.Aggregate(translation.Key, (c1, c2) => c1.Replace(c2, string.Empty));
|
||||||
|
|
||||||
|
// There might be null values for some keys. Those will display as (null string) in the Comment;
|
||||||
|
// The actual translation for the key will be the key itself at runtime.
|
||||||
|
var beautifiedValue = translation.Value ?? "(null string)";
|
||||||
|
|
||||||
|
// We need to trim the line ending literals for comments.
|
||||||
|
var beautifiedComment = beautifiedValue.Replace('\r', ' ').Replace('\n', ' ');
|
||||||
|
AddKey(sb, allowedPropertyName, beautifiedComment);
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine($"{Spacing(1)}}}");
|
||||||
|
sb.AppendLine("}");
|
||||||
|
|
||||||
|
context.AddSource($"{className}.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddKey(StringBuilder sb, string key, string? comment = null, int tabPos = 2)
|
||||||
|
{
|
||||||
|
var tabString = Spacing(tabPos);
|
||||||
|
_ = sb.AppendLine();
|
||||||
|
_ = sb.AppendLine($"{tabString}/// <summary>");
|
||||||
|
_ = sb.AppendLine($"{tabString}/// {comment}");
|
||||||
|
_ = sb.AppendLine($"{tabString}/// </summary>");
|
||||||
|
_ = sb.AppendLine($"{tabString}public static string {key} => Resources.GetTranslatedString(\"{key}\");");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// intent
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="n">tab</param>
|
||||||
|
/// <returns>4n*space</returns>
|
||||||
|
internal static string Spacing(int n)
|
||||||
|
{
|
||||||
|
Span<char> spaces = stackalloc char[n * 4];
|
||||||
|
spaces.Fill(' ');
|
||||||
|
|
||||||
|
var sb = new StringBuilder(n * 4);
|
||||||
|
foreach (var c in spaces)
|
||||||
|
_ = sb.Append(c);
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetNamespaceName(ClassDeclarationSyntax classDeclaration)
|
||||||
|
{
|
||||||
|
var namespaceName = string.Empty;
|
||||||
|
var potentialNamespaceParent = classDeclaration.Parent;
|
||||||
|
|
||||||
|
while (potentialNamespaceParent != null &&
|
||||||
|
potentialNamespaceParent is not NamespaceDeclarationSyntax &&
|
||||||
|
potentialNamespaceParent is not FileScopedNamespaceDeclarationSyntax)
|
||||||
|
{
|
||||||
|
potentialNamespaceParent = potentialNamespaceParent.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (potentialNamespaceParent is BaseNamespaceDeclarationSyntax namespaceParent)
|
||||||
|
{
|
||||||
|
namespaceName = namespaceParent.Name.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return namespaceName;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
Wino.SourceGenerators/Wino.SourceGenerators.csproj
Normal file
36
Wino.SourceGenerators/Wino.SourceGenerators.csproj
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
|
||||||
|
<DefineConstants>TRACE;DEBUG;NETFX_CORE</DefineConstants>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)' != 'Debug'">
|
||||||
|
<DefineConstants>TRACE;RELEASE;NETFX_CORE</DefineConstants>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="System.Text.Json" Version="8.0.5" PrivateAssets="all" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Target Name="GetDependencyTargetPaths" AfterTargets="ResolvePackageDependenciesForBuild">
|
||||||
|
<ItemGroup>
|
||||||
|
<TargetPathWithTargetPlatformMoniker Include="@(ResolvedCompileFileDefinitions)" IncludeRuntimeDependency="false" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
</Project>
|
||||||
18
Wino.sln
18
Wino.sln
@@ -10,6 +10,9 @@ EndProject
|
|||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Core.UWP", "Wino.Core.UWP\Wino.Core.UWP.csproj", "{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Core.UWP", "Wino.Core.UWP\Wino.Core.UWP.csproj", "{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Core.Domain", "Wino.Core.Domain\Wino.Core.Domain.csproj", "{CF3312E5-5DA0-4867-9945-49EA7598AF1F}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Core.Domain", "Wino.Core.Domain\Wino.Core.Domain.csproj", "{CF3312E5-5DA0-4867-9945-49EA7598AF1F}"
|
||||||
|
ProjectSection(ProjectDependencies) = postProject
|
||||||
|
{D4919A19-E70F-4916-83D2-5D5F87BEB949} = {D4919A19-E70F-4916-83D2-5D5F87BEB949}
|
||||||
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.BackgroundTasks", "Wino.BackgroundTasks\Wino.BackgroundTasks.csproj", "{D9EF0F59-F5F2-4D6C-A5BA-84043D8F3E08}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.BackgroundTasks", "Wino.BackgroundTasks\Wino.BackgroundTasks.csproj", "{D9EF0F59-F5F2-4D6C-A5BA-84043D8F3E08}"
|
||||||
EndProject
|
EndProject
|
||||||
@@ -23,6 +26,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Server", "Wino.Server\
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "lib", "lib", "{17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "lib", "lib", "{17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.SourceGenerators", "Wino.SourceGenerators\Wino.SourceGenerators.csproj", "{D4919A19-E70F-4916-83D2-5D5F87BEB949}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|ARM64 = Debug|ARM64
|
Debug|ARM64 = Debug|ARM64
|
||||||
@@ -153,6 +158,18 @@ Global
|
|||||||
{3D1942E5-1A3B-4062-B4BB-156A40DA47FE}.Release|x64.Build.0 = Release|x64
|
{3D1942E5-1A3B-4062-B4BB-156A40DA47FE}.Release|x64.Build.0 = Release|x64
|
||||||
{3D1942E5-1A3B-4062-B4BB-156A40DA47FE}.Release|x86.ActiveCfg = Release|x86
|
{3D1942E5-1A3B-4062-B4BB-156A40DA47FE}.Release|x86.ActiveCfg = Release|x86
|
||||||
{3D1942E5-1A3B-4062-B4BB-156A40DA47FE}.Release|x86.Build.0 = Release|x86
|
{3D1942E5-1A3B-4062-B4BB-156A40DA47FE}.Release|x86.Build.0 = Release|x86
|
||||||
|
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Debug|ARM64.ActiveCfg = Debug|Any CPU
|
||||||
|
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Debug|ARM64.Build.0 = Debug|Any CPU
|
||||||
|
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Release|ARM64.ActiveCfg = Release|Any CPU
|
||||||
|
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Release|ARM64.Build.0 = Release|Any CPU
|
||||||
|
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -164,6 +181,7 @@ Global
|
|||||||
{D9EF0F59-F5F2-4D6C-A5BA-84043D8F3E08} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
|
{D9EF0F59-F5F2-4D6C-A5BA-84043D8F3E08} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
|
||||||
{D62F1C03-DA57-4709-A640-0283296A8E66} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
|
{D62F1C03-DA57-4709-A640-0283296A8E66} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
|
||||||
{0C307D7E-256F-448C-8265-5622A812FBCC} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
|
{0C307D7E-256F-448C-8265-5622A812FBCC} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
|
||||||
|
{D4919A19-E70F-4916-83D2-5D5F87BEB949} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {721F946E-F69F-4987-823A-D084B436FC1E}
|
SolutionGuid = {721F946E-F69F-4987-823A-D084B436FC1E}
|
||||||
|
|||||||
Reference in New Issue
Block a user