Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Localize Reactor with fluent #65

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions Reactor.Benchmarks/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,28 @@
"resolved": "2.1.0",
"contentHash": "YYpq7NM50bSSVUDjXyV/eiITk6syXqItPjKBOb3jEWS6RFsk0DNhpWMPW6b3hKDmArARuWDU6S2pVu1IeVrvIA=="
},
"Linguini.Bundle": {
"type": "Transitive",
"resolved": "0.3.1",
"contentHash": "k3D0IBSsS91T0G9rYACRYtAS27NDTGz4T3ygHhbhaFdA9i3cCfCpR39MqYEpoA1Ark3lzmvklHePmRCX5EQi1A==",
"dependencies": {
"Linguini.Shared": "0.3.0",
"Linguini.Syntax": "0.3.0"
}
},
"Linguini.Shared": {
"type": "Transitive",
"resolved": "0.3.0",
"contentHash": "rEYA9jWU1Nc9iDuPKar+4IVXjFbGrcNyYZCeU6hiWQoS4nA2UecHu36Rp4bxzasvRGda4FB2JZqty75m4e26VQ=="
},
"Linguini.Syntax": {
"type": "Transitive",
"resolved": "0.3.0",
"contentHash": "v8YV3xPFZfP+OSWjMd6yysp+o7cNnQXxq/slN8Yekoi+dFjsTNF0YiQWJIwg1j2FUbs6claJxY9R1FFlc4s6MA==",
"dependencies": {
"Linguini.Shared": "0.3.0"
}
},
"Microsoft.Bcl.AsyncInterfaces": {
"type": "Transitive",
"resolved": "1.1.0",
Expand Down Expand Up @@ -1074,9 +1096,10 @@
"reactor": {
"type": "Project",
"dependencies": {
"BepInEx.Unity.IL2CPP": "[6.0.0-be.662, )"
"BepInEx.Unity.IL2CPP": "[6.0.0-be.662, )",
"Linguini.Bundle": "[0.3.1, )"
}
}
}
}
}
}
27 changes: 25 additions & 2 deletions Reactor.Debugger/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,28 @@
"resolved": "2.1.0",
"contentHash": "YYpq7NM50bSSVUDjXyV/eiITk6syXqItPjKBOb3jEWS6RFsk0DNhpWMPW6b3hKDmArARuWDU6S2pVu1IeVrvIA=="
},
"Linguini.Bundle": {
"type": "Transitive",
"resolved": "0.3.1",
"contentHash": "k3D0IBSsS91T0G9rYACRYtAS27NDTGz4T3ygHhbhaFdA9i3cCfCpR39MqYEpoA1Ark3lzmvklHePmRCX5EQi1A==",
"dependencies": {
"Linguini.Shared": "0.3.0",
"Linguini.Syntax": "0.3.0"
}
},
"Linguini.Shared": {
"type": "Transitive",
"resolved": "0.3.0",
"contentHash": "rEYA9jWU1Nc9iDuPKar+4IVXjFbGrcNyYZCeU6hiWQoS4nA2UecHu36Rp4bxzasvRGda4FB2JZqty75m4e26VQ=="
},
"Linguini.Syntax": {
"type": "Transitive",
"resolved": "0.3.0",
"contentHash": "v8YV3xPFZfP+OSWjMd6yysp+o7cNnQXxq/slN8Yekoi+dFjsTNF0YiQWJIwg1j2FUbs6claJxY9R1FFlc4s6MA==",
"dependencies": {
"Linguini.Shared": "0.3.0"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
"resolved": "6.0.1",
Expand Down Expand Up @@ -867,9 +889,10 @@
"reactor": {
"type": "Project",
"dependencies": {
"BepInEx.Unity.IL2CPP": "[6.0.0-be.662, )"
"BepInEx.Unity.IL2CPP": "[6.0.0-be.662, )",
"Linguini.Bundle": "[0.3.1, )"
}
}
}
}
}
}
27 changes: 25 additions & 2 deletions Reactor.Example/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,28 @@
"resolved": "2.1.0",
"contentHash": "YYpq7NM50bSSVUDjXyV/eiITk6syXqItPjKBOb3jEWS6RFsk0DNhpWMPW6b3hKDmArARuWDU6S2pVu1IeVrvIA=="
},
"Linguini.Bundle": {
"type": "Transitive",
"resolved": "0.3.1",
"contentHash": "k3D0IBSsS91T0G9rYACRYtAS27NDTGz4T3ygHhbhaFdA9i3cCfCpR39MqYEpoA1Ark3lzmvklHePmRCX5EQi1A==",
"dependencies": {
"Linguini.Shared": "0.3.0",
"Linguini.Syntax": "0.3.0"
}
},
"Linguini.Shared": {
"type": "Transitive",
"resolved": "0.3.0",
"contentHash": "rEYA9jWU1Nc9iDuPKar+4IVXjFbGrcNyYZCeU6hiWQoS4nA2UecHu36Rp4bxzasvRGda4FB2JZqty75m4e26VQ=="
},
"Linguini.Syntax": {
"type": "Transitive",
"resolved": "0.3.0",
"contentHash": "v8YV3xPFZfP+OSWjMd6yysp+o7cNnQXxq/slN8Yekoi+dFjsTNF0YiQWJIwg1j2FUbs6claJxY9R1FFlc4s6MA==",
"dependencies": {
"Linguini.Shared": "0.3.0"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
"resolved": "6.0.1",
Expand Down Expand Up @@ -867,9 +889,10 @@
"reactor": {
"type": "Project",
"dependencies": {
"BepInEx.Unity.IL2CPP": "[6.0.0-be.662, )"
"BepInEx.Unity.IL2CPP": "[6.0.0-be.662, )",
"Linguini.Bundle": "[0.3.1, )"
}
}
}
}
}
}
36 changes: 36 additions & 0 deletions Reactor.Linguinitor/FluentCommentParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Text;
using System.Text.RegularExpressions;
using Linguini.Syntax.Ast;

namespace Reactor.Linguinitor;

internal static class FluentCommentParser
{
private static readonly Regex _variableCommentRegex = new(@"^\$(?<id>[\w-]+) \((?<type>String|Number|Date)\)( - (?<description>.+))?$", RegexOptions.Compiled);

public static MessageInfo Parse(AstComment astComment)
{
var descriptionBuilder = new StringBuilder();
var variables = new Dictionary<string, VariableInfo>();

foreach (var s in astComment.AsStr().Split("\n"))
{
var line = s.Trim();

if (_variableCommentRegex.Match(line) is { Success: true } match)
{
variables.Add(match.Groups["id"].Value, new VariableInfo(match.Groups["type"].Value, match.Groups["description"].Value));
}
else
{
descriptionBuilder.AppendLine(line);
}
}

return new MessageInfo(descriptionBuilder.ToString(), variables);
}

public record MessageInfo(string Description, Dictionary<string, VariableInfo> Variables);

public record VariableInfo(string Type, string? Description);
}
166 changes: 166 additions & 0 deletions Reactor.Linguinitor/LinguinitorGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
using CSharpPoet;
using Linguini.Syntax.Ast;
using Linguini.Syntax.Parser;
using Microsoft.CodeAnalysis;

namespace Reactor.Linguinitor;

[Generator(LanguageNames.CSharp)]
public class LinguinitorGenerator : IIncrementalGenerator
{
private static CSharpFile CreateTranslateFile(string @namespace, params CSharpType.IMember[] members)
{
return new CSharpFile(@namespace)
{
new CSharpClass(Visibility.Internal, "Translate")
{
IsStatic = true,
IsPartial = true,
Members = members,
},
};
}

private static IEnumerable<Identifier> GetVariables(AstMessage astMessage)
{
foreach (var placeable in astMessage.Value!.Elements.OfType<Placeable>())
{
switch (placeable.Expression)
{
case VariableReference variableReference:
yield return variableReference.Id;
break;
}
}
}

public void Initialize(IncrementalGeneratorInitializationContext context)
{
var rootNamespaceProvider =
context.AnalyzerConfigOptionsProvider
.Select((options, _) => options.GlobalOptions.TryGetValue("build_property.RootNamespace", out var rootNamespace) ? rootNamespace : throw new InvalidOperationException("Failed to get RootNamespace"));

context.RegisterSourceOutput(rootNamespaceProvider, (spc, rootNamespace) =>
{
const string ProviderType = "Reactor.Localization.Providers.FluentLocalizationProvider";
spc.AddSource("Translate", CreateTranslateFile(
rootNamespace,
new CSharpField(Visibility.Private, ProviderType + "?", "_provider") { IsStatic = true },
new CSharpProperty(ProviderType, "Provider")
{
IsStatic = true,
Getter = new CSharpProperty.Accessor
{
Body = writer => writer.Write("_provider ?? throw new System.InvalidOperationException(\"Provider must be set before using Translate\");"),
},
Setter = new CSharpProperty.Accessor
{
Body = writer => writer.Write("_provider = value;"),
},
}
).ToString());
});

var textFiles = context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith(".ftl", StringComparison.Ordinal));

var namesAndContents = textFiles.Select((text, cancellationToken) => (
Name: Path.GetFileNameWithoutExtension(text.Path),
Content: text.GetText(cancellationToken)!.ToString()
));

context.RegisterSourceOutput(namesAndContents.Combine(rootNamespaceProvider), (spc, pair) =>
{
var ((name, content), rootNamespace) = pair;
var fixedName = name.ToPascalCase();

var idsClass = new CSharpClass("Ids")
{
IsStatic = true,
};

var fileClass = new CSharpClass(fixedName)
{
IsStatic = true,
Members = { idsClass },
};

var resource = new LinguiniParser(content).ParseWithComments();

foreach (var message in resource.Entries.OfType<AstMessage>())
{
var id = message.GetId();
var fixedId = id.ToPascalCase();

idsClass.Members.Add(new CSharpField(Visibility.Public, "string", fixedId)
{
IsConst = true,
DefaultValue = $"\"{id}\"",
});

var messageInfo = message.Comment != null ? FluentCommentParser.Parse(message.Comment) : null;
var variables = GetVariables(message).Select(m => m.ToString()).ToDictionary(
m => m,
m =>
{
if (messageInfo != null && messageInfo.Variables.TryGetValue(m, out var variableInfo))
{
return variableInfo.Type switch
{
"String" => "string",
"Number" => "double",
_ => throw new NotSupportedException($"Variable type {variableInfo.Type} is not supported"),
};
}

return "string";
}
);

void WriteComment(CodeWriter writer)
{
if (messageInfo == null) return;

writer.WriteLine("<summary>");

foreach (var line in messageInfo.Description.Trim().Split("\n"))
{
writer.WriteLine(line);
}

writer.WriteLine("</summary>");

foreach (var (variableId, info) in messageInfo.Variables)
{
writer.WriteLine($"<param name=\"{variableId}\">{info.Description}</param>");
}
}

if (variables.Count <= 0)
{
fileClass.Members.Add(new CSharpProperty("string", fixedId)
{
XmlComment = WriteComment,
IsStatic = true,
Getter = new CSharpProperty.Accessor
{
Body = writer => writer.Write($"Provider.GetText(Ids.{fixedId});"),
},
});
}
else
{
fileClass.Members.Add(new CSharpMethod("string", fixedId)
{
XmlComment = WriteComment,
IsStatic = true,
Parameters = variables.Select(v => new CSharpParameter(v.Value, v.Key)).ToArray(),
BodyType = BodyType.Expression,
Body = writer => writer.WriteLine($"Provider.GetText(Ids.{fixedId}, {string.Join(", ", variables.Select(v => $"(\"{v.Key}\", {v.Key.ToCamelCase()})"))});"),
});
}
}

spc.AddSource("Translate." + fixedName, CreateTranslateFile(rootNamespace, fileClass).ToString());
});
}
}
35 changes: 35 additions & 0 deletions Reactor.Linguinitor/Reactor.Linguinitor.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net472</TargetFrameworks>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CSharpPoet" Version="0.2.0-dev" Private="true" GeneratePathProperty="true" />

<PackageReference Include="Linguini.Shared" Version="0.3.0" Private="true" GeneratePathProperty="true" />
<PackageReference Include="Linguini.Syntax" Version="0.3.0" Private="true" GeneratePathProperty="true" />

<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" PrivateAssets="all" />
</ItemGroup>

<PropertyGroup>
<GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
</PropertyGroup>

<Target Name="GetDependencyTargetPaths">
<PropertyGroup>
<_LinguiniTargetFramework Condition="'$(TargetFramework)' == 'net6.0'">netstandard2.1</_LinguiniTargetFramework>
<_LinguiniTargetFramework Condition="'$(TargetFramework)' == 'net472'">net451</_LinguiniTargetFramework>
</PropertyGroup>

<ItemGroup>
<TargetPathWithTargetPlatformMoniker Include="$(PkgCSharpPoet)\lib\netstandard2.0\CSharpPoet.dll" IncludeRuntimeDependency="false" />
<TargetPathWithTargetPlatformMoniker Include="$(PkgLinguini_Shared)\lib\$(_LinguiniTargetFramework)\Linguini.Shared.dll" IncludeRuntimeDependency="false" />
<TargetPathWithTargetPlatformMoniker Include="$(PkgLinguini_Syntax)\lib\$(_LinguiniTargetFramework)\Linguini.Syntax.dll" IncludeRuntimeDependency="false" />
</ItemGroup>
</Target>
</Project>
Loading