mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 11:40:52 +01:00
476 lines
16 KiB
C#
476 lines
16 KiB
C#
using System;
|
|
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.GameObjects;
|
|
using Robust.Shared.GameObjects.Components.Localization;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, 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 bool TryGetEntityLocAttrib(IEntity entity, string attribute, [NotNullWhen(true)] out string? value)
|
|
{
|
|
var attributeSource = "";
|
|
|
|
if (entity.TryGetComponent<GrammarComponent>(out var grammar) && !string.IsNullOrEmpty(grammar.LocalizationId))
|
|
{
|
|
attributeSource = grammar.LocalizationId;
|
|
}
|
|
else if(!string.IsNullOrEmpty(entity.Prototype?.LocalizationID))
|
|
{
|
|
attributeSource = entity.Prototype.LocalizationID;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(attributeSource))
|
|
{
|
|
if (TryGetString($"{attributeSource}.{attribute}", out value))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
value = null;
|
|
return false;
|
|
}
|
|
|
|
void AddBuiltinFunctions(MessageContext context)
|
|
{
|
|
//Grammatical gender
|
|
context.Functions.Add("GENDER", (args, options) => CallFunction((args) =>
|
|
{
|
|
if (args.Args.Count < 1) return new LocValueString("other");
|
|
|
|
ILocValue entity0 = args.Args[0];
|
|
if (entity0.Value != null)
|
|
{
|
|
IEntity entity = (IEntity)entity0.Value;
|
|
|
|
if(entity.TryGetComponent<GrammarComponent>(out var grammar) && grammar.Gender.HasValue)
|
|
{
|
|
return new LocValueString(grammar.Gender.Value.ToString().ToLowerInvariant());
|
|
}
|
|
|
|
if(TryGetEntityLocAttrib(entity, "gender", out var gender))
|
|
{
|
|
return new LocValueString(gender);
|
|
}
|
|
}
|
|
|
|
return new LocValueString("other");
|
|
}, args, options));
|
|
|
|
//Proper nouns
|
|
context.Functions.Add("PROPER", (args, options) => CallFunction((args) =>
|
|
{
|
|
if (args.Args.Count < 1) return new LocValueString("other");
|
|
|
|
ILocValue entity0 = args.Args[0];
|
|
if (entity0.Value != null)
|
|
{
|
|
IEntity entity = (IEntity)entity0.Value;
|
|
|
|
if (entity.TryGetComponent<GrammarComponent>(out var grammar) && grammar.ProperNoun.HasValue)
|
|
{
|
|
return new LocValueString(grammar.ProperNoun.Value.ToString().ToLowerInvariant());
|
|
}
|
|
|
|
if (TryGetEntityLocAttrib(entity, "proper", out var proper))
|
|
{
|
|
return new LocValueString(proper);
|
|
}
|
|
}
|
|
|
|
return new LocValueString("other");
|
|
}, args, options));
|
|
|
|
//Misc Attribs
|
|
context.Functions.Add("ATTRIB", (args, options) => CallFunction((args) =>
|
|
{
|
|
if(args.Args.Count < 2) return new LocValueString("other");
|
|
|
|
ILocValue entity0 = args.Args[0];
|
|
if (entity0.Value != null)
|
|
{
|
|
IEntity entity = (IEntity)entity0.Value;
|
|
ILocValue attrib0 = args.Args[1];
|
|
if (TryGetEntityLocAttrib(entity, attrib0.Format(new LocContext(context)), out var attrib))
|
|
{
|
|
return new LocValueString(attrib);
|
|
}
|
|
}
|
|
|
|
return new LocValueString("other");
|
|
}, args, options));
|
|
}
|
|
|
|
public string GetString(string messageId)
|
|
{
|
|
if (_defaultCulture == null)
|
|
return messageId;
|
|
|
|
if (!TryGetString(messageId, out var msg))
|
|
{
|
|
_logSawmill.Warning("Unknown messageId ({culture}): {messageId}", _defaultCulture.Name, messageId);
|
|
msg = messageId;
|
|
}
|
|
|
|
return msg;
|
|
}
|
|
|
|
public bool TryGetString(string messageId, [NotNullWhen(true)] out string? value)
|
|
{
|
|
if (!TryGetNode(messageId, out var context, out var node))
|
|
{
|
|
value = null;
|
|
return false;
|
|
}
|
|
|
|
return DoFormat(messageId, out value, context, node);
|
|
}
|
|
|
|
public string GetString(string messageId, params (string, object)[] args0)
|
|
{
|
|
if (_defaultCulture == null)
|
|
return messageId;
|
|
|
|
if (!TryGetString(messageId, out var msg, args0))
|
|
{
|
|
_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)
|
|
{
|
|
if (!TryGetNode(messageId, out var context, out var node))
|
|
{
|
|
value = null;
|
|
return false;
|
|
}
|
|
|
|
var args = new Dictionary<string, object>();
|
|
foreach (var (k, v) in args0)
|
|
{
|
|
var val = v switch
|
|
{
|
|
IEntity entity => new LocValueEntity(entity),
|
|
bool or Enum => v.ToString()!.ToLowerInvariant(),
|
|
_ => v,
|
|
};
|
|
|
|
if (val is ILocValue locVal)
|
|
val = new FluentLocWrapperType(locVal);
|
|
|
|
args.Add(k, val);
|
|
}
|
|
|
|
return DoFormat(messageId, out value, context, node, args);
|
|
}
|
|
|
|
private bool TryGetNode(
|
|
string messageId,
|
|
[NotNullWhen(true)] out MessageContext? ctx,
|
|
[NotNullWhen(true)] out Node? node)
|
|
{
|
|
if (_defaultCulture == null)
|
|
{
|
|
ctx = null;
|
|
node = null;
|
|
return false;
|
|
}
|
|
|
|
ctx = _contexts[_defaultCulture];
|
|
string? attribName = null;
|
|
|
|
if (messageId.Contains('.'))
|
|
{
|
|
var split = messageId.Split('.');
|
|
messageId = split[0];
|
|
attribName = split[1];
|
|
}
|
|
|
|
var message = ctx.GetMessage(messageId);
|
|
|
|
if (message == null)
|
|
{
|
|
node = null;
|
|
return false;
|
|
}
|
|
|
|
if (attribName != null)
|
|
{
|
|
if (message.Attributes == null || !message.Attributes.TryGetValue(attribName, out var attrib))
|
|
{
|
|
node = null;
|
|
return false;
|
|
}
|
|
|
|
node = attrib;
|
|
}
|
|
else
|
|
{
|
|
node = message;
|
|
}
|
|
|
|
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
|
|
/// </summary>
|
|
[Obsolete]
|
|
[StringFormatMethod("text")]
|
|
public string GetString(string text, params object[] args)
|
|
{
|
|
return string.Format(text, args);
|
|
}
|
|
|
|
public CultureInfo? DefaultCulture
|
|
{
|
|
get => _defaultCulture;
|
|
set
|
|
{
|
|
if (value == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(value));
|
|
}
|
|
|
|
if (!_contexts.ContainsKey(value))
|
|
{
|
|
throw new ArgumentException("That culture is not yet loaded and cannot be used.", nameof(value));
|
|
}
|
|
|
|
_defaultCulture = value;
|
|
CultureInfo.CurrentCulture = value;
|
|
CultureInfo.CurrentUICulture = value;
|
|
}
|
|
}
|
|
|
|
public void LoadCulture(CultureInfo culture)
|
|
{
|
|
var context = new MessageContext(
|
|
culture.Name,
|
|
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>(),
|
|
}
|
|
);
|
|
AddBuiltinFunctions(context);
|
|
|
|
_contexts.Add(culture, context);
|
|
|
|
_loadData(_res, culture, context);
|
|
if (DefaultCulture == null)
|
|
{
|
|
DefaultCulture = culture;
|
|
}
|
|
}
|
|
|
|
public void AddLoadedToStringSerializer(IRobustMappedStringSerializer serializer)
|
|
{
|
|
/*
|
|
* TODO: need to expose Messages on MessageContext in Fluent.NET
|
|
serializer.AddStrings(StringIterator());
|
|
|
|
IEnumerable<string> StringIterator()
|
|
{
|
|
foreach (var context in _contexts.Values)
|
|
{
|
|
foreach (var (key, translations) in _context)
|
|
{
|
|
yield return key;
|
|
|
|
foreach (var t in translations)
|
|
{
|
|
yield return t;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
}
|
|
|
|
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.Name}/");
|
|
|
|
var files = resourceManager.ContentFindFiles(root)
|
|
.Where(c => c.Filename.EndsWith(".ftl", StringComparison.InvariantCultureIgnoreCase))
|
|
.ToArray();
|
|
|
|
var resources = files.AsParallel().Select(path =>
|
|
{
|
|
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 sealed class FluentLocWrapperType : FluentType
|
|
{
|
|
public readonly ILocValue WrappedValue;
|
|
|
|
public FluentLocWrapperType(ILocValue wrappedValue)
|
|
{
|
|
WrappedValue = wrappedValue;
|
|
}
|
|
|
|
public override string Format(MessageContext ctx)
|
|
{
|
|
return WrappedValue.Format(new LocContext(ctx));
|
|
}
|
|
|
|
public override bool Match(MessageContext ctx, object obj)
|
|
{
|
|
return false;
|
|
/*var strVal = obj is IFluentType ft ? ft.Value : obj.ToString() ?? "";
|
|
return WrappedValue.Matches(new LocContext(ctx), strVal);*/
|
|
}
|
|
}
|
|
}
|
|
}
|