Localize Reactor with fluent

This commit is contained in:
js6pak committed 2022-10-26 13:08:44 +02:00
1 parent 67c89f99ed
commit 1a7755b987
17 files changed
+783 -9

No files matched your search

+25 -2
View File
@@ -161,6 +161,28 @@
"resolved": "2.1.0", "resolved": "2.1.0",
"contentHash": "YYpq7NM50bSSVUDjXyV/eiITk6syXqItPjKBOb3jEWS6RFsk0DNhpWMPW6b3hKDmArARuWDU6S2pVu1IeVrvIA==" "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": { "Microsoft.Bcl.AsyncInterfaces": {
"type": "Transitive", "type": "Transitive",
"resolved": "1.1.0", "resolved": "1.1.0",
@@ -1074,9 +1096,10 @@
"reactor": { "reactor": {
"type": "Project", "type": "Project",
"dependencies": { "dependencies": {
"BepInEx.Unity.IL2CPP": "[6.0.0-be.662, )" "BepInEx.Unity.IL2CPP": "[6.0.0-be.662, )",
"Linguini.Bundle": "[0.3.1, )"
} }
} }
} }
} }
} }
+25 -2
View File
@@ -131,6 +131,28 @@
"resolved": "2.1.0", "resolved": "2.1.0",
"contentHash": "YYpq7NM50bSSVUDjXyV/eiITk6syXqItPjKBOb3jEWS6RFsk0DNhpWMPW6b3hKDmArARuWDU6S2pVu1IeVrvIA==" "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": { "Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "6.0.1", "resolved": "6.0.1",
@@ -867,9 +889,10 @@
"reactor": { "reactor": {
"type": "Project", "type": "Project",
"dependencies": { "dependencies": {
"BepInEx.Unity.IL2CPP": "[6.0.0-be.662, )" "BepInEx.Unity.IL2CPP": "[6.0.0-be.662, )",
"Linguini.Bundle": "[0.3.1, )"
} }
} }
} }
} }
} }
+25 -2
View File
@@ -131,6 +131,28 @@
"resolved": "2.1.0", "resolved": "2.1.0",
"contentHash": "YYpq7NM50bSSVUDjXyV/eiITk6syXqItPjKBOb3jEWS6RFsk0DNhpWMPW6b3hKDmArARuWDU6S2pVu1IeVrvIA==" "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": { "Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "6.0.1", "resolved": "6.0.1",
@@ -867,9 +889,10 @@
"reactor": { "reactor": {
"type": "Project", "type": "Project",
"dependencies": { "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);
}
+166
View File
@@ -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>
+38
View File
@@ -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);
}
}
+105
View File
@@ -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=="
}
}
}
}
+6
View File
@@ -18,6 +18,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Files", "Solution
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reactor.Networking.Shared", "Reactor.Networking.Shared\Reactor.Networking.Shared.csproj", "{1E68CA20-22EB-4E57-B525-81970BE6363D}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reactor.Networking.Shared", "Reactor.Networking.Shared\Reactor.Networking.Shared.csproj", "{1E68CA20-22EB-4E57-B525-81970BE6363D}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reactor.Linguinitor", "Reactor.Linguinitor\Reactor.Linguinitor.csproj", "{B4740F75-B657-4515-A8F6-8A84D64E8814}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{1E68CA20-22EB-4E57-B525-81970BE6363D}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
EndGlobalSection 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;
}
}
+2 -2
View File
@@ -87,7 +87,7 @@ internal static class HttpPatches
{ {
if (responseHeader == null) 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; size.x *= 2.5f;
background.size = size; background.size = size;
popup.TextAreaTMP.fontSizeMin = 2; 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) if (AmongUsClient.Instance.AmHost && AmongUsClient.Instance.IsGamePublic)
+5
View File
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<EnableDynamicLoading>true</EnableDynamicLoading>
<Description>Core mod and API for Among Us</Description> <Description>Core mod and API for Among Us</Description>
@@ -16,10 +17,14 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<Compile Include="..\Reactor.Networking.Shared\**\*.cs" LinkBase="Networking" Visible="false" /> <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>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Assets\**\*" Exclude="Assets\icon.png" /> <EmbeddedResource Include="Assets\**\*" Exclude="Assets\icon.png" />
<AdditionalFiles Include="Assets\Localization\en\*.ftl" />
</ItemGroup> </ItemGroup>
<Import Project="../AmongUs.props" /> <Import Project="../AmongUs.props" />
+3
View File
@@ -41,6 +41,8 @@ public partial class ReactorPlugin : BasePlugin
internal RegionInfoWatcher RegionInfoWatcher { get; } = new(); internal RegionInfoWatcher RegionInfoWatcher { get; } = new();
internal FluentLocalizationProvider LocalizationProvider { get; } = FluentLocalizationProvider.FromEmbeddedResources("Reactor.Assets.Localization");
/// <inheritdoc /> /// <inheritdoc />
public ReactorPlugin() public ReactorPlugin()
{ {
@@ -55,6 +57,7 @@ public partial class ReactorPlugin : BasePlugin
MethodRpcAttribute.Initialize(); MethodRpcAttribute.Initialize();
LocalizationManager.Register(new HardCodedLocalizationProvider()); LocalizationManager.Register(new HardCodedLocalizationProvider());
LocalizationManager.Register(Translate.Provider = LocalizationProvider);
} }
/// <inheritdoc /> /// <inheritdoc />
+24 -1
View File
@@ -37,6 +37,16 @@
"Samboy063.Cpp2IL.Core": "2022.0.7.2" "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": { "Microsoft.SourceLink.GitHub": {
"type": "Direct", "type": "Direct",
"requested": "[1.1.1, )", "requested": "[1.1.1, )",
@@ -141,6 +151,19 @@
"resolved": "2.1.0", "resolved": "2.1.0",
"contentHash": "YYpq7NM50bSSVUDjXyV/eiITk6syXqItPjKBOb3jEWS6RFsk0DNhpWMPW6b3hKDmArARuWDU6S2pVu1IeVrvIA==" "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": { "Microsoft.Build.Tasks.Git": {
"type": "Transitive", "type": "Transitive",
"resolved": "1.1.1", "resolved": "1.1.1",
@@ -886,4 +909,4 @@
} }
} }
} }
} }