mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Localization improvements:
*Allow content to define localization functions. * Add rldloc command to reload localizations. * Doc comments * Error handling * Parallelize loading of localization files, since I can only assume we'll have a lot eventually. * Type system stuff to allow content to pass custom data types into fluent. * Code cleanup.
This commit is contained in:
20
Robust.Client/Console/Commands/ReloadLocalizationsCommand.cs
Normal file
20
Robust.Client/Console/Commands/ReloadLocalizationsCommand.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Robust.Client.Console.Commands
|
||||
{
|
||||
internal sealed class ReloadLocalizationsCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "rldloc";
|
||||
public string Description => "Reloads localization (client & server)";
|
||||
public string Help => "Usage: rldloc";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
IoCManager.Resolve<ILocalizationManager>().ReloadLocalizations();
|
||||
|
||||
shell.RemoteExecuteCommand("sudo rldloc");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -318,7 +318,7 @@ namespace Robust.Client
|
||||
logManager.GetSawmill("discord").Level = LogLevel.Warning;
|
||||
logManager.GetSawmill("net.predict").Level = LogLevel.Info;
|
||||
logManager.GetSawmill("szr").Level = LogLevel.Info;
|
||||
logManager.GetSawmill("Loc").Level = LogLevel.Error;
|
||||
logManager.GetSawmill("loc").Level = LogLevel.Error;
|
||||
|
||||
#if DEBUG_ONLY_FCE_INFO
|
||||
#if DEBUG_ONLY_FCE_LOG
|
||||
|
||||
18
Robust.Server/Console/Commands/ReloadLocalizationsCommand.cs
Normal file
18
Robust.Server/Console/Commands/ReloadLocalizationsCommand.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Robust.Server.Console.Commands
|
||||
{
|
||||
internal sealed class ReloadLocalizationsCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "rldloc";
|
||||
public string Description => "Reloads localization (client & server)";
|
||||
public string Help => "Usage: rldloc";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
IoCManager.Resolve<ILocalizationManager>().ReloadLocalizations();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ namespace Robust.Server
|
||||
mgr.RootSawmill.AddHandler(handler);
|
||||
mgr.GetSawmill("res.typecheck").Level = LogLevel.Info;
|
||||
mgr.GetSawmill("go.sys").Level = LogLevel.Info;
|
||||
mgr.GetSawmill("Loc").Level = LogLevel.Error;
|
||||
mgr.GetSawmill("loc").Level = LogLevel.Error;
|
||||
// mgr.GetSawmill("szr").Level = LogLevel.Info;
|
||||
|
||||
#if DEBUG_ONLY_FCE_INFO
|
||||
|
||||
@@ -31,6 +31,13 @@ namespace Robust.Shared.Localization
|
||||
/// </returns>
|
||||
string GetString(string messageId);
|
||||
|
||||
/// <summary>
|
||||
/// Try- version of <see cref="GetString(string)"/>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Does not log a warning if the message does not exist.
|
||||
/// Does however log errors if any occur while formatting.
|
||||
/// </remarks>
|
||||
bool TryGetString(string messageId, [NotNullWhen(true)] out string? value);
|
||||
|
||||
/// <summary>
|
||||
@@ -38,6 +45,13 @@ namespace Robust.Shared.Localization
|
||||
/// </summary>
|
||||
string GetString(string messageId, params (string, object)[] args);
|
||||
|
||||
/// <summary>
|
||||
/// Try- version of <see cref="GetString(string, ValueTuple{string, object}[])"/>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Does not log a warning if the message does not exist.
|
||||
/// Does however log errors if any occur while formatting.
|
||||
/// </remarks>
|
||||
bool TryGetString(string messageId, [NotNullWhen(true)] out string? value, params (string, object)[] args);
|
||||
|
||||
/// <summary>
|
||||
@@ -51,10 +65,27 @@ namespace Robust.Shared.Localization
|
||||
/// </summary>
|
||||
/// <param name="resourceManager"></param>
|
||||
/// <param name="culture"></param>
|
||||
void LoadCulture(IResourceManager resourceManager, CultureInfo culture);
|
||||
[Obsolete("Use LoadCulture without IResourceManager overload instead.")]
|
||||
void LoadCulture(IResourceManager resourceManager, CultureInfo culture) => LoadCulture(culture);
|
||||
|
||||
/// <summary>
|
||||
/// Load data for a culture.
|
||||
/// </summary>
|
||||
/// <param name="culture"></param>
|
||||
void LoadCulture(CultureInfo culture);
|
||||
|
||||
/// <summary>
|
||||
/// Immediately reload ALL localizations from resources.
|
||||
/// </summary>
|
||||
void ReloadLocalizations();
|
||||
|
||||
/// <summary>
|
||||
/// Add a function that can be called from Fluent localizations.
|
||||
/// </summary>
|
||||
/// <param name="culture">The culture to add the function instance for.</param>
|
||||
/// <param name="name">The name of the function.</param>
|
||||
/// <param name="function">The function itself.</param>
|
||||
void AddFunction(CultureInfo culture, string name, LocFunction function);
|
||||
|
||||
/// <summary>
|
||||
/// Remnants of the old Localization system.
|
||||
|
||||
@@ -61,8 +61,8 @@ namespace Robust.Shared.Localization
|
||||
/// Load data for a culture.
|
||||
/// </summary>
|
||||
/// <param name="resourceManager"></param>
|
||||
/// <param name="macroFactory"></param>
|
||||
/// <param name="culture"></param>
|
||||
[Obsolete("Use ILocalizationManager directly for setup methods.")]
|
||||
public static void LoadCulture(IResourceManager resourceManager, CultureInfo culture)
|
||||
{
|
||||
LocalizationManager.LoadCulture(resourceManager, culture);
|
||||
|
||||
142
Robust.Shared/Localization/LocFunction.cs
Normal file
142
Robust.Shared/Localization/LocFunction.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Fluent.Net;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Robust.Shared.Localization
|
||||
{
|
||||
// Basically Fluent.Net is based on an ancient pre-1.0 version of fluent.js.
|
||||
// Said version of fluent.js was also a complete garbage fire implementation wise.
|
||||
// So Fluent.Net is garbage implementation wise.
|
||||
// ...yay
|
||||
// (the current implementation of fluent.js is written in TS and actually sane)
|
||||
//
|
||||
// Because of this, we can't expose it to content, so we have to wrap everything related to functions.
|
||||
// This basically mimics the modern typescript fluent.js API. Somewhat.
|
||||
|
||||
/// <summary>
|
||||
/// Function signature runnable by localizations.
|
||||
/// </summary>
|
||||
/// <param name="args">Contains arguments and options passed to the function by the calling localization.</param>
|
||||
public delegate ILocValue LocFunction(LocArgs args);
|
||||
|
||||
[PublicAPI]
|
||||
public readonly struct LocContext
|
||||
{
|
||||
public CultureInfo Culture => Context.Culture;
|
||||
|
||||
internal readonly MessageContext Context;
|
||||
|
||||
internal LocContext(MessageContext ctx)
|
||||
{
|
||||
Context = ctx;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Arguments and options passed to a localization function.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public readonly struct LocArgs
|
||||
{
|
||||
public LocArgs(IReadOnlyList<ILocValue> args, IReadOnlyDictionary<string, ILocValue> options)
|
||||
{
|
||||
Args = args;
|
||||
Options = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Positional arguments passed to the function, in order.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ILocValue> Args { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Key-value options passed to the function.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, ILocValue> Options { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A value passed around in the localization system.
|
||||
/// </summary>
|
||||
/// <seealso cref="LocValue{T}"/>
|
||||
public interface ILocValue
|
||||
{
|
||||
/// <summary>
|
||||
/// Format this value to a string.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used when this value is interpolated directly in localizations.
|
||||
/// </remarks>
|
||||
/// <param name="ctx">Context containing data like culture used.</param>
|
||||
/// <returns>The formatted string.</returns>
|
||||
string Format(LocContext ctx);
|
||||
|
||||
/// <summary>
|
||||
/// Boxed value stored by this instance.
|
||||
/// </summary>
|
||||
object? Value { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of a localization value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The idea is that inheritors could add extra data like formatting parameters
|
||||
/// and then use those by overriding <see cref="Format"/>.
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">The type of value stored.</typeparam>
|
||||
[PublicAPI]
|
||||
public abstract record LocValue<T> : ILocValue
|
||||
{
|
||||
/// <summary>
|
||||
/// The stored value.
|
||||
/// </summary>
|
||||
public T Value { get; init; }
|
||||
|
||||
object? ILocValue.Value => Value;
|
||||
|
||||
protected LocValue(T val)
|
||||
{
|
||||
Value = val;
|
||||
}
|
||||
|
||||
public abstract string Format(LocContext ctx);
|
||||
}
|
||||
|
||||
public sealed record LocValueNumber(double Value) : LocValue<double>(Value)
|
||||
{
|
||||
public override string Format(LocContext ctx)
|
||||
{
|
||||
return Value.ToString(ctx.Culture);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record LocValueDateTime(DateTime Value) : LocValue<DateTime>(Value)
|
||||
{
|
||||
public override string Format(LocContext ctx)
|
||||
{
|
||||
return Value.ToString(ctx.Culture);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record LocValueString(string Value) : LocValue<string>(Value)
|
||||
{
|
||||
public override string Format(LocContext ctx)
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores an "invalid" string value. Produced by e.g. unresolved variable references.
|
||||
/// </summary>
|
||||
public sealed record LocValueNone(string Value) : LocValue<string>(Value)
|
||||
{
|
||||
public override string Format(LocContext ctx)
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,22 +3,33 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Fluent.Net;
|
||||
using Fluent.Net.RuntimeAst;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Localization
|
||||
{
|
||||
internal sealed class LocalizationManager : ILocalizationManagerInternal
|
||||
internal sealed class LocalizationManager : ILocalizationManagerInternal, IPostInjectInit
|
||||
{
|
||||
[Dependency] private readonly IResourceManager _res = default!;
|
||||
[Dependency] private readonly ILogManager _log = default!;
|
||||
|
||||
private ISawmill _logSawmill = default!;
|
||||
private readonly Dictionary<CultureInfo, MessageContext> _contexts = new();
|
||||
|
||||
private CultureInfo? _defaultCulture;
|
||||
|
||||
void IPostInjectInit.PostInject()
|
||||
{
|
||||
_logSawmill = _log.GetSawmill("loc");
|
||||
}
|
||||
|
||||
public string GetString(string messageId)
|
||||
{
|
||||
if (_defaultCulture == null)
|
||||
@@ -26,7 +37,7 @@ namespace Robust.Shared.Localization
|
||||
|
||||
if (!TryGetString(messageId, out var msg))
|
||||
{
|
||||
Logger.WarningS("Loc", $"Unknown messageId ({_defaultCulture.IetfLanguageTag}): {messageId}");
|
||||
_logSawmill.Warning("Unknown messageId ({culture}): {messageId}", _defaultCulture.Name, messageId);
|
||||
msg = messageId;
|
||||
}
|
||||
|
||||
@@ -41,8 +52,7 @@ namespace Robust.Shared.Localization
|
||||
return false;
|
||||
}
|
||||
|
||||
value = context.Format(node, null, null);
|
||||
return true;
|
||||
return DoFormat(messageId, out value, context, node);
|
||||
}
|
||||
|
||||
public string GetString(string messageId, params (string, object)[] args0)
|
||||
@@ -52,14 +62,15 @@ namespace Robust.Shared.Localization
|
||||
|
||||
if (!TryGetString(messageId, out var msg, args0))
|
||||
{
|
||||
Logger.WarningS("Loc", $"Unknown messageId ({_defaultCulture.IetfLanguageTag}): {messageId}");
|
||||
_logSawmill.Warning("Unknown messageId ({culture}): {messageId}", _defaultCulture.Name, messageId);
|
||||
msg = messageId;
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
public bool TryGetString(string messageId, [NotNullWhen(true)] out string? value, params (string, object)[] args0)
|
||||
public bool TryGetString(string messageId, [NotNullWhen(true)] out string? value,
|
||||
params (string, object)[] args0)
|
||||
{
|
||||
if (!TryGetNode(messageId, out var context, out var node))
|
||||
{
|
||||
@@ -70,11 +81,14 @@ namespace Robust.Shared.Localization
|
||||
var args = new Dictionary<string, object>();
|
||||
foreach (var (k, v) in args0)
|
||||
{
|
||||
args.Add(k, v);
|
||||
var val = v;
|
||||
if (v is ILocValue locVal)
|
||||
val = ValToFluent(locVal);
|
||||
|
||||
args.Add(k, val);
|
||||
}
|
||||
|
||||
value = context.Format(node, args, null);
|
||||
return false;
|
||||
return DoFormat(messageId, out value, context, node, args);
|
||||
}
|
||||
|
||||
private bool TryGetNode(
|
||||
@@ -109,7 +123,7 @@ namespace Robust.Shared.Localization
|
||||
|
||||
if (attribName != null)
|
||||
{
|
||||
if (!message.Attributes.TryGetValue(attribName, out var attrib))
|
||||
if (message.Attributes == null || !message.Attributes.TryGetValue(attribName, out var attrib))
|
||||
{
|
||||
node = null;
|
||||
return false;
|
||||
@@ -125,6 +139,105 @@ namespace Robust.Shared.Localization
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ReloadLocalizations()
|
||||
{
|
||||
foreach (var (culture, context) in _contexts.ToArray())
|
||||
{
|
||||
// Fluent.Net doesn't allow us to remove messages so...
|
||||
var newContext = new MessageContext(
|
||||
culture.Name,
|
||||
new MessageContextOptions
|
||||
{
|
||||
UseIsolating = false,
|
||||
Functions = context.Functions
|
||||
}
|
||||
);
|
||||
|
||||
_contexts[culture] = newContext;
|
||||
|
||||
_loadData(_res, culture, newContext);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddFunction(CultureInfo culture, string name, LocFunction function)
|
||||
{
|
||||
var context = _contexts[culture];
|
||||
|
||||
context.Functions.Add(name, (args, options) => CallFunction(function, args, options));
|
||||
}
|
||||
|
||||
private FluentType CallFunction(
|
||||
LocFunction function,
|
||||
IList<object> fluentArgs, IDictionary<string, object> fluentOptions)
|
||||
{
|
||||
var args = new ILocValue[fluentArgs.Count];
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
args[i] = ValFromFluent(fluentArgs[i]);
|
||||
}
|
||||
|
||||
var options = new Dictionary<string, ILocValue>(fluentOptions.Count);
|
||||
foreach (var (k, v) in fluentOptions)
|
||||
{
|
||||
options.Add(k, ValFromFluent(v));
|
||||
}
|
||||
|
||||
var argStruct = new LocArgs(args, options);
|
||||
|
||||
var ret = function(argStruct);
|
||||
|
||||
return ValToFluent(ret);
|
||||
}
|
||||
|
||||
private static ILocValue ValFromFluent(object arg)
|
||||
{
|
||||
return arg switch
|
||||
{
|
||||
FluentNone none => new LocValueNone(none.Value),
|
||||
FluentNumber number => new LocValueNumber(double.Parse(number.Value)),
|
||||
FluentString str => new LocValueString(str.Value),
|
||||
FluentDateTime dateTime =>
|
||||
new LocValueDateTime(DateTime.Parse(dateTime.Value, null, DateTimeStyles.RoundtripKind)),
|
||||
FluentLocWrapperType wrap => wrap.WrappedValue,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(arg))
|
||||
};
|
||||
}
|
||||
|
||||
private static FluentType ValToFluent(ILocValue arg)
|
||||
{
|
||||
return arg switch
|
||||
{
|
||||
LocValueNone =>
|
||||
throw new NotSupportedException("Cannot currently return LocValueNone from loc functions."),
|
||||
LocValueNumber number => new FluentNumber(number.Value.ToString("R")),
|
||||
LocValueString str => new FluentString(str.Value),
|
||||
LocValueDateTime dateTime => new FluentDateTime(dateTime.Value),
|
||||
_ => new FluentLocWrapperType(arg)
|
||||
};
|
||||
}
|
||||
|
||||
private bool DoFormat(string messageId, out string? value, MessageContext context, Node node, IDictionary<string, object>? args = null)
|
||||
{
|
||||
var errs = new List<FluentError>();
|
||||
try
|
||||
{
|
||||
value = context.Format(node, args, errs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logSawmill.Error("{culture}/{messageId}: {exception}", _defaultCulture!.Name, messageId, e);
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var err in errs)
|
||||
{
|
||||
_logSawmill.Error("{culture}/{messageId}: {error}", _defaultCulture!.Name, messageId, err);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remnants of the old Localization system.
|
||||
/// It exists to prevent source errors and allow existing game text to *mostly* work
|
||||
@@ -157,16 +270,23 @@ namespace Robust.Shared.Localization
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadCulture(IResourceManager resourceManager, CultureInfo culture)
|
||||
public void LoadCulture(CultureInfo culture)
|
||||
{
|
||||
var context = new MessageContext(
|
||||
culture.Name,
|
||||
new MessageContextOptions {UseIsolating = false}
|
||||
new MessageContextOptions
|
||||
{
|
||||
UseIsolating = false,
|
||||
// Have to pass empty dict here or else Fluent.Net will fuck up
|
||||
// and share the same dict between multiple message contexts.
|
||||
// Yes, you read that right.
|
||||
Functions = new Dictionary<string, Resolver.ExternalFunction>(),
|
||||
}
|
||||
);
|
||||
|
||||
_contexts.Add(culture, context);
|
||||
|
||||
_loadData(resourceManager, culture, context);
|
||||
_loadData(_res, culture, context);
|
||||
if (DefaultCulture == null)
|
||||
{
|
||||
DefaultCulture = culture;
|
||||
@@ -197,31 +317,51 @@ namespace Robust.Shared.Localization
|
||||
*/
|
||||
}
|
||||
|
||||
private static void _loadData(IResourceManager resourceManager, CultureInfo culture, MessageContext context)
|
||||
private void _loadData(IResourceManager resourceManager, CultureInfo culture, MessageContext context)
|
||||
{
|
||||
// Load data from .ftl files.
|
||||
// Data is loaded from /Locale/<language-code>/*
|
||||
|
||||
var root = new ResourcePath($"/Locale/{culture.IetfLanguageTag}/");
|
||||
var root = new ResourcePath($"/Locale/{culture.Name}/");
|
||||
|
||||
foreach (var file in resourceManager.ContentFindFiles(root))
|
||||
var files = resourceManager.ContentFindFiles(root).ToArray();
|
||||
|
||||
var resources = files.AsParallel().Select(path =>
|
||||
{
|
||||
var ftlFile = root / file;
|
||||
_loadFromFile(resourceManager, ftlFile, context);
|
||||
using var fileStream = resourceManager.ContentFileRead(path);
|
||||
using var reader = new StreamReader(fileStream, EncodingHelpers.UTF8);
|
||||
|
||||
var resource = FluentResource.FromReader(reader);
|
||||
return (path, resource);
|
||||
});
|
||||
|
||||
foreach (var (path, resource) in resources)
|
||||
{
|
||||
var errors = context.AddResource(resource);
|
||||
foreach (var error in errors)
|
||||
{
|
||||
_logSawmill.Warning("{path}: {exception}", path, error.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void _loadFromFile(IResourceManager resourceManager, ResourcePath filePath,
|
||||
MessageContext context)
|
||||
private sealed class FluentLocWrapperType : FluentType
|
||||
{
|
||||
using (var fileStream = resourceManager.ContentFileRead(filePath))
|
||||
using (var reader = new StreamReader(fileStream, EncodingHelpers.UTF8))
|
||||
public readonly ILocValue WrappedValue;
|
||||
|
||||
public FluentLocWrapperType(ILocValue wrappedValue)
|
||||
{
|
||||
var errors = context.AddMessages(reader);
|
||||
foreach (var error in errors)
|
||||
{
|
||||
Logger.WarningS("Loc", error.Message);
|
||||
}
|
||||
WrappedValue = wrappedValue;
|
||||
}
|
||||
|
||||
public override string Format(MessageContext ctx)
|
||||
{
|
||||
return WrappedValue.Format(new LocContext(ctx));
|
||||
}
|
||||
|
||||
public override bool Match(MessageContext ctx, object obj)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
using System.Globalization;
|
||||
using JetBrains.Annotations;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.UnitTesting.Shared.Localization
|
||||
{
|
||||
[TestFixture]
|
||||
internal sealed class LocalizationFunctionTest
|
||||
{
|
||||
private const string FluentCode = @"
|
||||
foo = { BAR($baz) }
|
||||
";
|
||||
|
||||
[Test]
|
||||
public void TestCustomTypes()
|
||||
{
|
||||
var ioc = new DependencyCollection();
|
||||
ioc.Register<ILocalizationManager, LocalizationManager>();
|
||||
ioc.Register<IResourceManager, ResourceManager>();
|
||||
ioc.Register<IResourceManagerInternal, ResourceManager>();
|
||||
ioc.Register<IConfigurationManager, ConfigurationManager>();
|
||||
ioc.RegisterLogs();
|
||||
ioc.BuildGraph();
|
||||
|
||||
ioc.Resolve<ILogManager>().RootSawmill.AddHandler(new ConsoleLogHandler());
|
||||
|
||||
var res = ioc.Resolve<IResourceManagerInternal>();
|
||||
res.MountString("/Locale/en-US/a.ftl", FluentCode);
|
||||
|
||||
var loc = ioc.Resolve<ILocalizationManager>();
|
||||
var culture = new CultureInfo("en-US", false);
|
||||
loc.LoadCulture(res, culture);
|
||||
|
||||
loc.AddFunction(culture, "BAR", Function);
|
||||
|
||||
var ret = loc.GetString("foo", ("baz", new LocValueVector2((-7, 5))));
|
||||
|
||||
Assert.That(ret, Is.EqualTo("5"));
|
||||
}
|
||||
|
||||
private static ILocValue Function(LocArgs args)
|
||||
{
|
||||
return new LocValueNumber(((LocValueVector2) args.Args[0]).Value.Y);
|
||||
}
|
||||
|
||||
private sealed record LocValueVector2(Vector2 Value) : LocValue<Vector2>(Value)
|
||||
{
|
||||
public override string Format(LocContext ctx)
|
||||
{
|
||||
return Value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user