mirror of
https://github.com/NuclearPowered/Reactor.git
synced 2026-10-19 15:51:54 +00:00
Localize Reactor with fluent
This commit is contained in:
17 files changed
+779
-5
No files matched your search
@@ -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",
|
||||
@@ -1074,7 +1096,8 @@
|
||||
"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, )"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -867,7 +889,8 @@
|
||||
"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, )"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -867,7 +889,8 @@
|
||||
"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, )"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Reactor.Linguinitor;
|
||||
|
||||
internal static class StringExtensions
|
||||
{
|
||||
private static string ToCase(this string value, bool capitalizeFirst)
|
||||
{
|
||||
var stringBuilder = new StringBuilder();
|
||||
|
||||
for (var i = 0; i < value.Length; i++)
|
||||
{
|
||||
var c = value[i];
|
||||
|
||||
if (c == '-')
|
||||
{
|
||||
i++;
|
||||
stringBuilder.Append(char.ToUpperInvariant(value[i]));
|
||||
}
|
||||
else
|
||||
{
|
||||
stringBuilder.Append(capitalizeFirst && i == 0 ? char.ToUpperInvariant(c) : c);
|
||||
}
|
||||
}
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
public static string ToPascalCase(this string value)
|
||||
{
|
||||
return value.ToCase(true);
|
||||
}
|
||||
|
||||
public static string ToCamelCase(this string value)
|
||||
{
|
||||
return value.ToCase(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net6.0": {
|
||||
"CSharpPoet": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.2.0-dev, )",
|
||||
"resolved": "0.2.0-dev"
|
||||
},
|
||||
"Linguini.Shared": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.3.0, )",
|
||||
"resolved": "0.3.0",
|
||||
"contentHash": "rEYA9jWU1Nc9iDuPKar+4IVXjFbGrcNyYZCeU6hiWQoS4nA2UecHu36Rp4bxzasvRGda4FB2JZqty75m4e26VQ=="
|
||||
},
|
||||
"Linguini.Syntax": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.3.0, )",
|
||||
"resolved": "0.3.0",
|
||||
"contentHash": "v8YV3xPFZfP+OSWjMd6yysp+o7cNnQXxq/slN8Yekoi+dFjsTNF0YiQWJIwg1j2FUbs6claJxY9R1FFlc4s6MA==",
|
||||
"dependencies": {
|
||||
"Linguini.Shared": "0.3.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.CodeAnalysis.CSharp": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.3.1, )",
|
||||
"resolved": "4.3.1",
|
||||
"contentHash": "5C9VHvahL98tumlyaT/loB5rW+K/6q0UU7uLyT1Dv15YjZraRkqML9u2t2e8GaO7XqEOtBVqK/SlxXOPqwzxog==",
|
||||
"dependencies": {
|
||||
"Microsoft.CodeAnalysis.Common": "[4.3.1]"
|
||||
}
|
||||
},
|
||||
"StyleCop.Analyzers": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.2.0-beta.435, )",
|
||||
"resolved": "1.2.0-beta.435",
|
||||
"contentHash": "TADk7vdGXtfTnYCV7GyleaaRTQjfoSfZXprQrVMm7cSJtJbFc1QIbWPyLvrgrfGdfHbGmUPvaN4ODKNxg2jgPQ==",
|
||||
"dependencies": {
|
||||
"StyleCop.Analyzers.Unstable": "1.2.0.435"
|
||||
}
|
||||
},
|
||||
"Microsoft.CodeAnalysis.Analyzers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.3.3",
|
||||
"contentHash": "j/rOZtLMVJjrfLRlAMckJLPW/1rze9MT1yfWqSIbUPGRu1m1P0fuo9PmqapwsmePfGB5PJrudQLvmUOAMF0DqQ=="
|
||||
},
|
||||
"Microsoft.CodeAnalysis.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.1",
|
||||
"contentHash": "wexpJffSEEwptwe6UTMxRDZCCtz+XubI4Qewl4JECnNhcQrtb0anhSUEV9Nz7WkoNfWkx1fptR8xh1egoxYrqw==",
|
||||
"dependencies": {
|
||||
"Microsoft.CodeAnalysis.Analyzers": "3.3.3",
|
||||
"System.Collections.Immutable": "6.0.0",
|
||||
"System.Memory": "4.5.4",
|
||||
"System.Reflection.Metadata": "5.0.0",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0",
|
||||
"System.Text.Encoding.CodePages": "6.0.0",
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
}
|
||||
},
|
||||
"StyleCop.Analyzers.Unstable": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.2.0.435",
|
||||
"contentHash": "ouwPWZxbOV3SmCZxIRqHvljkSzkCyi1tDoMzQtDb/bRP8ctASV/iRJr+A2Gdj0QLaLmWnqTWDrH82/iP+X80Lg=="
|
||||
},
|
||||
"System.Collections.Immutable": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "l4zZJ1WU2hqpQQHXz1rvC3etVZN+2DLmQMO79FhOTZHMn8tDRr+WU287sbomD0BETlmKDn0ygUgVy9k5xkkJdA==",
|
||||
"dependencies": {
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
||||
}
|
||||
},
|
||||
"System.Memory": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.4",
|
||||
"contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw=="
|
||||
},
|
||||
"System.Reflection.Metadata": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ=="
|
||||
},
|
||||
"System.Runtime.CompilerServices.Unsafe": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
|
||||
},
|
||||
"System.Text.Encoding.CodePages": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==",
|
||||
"dependencies": {
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
||||
}
|
||||
},
|
||||
"System.Threading.Tasks.Extensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.4",
|
||||
"contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Files", "Solution
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reactor.Networking.Shared", "Reactor.Networking.Shared\Reactor.Networking.Shared.csproj", "{1E68CA20-22EB-4E57-B525-81970BE6363D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reactor.Linguinitor", "Reactor.Linguinitor\Reactor.Linguinitor.csproj", "{B4740F75-B657-4515-A8F6-8A84D64E8814}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -44,6 +46,10 @@ Global
|
||||
{1E68CA20-22EB-4E57-B525-81970BE6363D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1E68CA20-22EB-4E57-B525-81970BE6363D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1E68CA20-22EB-4E57-B525-81970BE6363D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B4740F75-B657-4515-A8F6-8A84D64E8814}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B4740F75-B657-4515-A8F6-8A84D64E8814}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B4740F75-B657-4515-A8F6-8A84D64E8814}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B4740F75-B657-4515-A8F6-8A84D64E8814}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
EndGlobalSection
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
-for-more-info-see = For more info see https://reactor.gg/handshake
|
||||
|
||||
vanilla-server-warning =
|
||||
This server doesn't support Reactor protocol.
|
||||
The lobbies shown may not be compatible with your current mods.
|
||||
{ -for-more-info-see }
|
||||
|
||||
public-lobby-warning =
|
||||
You can't make public lobbies on servers that don't support Reactor protocol.
|
||||
{ -for-more-info-see }
|
||||
@@ -0,0 +1,10 @@
|
||||
-for-more-info-see = Voor meer informatie, zie https://reactor.gg/handshake
|
||||
|
||||
vanilla-server-warning =
|
||||
Deze server ondersteunt het Reactor protocol niet.
|
||||
De lobbies die je ziet passen mogelijk niet bij jouw huidige mods.
|
||||
{ -for-more-info-see }
|
||||
|
||||
public-lobby-warning =
|
||||
Je kan geen publieke lobbies maken op servers die het Reactor protocol niet ondersteunen.
|
||||
{ -for-more-info-see }
|
||||
@@ -0,0 +1,10 @@
|
||||
-for-more-info-see = Dla więcej informacji, zobacz https://reactor.gg/handshake
|
||||
|
||||
vanilla-server-warning =
|
||||
Ten serwer nie wspiera protokołu Reactor.
|
||||
Pokazane gry mogą nie być kompatibline z twoimi modami.
|
||||
{ -for-more-info-see }
|
||||
|
||||
public-lobby-warning =
|
||||
Nie możesz tworzyć publicznych gier w serwerach, które nie wspierają protokołu Reactor.
|
||||
{ -for-more-info-see }
|
||||
@@ -0,0 +1,258 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Il2CppInterop.Runtime;
|
||||
using Il2CppInterop.Runtime.InteropTypes;
|
||||
using Il2CppInterop.Runtime.InteropTypes.Arrays;
|
||||
using Linguini.Bundle;
|
||||
using Linguini.Bundle.Builder;
|
||||
using Linguini.Bundle.Errors;
|
||||
using Linguini.Shared.Types.Bundle;
|
||||
using Linguini.Syntax.Ast;
|
||||
using Reactor.Localization.Extensions;
|
||||
using Reactor.Localization.Utilities;
|
||||
using Reactor.Utilities;
|
||||
using Object = Il2CppSystem.Object;
|
||||
|
||||
namespace Reactor.Localization.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a localization provider that uses Fluent.
|
||||
/// </summary>
|
||||
public class FluentLocalizationProvider : LocalizationProvider
|
||||
{
|
||||
private readonly Func<CultureInfo, IEnumerable<Resource>> _loadResources;
|
||||
private readonly List<FluentBundle> _bundles = new();
|
||||
private readonly Dictionary<StringNames, string> _ids = new();
|
||||
private readonly Dictionary<StringNames, string[]> _arguments = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the fallback language.
|
||||
/// </summary>
|
||||
public SupportedLangs FallbackLanguage { get; set; } = SupportedLangs.English;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FluentLocalizationProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="loadResources">The implementation of resource loading.</param>
|
||||
public FluentLocalizationProvider(Func<CultureInfo, IEnumerable<Resource>> loadResources)
|
||||
{
|
||||
_loadResources = loadResources;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FluentLocalizationProvider"/> class that loads resources from the calling assembly.
|
||||
/// </summary>
|
||||
/// <param name="prefix">The prefix of resource file names to load.</param>
|
||||
/// <returns>a <see cref="FluentLocalizationProvider"/>.</returns>
|
||||
public static FluentLocalizationProvider FromEmbeddedResources(string prefix) => FromEmbeddedResources(Assembly.GetCallingAssembly(), prefix);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FluentLocalizationProvider"/> class that loads resources from the specified <paramref name="assembly"/>.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly to load resources from.</param>
|
||||
/// <param name="prefix">The prefix of resource file names to load.</param>
|
||||
/// <returns>a <see cref="FluentLocalizationProvider"/>.</returns>
|
||||
public static FluentLocalizationProvider FromEmbeddedResources(Assembly assembly, string prefix)
|
||||
{
|
||||
return new FluentLocalizationProvider(cultureInfo =>
|
||||
{
|
||||
return assembly.GetManifestResourceNames()
|
||||
.Where(n => n.StartsWith($"{prefix}.{cultureInfo.Name}.", StringComparison.Ordinal) && n.EndsWith(".ftl", StringComparison.Ordinal))
|
||||
.Select(n => (Resource) new StreamReader(assembly.GetManifestResourceStream(n)!));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a <see cref="StringNames"/> for use with base game apis.
|
||||
/// </summary>
|
||||
/// <param name="id">The id of a Fluent message.</param>
|
||||
/// <returns>A <see cref="StringNames"/>.</returns>
|
||||
public StringNames Register(string id)
|
||||
{
|
||||
var stringNames = CustomStringName.Create();
|
||||
_ids[stringNames] = id;
|
||||
|
||||
return stringNames;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a <see cref="StringNames"/> for use with base game apis with overriden format argument names.
|
||||
/// </summary>
|
||||
/// <param name="id">The id of a Fluent message.</param>
|
||||
/// <param name="argumentNames">An array of Fluent variable names in the same order as format arguments.</param>
|
||||
/// <returns>A <see cref="StringNames"/>.</returns>
|
||||
public StringNames Register(string id, params string[] argumentNames)
|
||||
{
|
||||
var stringNames = Register(id);
|
||||
_arguments[stringNames] = argumentNames;
|
||||
|
||||
return stringNames;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int Priority => ReactorPriority.Normal;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLanguageChanged(SupportedLangs newLanguage)
|
||||
{
|
||||
if (CurrentLanguage == newLanguage) return;
|
||||
|
||||
_bundles.Clear();
|
||||
_bundles.Add(Load(newLanguage.ToCultureInfo()));
|
||||
if (newLanguage != FallbackLanguage) _bundles.Add(Load(FallbackLanguage.ToCultureInfo()));
|
||||
}
|
||||
|
||||
private FluentBundle Load(CultureInfo culture)
|
||||
{
|
||||
return LinguiniBuilder.Builder()
|
||||
.CultureInfo(culture)
|
||||
.AddResources(_loadResources(culture))
|
||||
.SetUseIsolating(false)
|
||||
.UncheckedBuild();
|
||||
}
|
||||
|
||||
private string GetMsg(string id, IDictionary<string, IFluentType>? args)
|
||||
{
|
||||
foreach (var bundle in _bundles)
|
||||
{
|
||||
if (bundle.TryGetMsg(id, null, args, out var errors, out var message))
|
||||
{
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
throw new LinguiniException(errors);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
throw new KeyNotFoundException();
|
||||
}
|
||||
|
||||
private static bool Is<T>(Il2CppObjectBase o)
|
||||
{
|
||||
return IL2CPP.il2cpp_class_is_assignable_from(Il2CppClassPointerStore<T>.NativeClassPtr, IL2CPP.il2cpp_object_get_class(o.Pointer));
|
||||
}
|
||||
|
||||
private static bool TryUnbox<T>(Il2CppObjectBase o, [NotNullWhen(true)] out T? result) where T : unmanaged
|
||||
{
|
||||
if (!Is<T>(o))
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = o.Unbox<T>();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static IFluentType ToFluentType(Object o)
|
||||
{
|
||||
if (Is<string>(o)) return new FluentString(IL2CPP.Il2CppStringToManaged(o.Pointer));
|
||||
|
||||
if (TryUnbox<byte>(o, out var @byte)) return (FluentNumber) @byte;
|
||||
if (TryUnbox<sbyte>(o, out var @sbyte)) return (FluentNumber) @sbyte;
|
||||
if (TryUnbox<short>(o, out var @short)) return (FluentNumber) @short;
|
||||
if (TryUnbox<int>(o, out var @int)) return (FluentNumber) @int;
|
||||
if (TryUnbox<long>(o, out var @long)) return (FluentNumber) @long;
|
||||
if (TryUnbox<ushort>(o, out var @ushort)) return (FluentNumber) @ushort;
|
||||
if (TryUnbox<uint>(o, out var @uint)) return (FluentNumber) @uint;
|
||||
if (TryUnbox<ulong>(o, out var @ulong)) return (FluentNumber) @ulong;
|
||||
if (TryUnbox<float>(o, out var @float)) return (FluentNumber) @float;
|
||||
if (TryUnbox<double>(o, out var @double)) return (FluentNumber) @double;
|
||||
|
||||
return new FluentString(o.ToString());
|
||||
}
|
||||
|
||||
private static IFluentType ToFluentType(object o)
|
||||
{
|
||||
return o switch
|
||||
{
|
||||
string str => (FluentString) str,
|
||||
byte num => (FluentNumber) num,
|
||||
sbyte num => (FluentNumber) num,
|
||||
short num => (FluentNumber) num,
|
||||
ushort num => (FluentNumber) num,
|
||||
int num => (FluentNumber) num,
|
||||
uint num => (FluentNumber) num,
|
||||
long num => (FluentNumber) num,
|
||||
ulong num => (FluentNumber) num,
|
||||
double num => (FluentNumber) num,
|
||||
float num => (FluentNumber) num,
|
||||
_ => (FluentString) o.ToString()!,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the localized text for the given fluent message <paramref name="id"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The id of the message.</param>
|
||||
/// <param name="args">The arguments used for formatting.</param>
|
||||
/// <returns>Text localized and formatted by fluent.</returns>
|
||||
public string GetText(string id, params (string, object)[] args)
|
||||
{
|
||||
var fluentArgs = new Dictionary<string, IFluentType>(args.Length);
|
||||
foreach (var (key, value) in args)
|
||||
{
|
||||
fluentArgs[key] = ToFluentType(value);
|
||||
}
|
||||
|
||||
return GetMsg(id, fluentArgs);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool TryGetTextFormatted(StringNames stringName, Il2CppReferenceArray<Object> parts, [NotNullWhen(true)] out string? result)
|
||||
{
|
||||
if (_ids.TryGetValue(stringName, out var id))
|
||||
{
|
||||
var hasArgumentsOverride = _arguments.TryGetValue(stringName, out var arguments);
|
||||
if (hasArgumentsOverride)
|
||||
{
|
||||
if (arguments!.Length != parts.Length)
|
||||
throw new InvalidOperationException($"Arguments override for {id} has an invalid count of arguments");
|
||||
}
|
||||
|
||||
var args = new Dictionary<string, IFluentType>(parts.Length);
|
||||
for (var i = 0; i < parts.Count; i++)
|
||||
{
|
||||
var part = parts[i];
|
||||
|
||||
if (hasArgumentsOverride)
|
||||
{
|
||||
args[arguments![i]] = ToFluentType(part);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fluent doesn't allow positional variables nor starting variable names with numbers so we have to use a,b,c,d... instead
|
||||
if (i > 25) throw new InvalidOperationException("Too many format parts");
|
||||
var c = (char) ('a' + i);
|
||||
args[c.ToString()] = ToFluentType(part);
|
||||
}
|
||||
}
|
||||
|
||||
result = GetMsg(id, args);
|
||||
return true;
|
||||
}
|
||||
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool TryGetText(StringNames stringName, [NotNullWhen(true)] out string? result)
|
||||
{
|
||||
if (_ids.TryGetValue(stringName, out var id))
|
||||
{
|
||||
result = GetMsg(id, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ internal static class HttpPatches
|
||||
{
|
||||
if (responseHeader == null)
|
||||
{
|
||||
DisconnectPopup.Instance.ShowCustom("This region doesn't support modded handshake.\nThe lobbies shown may not be compatible with your current mods.\nFor more info see https://reactor.gg/handshake");
|
||||
DisconnectPopup.Instance.ShowCustom(Translate.Messages.VanillaServerWarning);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ internal static class HttpPatches
|
||||
size.x *= 2.5f;
|
||||
background.size = size;
|
||||
popup.TextAreaTMP.fontSizeMin = 2;
|
||||
popup.Show("You can't make public lobbies on regions that don't support modded handshake.\nFor more info see https://reactor.gg/handshake");
|
||||
popup.Show(Translate.Messages.PublicLobbyWarning);
|
||||
}));
|
||||
|
||||
if (AmongUsClient.Instance.AmHost && AmongUsClient.Instance.IsGamePublic)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
|
||||
<Description>Core mod and API for Among Us</Description>
|
||||
|
||||
@@ -16,10 +17,14 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||
<Compile Include="..\Reactor.Networking.Shared\**\*.cs" LinkBase="Networking" Visible="false" />
|
||||
|
||||
<PackageReference Include="Linguini.Bundle" Version="0.3.1" />
|
||||
<ProjectReference Include="..\Reactor.Linguinitor\Reactor.Linguinitor.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Assets\**\*" Exclude="Assets\icon.png" />
|
||||
<AdditionalFiles Include="Assets\Localization\en\*.ftl" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="../AmongUs.props" />
|
||||
|
||||
@@ -41,6 +41,8 @@ public partial class ReactorPlugin : BasePlugin
|
||||
|
||||
internal RegionInfoWatcher RegionInfoWatcher { get; } = new();
|
||||
|
||||
internal FluentLocalizationProvider LocalizationProvider { get; } = FluentLocalizationProvider.FromEmbeddedResources("Reactor.Assets.Localization");
|
||||
|
||||
/// <inheritdoc />
|
||||
public ReactorPlugin()
|
||||
{
|
||||
@@ -55,6 +57,7 @@ public partial class ReactorPlugin : BasePlugin
|
||||
MethodRpcAttribute.Initialize();
|
||||
|
||||
LocalizationManager.Register(new HardCodedLocalizationProvider());
|
||||
LocalizationManager.Register(Translate.Provider = LocalizationProvider);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -37,6 +37,16 @@
|
||||
"Samboy063.Cpp2IL.Core": "2022.0.7.2"
|
||||
}
|
||||
},
|
||||
"Linguini.Bundle": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.3.1, )",
|
||||
"resolved": "0.3.1",
|
||||
"contentHash": "k3D0IBSsS91T0G9rYACRYtAS27NDTGz4T3ygHhbhaFdA9i3cCfCpR39MqYEpoA1Ark3lzmvklHePmRCX5EQi1A==",
|
||||
"dependencies": {
|
||||
"Linguini.Shared": "0.3.0",
|
||||
"Linguini.Syntax": "0.3.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.1.1, )",
|
||||
@@ -141,6 +151,19 @@
|
||||
"resolved": "2.1.0",
|
||||
"contentHash": "YYpq7NM50bSSVUDjXyV/eiITk6syXqItPjKBOb3jEWS6RFsk0DNhpWMPW6b3hKDmArARuWDU6S2pVu1IeVrvIA=="
|
||||
},
|
||||
"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.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.1.1",
|
||||
|
||||
Reference in new issue
Block a user