mirror of
https://github.com/godotengine/godot.git
synced 2025-10-19 07:53:26 +00:00
feat: Add c# translation parser support
Use semantic model to analyze the method to be selected Support translation comment Make C# multi-line comments also ignore translation Add preprocessor symbols support
This commit is contained in:
parent
71a9948157
commit
da8f647fa1
3 changed files with 450 additions and 0 deletions
|
@ -0,0 +1,446 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
using GodotTools.Internals;
|
||||
using Microsoft.Build.Evaluation;
|
||||
using Microsoft.Build.Execution;
|
||||
using Microsoft.Build.Locator;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace GodotTools;
|
||||
|
||||
public partial class CsTranslationParserPlugin : EditorTranslationParserPlugin
|
||||
{
|
||||
|
||||
private class CommentData
|
||||
{
|
||||
public string Comment = "";
|
||||
public int StartLine;
|
||||
public int EndLine;
|
||||
public bool Newline = true;
|
||||
}
|
||||
|
||||
private List<MetadataReference>? _projectReferences;
|
||||
private Array<string[]> _ret = new Array<string[]>();
|
||||
private List<SyntaxTree> _syntaxTreeCaches = new List<SyntaxTree>();
|
||||
|
||||
private const string TranslationCommentPrefix = "TRANSLATORS:";
|
||||
private const string NoTranslateComment = "NO_TRANSLATE";
|
||||
private const string TranslationStaticClass = "Godot.TranslationServer";
|
||||
private const string TranslationMethod = "Translate";
|
||||
private const string TranslationPluralMethod = "TranslatePlural";
|
||||
private const string TranslationClass = "Godot.GodotObject";
|
||||
private const string TranslationMethodTr = "Tr";
|
||||
private const string TranslationMethodTrN = "TrN";
|
||||
private static readonly string[] _configurations = ["Debug", "Release"];
|
||||
private static readonly string[] _targetPlatforms = ["windows", "linuxbsd", "macos", "android", "ios", "web"];
|
||||
|
||||
public override string[] _GetRecognizedExtensions()
|
||||
{
|
||||
return ["cs"];
|
||||
}
|
||||
|
||||
public override Array<string[]> _ParseFile(string path)
|
||||
{
|
||||
_ret = [];
|
||||
|
||||
if (_projectReferences == null)
|
||||
{
|
||||
_projectReferences = new List<MetadataReference>();
|
||||
foreach (string configuration in _configurations)
|
||||
{
|
||||
foreach (string targetPlatform in _targetPlatforms)
|
||||
{
|
||||
GetProjectReferences(GodotSharpDirs.ProjectCsProjPath, configuration, targetPlatform).ForEach(reference =>
|
||||
{
|
||||
if (!_projectReferences.Contains(reference))
|
||||
{
|
||||
_projectReferences.Add(reference);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
System.AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(a => !a.IsDynamic)
|
||||
.Where(a => a.Location != "")
|
||||
.Select(a => MetadataReference.CreateFromFile(a.Location))
|
||||
.Cast<MetadataReference>()
|
||||
.ToList()
|
||||
.ForEach(reference =>
|
||||
{
|
||||
if (!_projectReferences.Contains(reference))
|
||||
{
|
||||
_projectReferences.Add(reference);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var res = ResourceLoader.Load<CSharpScript>(path, "Script");
|
||||
var text = res.SourceCode;
|
||||
|
||||
foreach (string configuration in _configurations)
|
||||
{
|
||||
foreach (string targetPlatform in _targetPlatforms)
|
||||
{
|
||||
var symbols = GetProjectDefineConstants(GodotSharpDirs.ProjectCsProjPath, configuration, targetPlatform);
|
||||
ParseCode(text, symbols, _projectReferences);
|
||||
}
|
||||
}
|
||||
_syntaxTreeCaches.Clear();
|
||||
return _ret;
|
||||
}
|
||||
|
||||
private void ParseCode(string code, string[] symbols, List<MetadataReference> references)
|
||||
{
|
||||
var options = new CSharpParseOptions(LanguageVersion.Default, DocumentationMode.Parse, SourceCodeKind.Script, symbols);
|
||||
var tree = CSharpSyntaxTree.ParseText(code, options);
|
||||
if (SyntaxTreeContains(tree) || tree == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_syntaxTreeCaches.Add(tree);
|
||||
var compilation = CSharpCompilation.Create("TranslationParser", options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
|
||||
.AddReferences(references)
|
||||
.AddSyntaxTrees(tree);
|
||||
|
||||
var semanticModel = compilation.GetSemanticModel(tree);
|
||||
var comments = tree.GetRoot().DescendantNodes()
|
||||
.SelectMany(
|
||||
node => node.GetTrailingTrivia()
|
||||
.Where(trivia => trivia.IsKind(SyntaxKind.SingleLineCommentTrivia))
|
||||
.Concat(node.GetLeadingTrivia().Where(trivia => trivia.IsKind(SyntaxKind.SingleLineCommentTrivia)
|
||||
|| trivia.IsKind(SyntaxKind.MultiLineCommentTrivia))))
|
||||
.Select(trivia => new CommentData
|
||||
{
|
||||
Comment = trivia.ToFullString(),
|
||||
StartLine = GetStartLine(trivia.GetLocation()),
|
||||
EndLine = GetEndLine(trivia.GetLocation()),
|
||||
Newline = tree.GetRoot().DescendantNodes()
|
||||
.FirstOrDefault(node => GetStartLine(node.GetLocation()) == GetStartLine(trivia.GetLocation())) == null
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
foreach (var syntaxNode in tree.GetRoot().DescendantNodes().Where(node => node is InvocationExpressionSyntax))
|
||||
{
|
||||
var invocation = (InvocationExpressionSyntax)syntaxNode;
|
||||
var commentText = "";
|
||||
var skip = false;
|
||||
// Parse inline comment
|
||||
var line = GetStartLine(syntaxNode.GetLocation());
|
||||
|
||||
var commentData = comments.FirstOrDefault(comment => comment.StartLine == line);
|
||||
if (commentData != null)
|
||||
{
|
||||
commentText = commentData.Comment.TrimStart('/').Trim();
|
||||
if (commentText.StartsWith(TranslationCommentPrefix))
|
||||
{
|
||||
commentText = commentText.TrimPrefix(TranslationCommentPrefix).Trim();
|
||||
}
|
||||
else if (commentText == NoTranslateComment || commentText.StartsWith(NoTranslateComment + ":"))
|
||||
{
|
||||
skip = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Parse multiline comment
|
||||
for (var index = line - 1; index >= 0; index--)
|
||||
{
|
||||
var multilineCommentData =
|
||||
comments.FirstOrDefault(comment => comment.EndLine == index && comment.Newline);
|
||||
if (multilineCommentData == null)
|
||||
{
|
||||
commentText = "";
|
||||
break;
|
||||
}
|
||||
// multiline comment
|
||||
if (multilineCommentData.StartLine != multilineCommentData.EndLine)
|
||||
{
|
||||
var multilineComments = multilineCommentData.Comment.TrimSuffix("*/").Trim().Split("\n")
|
||||
.Select(lineStr => lineStr.TrimPrefix("/*").Trim().TrimPrefix(TranslationCommentPrefix));
|
||||
commentText = string.Join("\n", multilineComments);
|
||||
if (commentText == NoTranslateComment || commentText.StartsWith(NoTranslateComment + ":"))
|
||||
{
|
||||
commentText = "";
|
||||
skip = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// multiline single line comment
|
||||
var currentComment = multilineCommentData.Comment.TrimStart('/').Trim();
|
||||
if (currentComment == "")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (commentText == "")
|
||||
{
|
||||
commentText = currentComment;
|
||||
}
|
||||
else
|
||||
{
|
||||
commentText = currentComment + "\n" + commentText;
|
||||
}
|
||||
if (currentComment.StartsWith(TranslationCommentPrefix))
|
||||
{
|
||||
commentText = commentText.TrimPrefix(TranslationCommentPrefix).Trim();
|
||||
break;
|
||||
}
|
||||
if (currentComment == NoTranslateComment || currentComment.StartsWith(NoTranslateComment + ":"))
|
||||
{
|
||||
commentText = "";
|
||||
skip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SymbolInfo? symbolInfo = null;
|
||||
if (invocation.Expression is IdentifierNameSyntax identifierNameSyntax)
|
||||
{
|
||||
symbolInfo = semanticModel.GetSymbolInfo(identifierNameSyntax);
|
||||
}
|
||||
if (invocation.Expression is MemberAccessExpressionSyntax { Name: IdentifierNameSyntax nameSyntax })
|
||||
{
|
||||
symbolInfo = semanticModel.GetSymbolInfo(nameSyntax);
|
||||
}
|
||||
|
||||
var methodSymbol = symbolInfo?.Symbol as IMethodSymbol;
|
||||
if (methodSymbol == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (methodSymbol.Name == TranslationMethod &&
|
||||
methodSymbol.ContainingType.ToDisplayString() == TranslationStaticClass)
|
||||
{
|
||||
if (skip)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
AddMsg(invocation.ArgumentList.Arguments, semanticModel, commentText);
|
||||
}
|
||||
|
||||
if (methodSymbol.Name == TranslationPluralMethod &&
|
||||
methodSymbol.ContainingType.ToDisplayString() == TranslationStaticClass)
|
||||
{
|
||||
if (skip)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
AddPluralMsg(invocation.ArgumentList.Arguments, semanticModel, commentText);
|
||||
}
|
||||
|
||||
if (methodSymbol.Name is TranslationMethodTr or TranslationMethodTrN
|
||||
&& methodSymbol.MethodKind == MethodKind.Ordinary)
|
||||
{
|
||||
var receiverType = methodSymbol.ReceiverType ?? methodSymbol.ContainingType;
|
||||
|
||||
if (receiverType != null && InheritsFromGodotObject(receiverType))
|
||||
{
|
||||
if (skip)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (methodSymbol.Name == TranslationMethodTr)
|
||||
{
|
||||
AddMsg(invocation.ArgumentList.Arguments, semanticModel, commentText);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddPluralMsg(invocation.ArgumentList.Arguments, semanticModel, commentText);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool SyntaxTreeContains(SyntaxTree otherTree)
|
||||
{
|
||||
return _syntaxTreeCaches.Any(syntaxTree => syntaxTree.GetRoot().IsEquivalentTo(otherTree.GetRoot()));
|
||||
}
|
||||
private int GetStartLine(Location location)
|
||||
{
|
||||
return location.GetLineSpan().StartLinePosition.Line;
|
||||
}
|
||||
|
||||
private int GetEndLine(Location location)
|
||||
{
|
||||
return location.GetLineSpan().EndLinePosition.Line;
|
||||
}
|
||||
|
||||
private bool InheritsFromGodotObject(ITypeSymbol typeSymbol)
|
||||
{
|
||||
while (typeSymbol != null)
|
||||
{
|
||||
if (typeSymbol.ToDisplayString() == TranslationClass)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
#pragma warning disable CS8600
|
||||
typeSymbol = typeSymbol.BaseType;
|
||||
#pragma warning restore CS8600
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void AddMsg(SeparatedSyntaxList<ArgumentSyntax> arguments, SemanticModel semanticModel, string comment)
|
||||
{
|
||||
switch (arguments.Count)
|
||||
{
|
||||
case 1:
|
||||
{
|
||||
var argExpr = arguments[0].Expression;
|
||||
var constantValue = semanticModel.GetConstantValue(argExpr);
|
||||
|
||||
if (constantValue is { HasValue: true, Value: string message })
|
||||
{
|
||||
_ret.Add([message, "", "", comment]);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
{
|
||||
var msgExpr = arguments[0].Expression;
|
||||
var ctxExpr = arguments[1].Expression;
|
||||
|
||||
var msgValue = semanticModel.GetConstantValue(msgExpr);
|
||||
var ctxValue = semanticModel.GetConstantValue(ctxExpr);
|
||||
|
||||
if (msgValue is { HasValue: true, Value: string message } &&
|
||||
ctxValue is { HasValue: true, Value: string context })
|
||||
{
|
||||
_ret.Add([message, context, "", comment]);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddPluralMsg(SeparatedSyntaxList<ArgumentSyntax> arguments, SemanticModel semanticModel, string comment)
|
||||
{
|
||||
var singularExpr = arguments[0].Expression;
|
||||
var pluralExpr = arguments[1].Expression;
|
||||
|
||||
var singularValue = semanticModel.GetConstantValue(singularExpr);
|
||||
var pluralValue = semanticModel.GetConstantValue(pluralExpr);
|
||||
|
||||
if (!singularValue.HasValue || singularValue.Value is not string singular ||
|
||||
!pluralValue.HasValue || pluralValue.Value is not string plural)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var context = "";
|
||||
if (arguments.Count == 4)
|
||||
{
|
||||
var ctxExpr = arguments[3].Expression;
|
||||
var ctxValue = semanticModel.GetConstantValue(ctxExpr);
|
||||
if (ctxValue is { HasValue: true, Value: string ctx })
|
||||
{
|
||||
context = ctx;
|
||||
}
|
||||
}
|
||||
_ret.Add([singular, context, plural, comment]);
|
||||
}
|
||||
|
||||
private List<MetadataReference> GetProjectReferences(string projectPath, string configuration = "Debug", string? targetPlatform = null)
|
||||
{
|
||||
if (!MSBuildLocator.IsRegistered)
|
||||
{
|
||||
MSBuildLocator.RegisterDefaults();
|
||||
}
|
||||
|
||||
var referencePaths = GetProjectReferencePaths(projectPath, configuration, targetPlatform ?? OS.GetName());
|
||||
|
||||
var metadataReferences = new List<MetadataReference>();
|
||||
foreach (var dllPath in referencePaths)
|
||||
{
|
||||
if (File.Exists(dllPath))
|
||||
{
|
||||
var metadataReference = MetadataReference.CreateFromFile(dllPath);
|
||||
metadataReferences.Add(metadataReference);
|
||||
}
|
||||
}
|
||||
|
||||
return metadataReferences;
|
||||
}
|
||||
|
||||
private List<string> GetProjectReferencePaths(string projectPath, string configuration, string targetPlatform)
|
||||
{
|
||||
var referencePaths = new List<string>();
|
||||
|
||||
var projectCollection = new ProjectCollection();
|
||||
var project = projectCollection.LoadProject(projectPath);
|
||||
|
||||
project.SetProperty("Configuration", configuration);
|
||||
project.SetProperty("Platform", "Any CPU");
|
||||
project.SetProperty("GodotTargetPlatform", targetPlatform);
|
||||
|
||||
var buildParameters = new BuildParameters(projectCollection);
|
||||
var buildRequest = new BuildRequestData(project.FullPath, project.GlobalProperties, null, ["GetTargetPath"], null);
|
||||
var buildResult = BuildManager.DefaultBuildManager.Build(buildParameters, buildRequest);
|
||||
|
||||
if (buildResult.OverallResult == BuildResultCode.Success)
|
||||
{
|
||||
referencePaths.AddRange(buildResult.ResultsByTarget["GetTargetPath"].Items.Select(item => item.ItemSpec));
|
||||
}
|
||||
|
||||
projectCollection.UnloadAllProjects();
|
||||
projectCollection.Dispose();
|
||||
|
||||
return referencePaths;
|
||||
}
|
||||
|
||||
private string[] GetProjectDefineConstants(string projectPath, string configuration = "Debug", string? targetPlatform = null)
|
||||
{
|
||||
if (!MSBuildLocator.IsRegistered)
|
||||
{
|
||||
MSBuildLocator.RegisterDefaults();
|
||||
}
|
||||
string[] defineConstants = [];
|
||||
|
||||
var projectCollection = new ProjectCollection();
|
||||
var project = projectCollection.LoadProject(projectPath);
|
||||
|
||||
project.SetProperty("Configuration", configuration);
|
||||
project.SetProperty("Platform", "Any CPU");
|
||||
project.SetProperty("GodotTargetPlatform", targetPlatform ?? OS.GetName());
|
||||
|
||||
var target = project.Xml.AddTarget("GetDefineConstants");
|
||||
var propertyGroup = target.AddPropertyGroup();
|
||||
propertyGroup.AddProperty("DefineConstantsValue", "$(DefineConstants)");
|
||||
var itemGroup = target.AddItemGroup();
|
||||
itemGroup.AddItem("DefineConstantsItem", "$(DefineConstantsValue)");
|
||||
var task = target.AddTask("WriteLinesToFile");
|
||||
var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||
task.SetParameter("File", tempFilePath);
|
||||
task.SetParameter("Lines", "@(DefineConstantsItem)");
|
||||
task.SetParameter("Overwrite", "true");
|
||||
|
||||
var buildParameters = new BuildParameters(projectCollection);
|
||||
var buildRequest = new BuildRequestData(project.FullPath, project.GlobalProperties, null, ["GetDefineConstants"], null);
|
||||
var buildResult = BuildManager.DefaultBuildManager.Build(buildParameters, buildRequest);
|
||||
|
||||
if (buildResult.OverallResult == BuildResultCode.Success)
|
||||
{
|
||||
var defineConstantsOutput = File.ReadAllText(tempFilePath);
|
||||
if (string.IsNullOrEmpty(defineConstantsOutput))
|
||||
{
|
||||
defineConstants = defineConstantsOutput.Split('\n')
|
||||
.Select(symbol => symbol.Trim('\r').Trim('\n'))
|
||||
.Where(defineConstant => defineConstant != "").ToArray();
|
||||
}
|
||||
File.Delete(tempFilePath);
|
||||
}
|
||||
|
||||
projectCollection.UnloadAllProjects();
|
||||
projectCollection.Dispose();
|
||||
|
||||
return defineConstants;
|
||||
}
|
||||
}
|
|
@ -643,6 +643,9 @@ namespace GodotTools
|
|||
AddInspectorPlugin(inspectorPlugin);
|
||||
_inspectorPluginWeak = WeakRef(inspectorPlugin);
|
||||
|
||||
// TranslationParser Plugin
|
||||
AddTranslationParserPlugin(new CsTranslationParserPlugin());
|
||||
|
||||
BuildManager.Initialize();
|
||||
|
||||
GodotIdeManager = new GodotIdeManager();
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3.0" ExcludeAssets="runtime" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
|
||||
<PackageReference Include="JetBrains.Rider.PathLocator" Version="1.0.12" />
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue