Port to Linguini

This commit is contained in:
Matthias Mailänder
2022-04-22 22:12:13 +02:00
committed by Paul Chote
parent 9d8c2bb4c4
commit 834de4efbe
11 changed files with 206 additions and 153 deletions

View File

@@ -1366,12 +1366,12 @@ namespace OpenRA
return false;
}
public string Translate(string key, IDictionary<string, object> args = null, string attribute = null)
public string Translate(string key, IDictionary<string, object> args = null)
{
if (Translation.GetFormattedMessage(key, args, attribute) == key)
return modData.Translation.GetFormattedMessage(key, args, attribute);
if (Translation.TryGetString(key, out var message, args))
return message;
return Translation.GetFormattedMessage(key, args, attribute);
return modData.Translation.GetString(key, args);
}
}
}

View File

@@ -13,13 +13,12 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Fluent.Net;
using Linguini.Shared.Types.Bundle;
namespace OpenRA.Network
{
public class FluentArgument
{
[Flags]
public enum FluentArgumentType
{
String = 0,
@@ -41,18 +40,9 @@ namespace OpenRA.Network
static FluentArgumentType GetFluentArgumentType(object value)
{
switch (value)
switch (value.ToFluentType())
{
case byte _:
case sbyte _:
case short _:
case uint _:
case int _:
case long _:
case ulong _:
case float _:
case double _:
case decimal _:
case FluentNumber _:
return FluentArgumentType.Number;
default:
return FluentArgumentType.String;
@@ -64,10 +54,12 @@ namespace OpenRA.Network
{
public const int ProtocolVersion = 1;
public readonly string Key;
public readonly string Key = string.Empty;
[FieldLoader.LoadUsing(nameof(LoadArguments))]
public readonly FluentArgument[] Arguments;
public readonly FluentArgument[] Arguments = Array.Empty<FluentArgument>();
public string TranslatedText { get; }
static object LoadArguments(MiniYaml yaml)
{
@@ -84,30 +76,41 @@ namespace OpenRA.Network
return arguments.ToArray();
}
static readonly string[] SerializeFields = { "Key" };
public LocalizedMessage(MiniYaml yaml)
public LocalizedMessage(ModData modData, MiniYaml yaml)
{
// Let the FieldLoader do the dirty work of loading the public fields.
FieldLoader.Load(this, yaml);
var argumentDictionary = new Dictionary<string, object>();
foreach (var argument in Arguments)
{
if (argument.Type == FluentArgument.FluentArgumentType.Number)
{
if (!double.TryParse(argument.Value, out var number))
Log.Write("debug", $"Failed to parse {argument.Value}");
argumentDictionary.Add(argument.Key, number);
}
else
argumentDictionary.Add(argument.Key, argument.Value);
}
TranslatedText = modData.Translation.GetString(Key, argumentDictionary);
}
public LocalizedMessage(string key, Dictionary<string, object> arguments = null)
public static string Serialize(string key, Dictionary<string, object> arguments = null)
{
Key = key;
Arguments = arguments?.Select(a => new FluentArgument(a.Key, a.Value)).ToArray();
}
var root = new List<MiniYamlNode>
{
new MiniYamlNode("Protocol", ProtocolVersion.ToString()),
new MiniYamlNode("Key", key)
};
public string Serialize()
{
var root = new List<MiniYamlNode>() { new MiniYamlNode("Protocol", ProtocolVersion.ToString()) };
foreach (var field in SerializeFields)
root.Add(FieldSaver.SaveField(this, field));
if (Arguments != null)
if (arguments != null)
{
var argumentsNode = new MiniYaml("");
var i = 0;
foreach (var argument in Arguments)
foreach (var argument in arguments.Select(a => new FluentArgument(a.Key, a.Value)))
argumentsNode.Nodes.Add(new MiniYamlNode("Argument@" + i++, FieldSaver.Save(argument)));
root.Add(new MiniYamlNode("Arguments", argumentsNode));
@@ -117,19 +120,5 @@ namespace OpenRA.Network
.ToLines("LocalizedMessage")
.JoinWith("\n");
}
public string Translate(ModData modData)
{
var argumentDictionary = new Dictionary<string, object>();
foreach (var argument in Arguments)
{
if (argument.Type == FluentArgument.FluentArgumentType.Number)
argumentDictionary.Add(argument.Key, new FluentNumber(argument.Value));
else
argumentDictionary.Add(argument.Key, new FluentString(argument.Value));
}
return modData.Translation.GetFormattedMessage(Key, argumentDictionary);
}
}
}

View File

@@ -43,8 +43,8 @@ namespace OpenRA.Network
var yaml = MiniYaml.FromString(order.TargetString);
foreach (var node in yaml)
{
var localizedMessage = new LocalizedMessage(node.Value);
TextNotificationsManager.AddSystemLine(localizedMessage.Translate(Game.ModData));
var localizedMessage = new LocalizedMessage(Game.ModData, node.Value);
TextNotificationsManager.AddSystemLine(localizedMessage.TranslatedText);
}
break;

View File

@@ -7,7 +7,7 @@
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fluent.Net" Version="1.0.52" />
<PackageReference Include="Linguini.Bundle" Version="0.2.2" />
<PackageReference Include="OpenRA-Eluant" Version="1.0.18" />
<PackageReference Include="Mono.NAT" Version="3.0.3" />
<PackageReference Include="SharpZipLib" Version="1.3.3" />

View File

@@ -935,16 +935,16 @@ namespace OpenRA.Server
public void SendLocalizedMessage(string key, Dictionary<string, object> arguments = null)
{
var text = new LocalizedMessage(key, arguments).Serialize();
var text = LocalizedMessage.Serialize(key, arguments);
DispatchServerOrdersToClients(Order.FromTargetString("LocalizedMessage", text, true));
if (Type == ServerType.Dedicated)
WriteLineWithTimeStamp(ModData.Translation.GetFormattedMessage(key, arguments));
WriteLineWithTimeStamp(ModData.Translation.GetString(key, arguments));
}
public void SendLocalizedMessageTo(Connection conn, string key, Dictionary<string, object> arguments = null)
{
var text = new LocalizedMessage(key, arguments).Serialize();
var text = LocalizedMessage.Serialize(key, arguments);
DispatchOrdersToClient(conn, 0, 0, Order.FromTargetString("LocalizedMessage", text, true).Serialize());
}
@@ -1287,7 +1287,7 @@ namespace OpenRA.Server
{
lock (LobbyInfo)
{
WriteLineWithTimeStamp(ModData.Translation.GetFormattedMessage(GameStarted));
WriteLineWithTimeStamp(ModData.Translation.GetString(GameStarted));
// Drop any players who are not ready
foreach (var c in Conns.Where(c => !c.Validated || GetClient(c).IsInvalid).ToArray())

View File

@@ -11,10 +11,13 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Fluent.Net;
using Fluent.Net.RuntimeAst;
using Linguini.Bundle;
using Linguini.Bundle.Builder;
using Linguini.Shared.Types.Bundle;
using Linguini.Syntax.Parser;
using OpenRA.FileSystem;
namespace OpenRA
@@ -34,82 +37,87 @@ namespace OpenRA
public class Translation
{
readonly IEnumerable<MessageContext> messageContexts;
readonly FluentBundle bundle;
public Translation(string language, string[] translations, IReadOnlyFileSystem fileSystem)
{
if (translations == null || translations.Length == 0)
return;
messageContexts = GetMessageContext(language, translations, fileSystem).ToList();
bundle = LinguiniBuilder.Builder()
.CultureInfo(CultureInfo.InvariantCulture)
.SkipResources()
.SetUseIsolating(false)
.UseConcurrent()
.UncheckedBuild();
ParseTranslations(language, translations, fileSystem);
}
static IEnumerable<MessageContext> GetMessageContext(string language, string[] translations, IReadOnlyFileSystem fileSystem)
void ParseTranslations(string language, string[] translations, IReadOnlyFileSystem fileSystem)
{
var backfall = translations.Where(t => t.EndsWith("en.ftl"));
var paths = translations.Where(t => t.EndsWith(language + ".ftl"));
foreach (var path in paths.Concat(backfall))
// Always load english strings to provide a fallback for missing translations.
// It is important to load the english files first so the chosen language's files can override them.
var paths = translations.Where(t => t.EndsWith("en.ftl")).ToHashSet();
foreach (var t in translations)
if (t.EndsWith($"{language}.ftl"))
paths.Add(t);
foreach (var path in paths)
{
var stream = fileSystem.Open(path);
using (var reader = new StreamReader(stream))
{
var options = new MessageContextOptions { UseIsolating = false };
var messageContext = new MessageContext(language, options);
var errors = messageContext.AddMessages(reader);
foreach (var error in errors)
var parser = new LinguiniParser(reader);
var resource = parser.Parse();
foreach (var error in resource.Errors)
Log.Write("debug", error.ToString());
yield return messageContext;
bundle.AddResourceOverriding(resource);
}
}
}
public string GetFormattedMessage(string key, IDictionary<string, object> args = null, string attribute = null)
public string GetString(string key, IDictionary<string, object> arguments = null)
{
if (key == null)
return "";
if (!TryGetString(key, out var message, arguments))
message = key;
foreach (var messageContext in messageContexts)
return message;
}
public bool TryGetString(string key, out string value, IDictionary<string, object> arguments = null)
{
if (!HasMessage(key))
{
var message = messageContext.GetMessage(key);
if (message != null)
{
if (string.IsNullOrEmpty(attribute))
return messageContext.Format(message, args);
else
return messageContext.Format(message.Attributes[attribute], args);
}
value = null;
return false;
}
return key;
}
var fluentArguments = new Dictionary<string, IFluentType>();
if (arguments != null)
foreach (var (k, v) in arguments)
fluentArguments.Add(k, v.ToFluentType());
public bool HasAttribute(string key)
{
foreach (var messageContext in messageContexts)
if (messageContext.HasMessage(key))
return true;
return false;
}
public string GetAttribute(string key, string attribute)
{
if (key == null)
return "";
foreach (var messageContext in messageContexts)
try
{
var message = messageContext.GetMessage(key);
if (message != null && message.Attributes != null && message.Attributes.ContainsKey(attribute))
{
var node = message.Attributes[attribute];
var stringLiteral = (StringLiteral)node;
return stringLiteral.Value;
}
}
var result = bundle.TryGetAttrMsg(key, fluentArguments, out var errors, out value);
foreach (var error in errors)
Log.Write("debug", $"Translation of {key}: {error}");
return "";
return result;
}
catch (Exception e)
{
Log.Write("debug", $"Translation of {key}: {e}");
value = null;
return false;
}
}
public bool HasMessage(string key)
{
return bundle.HasMessage(key);
}
// Adapted from Fluent.Net.SimpleExample.TranslationService by Mark Weaver
@@ -125,10 +133,8 @@ namespace OpenRA
{
name = args[i] as string;
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException($"Expected the argument at index {i} to be a non-empty string",
nameof(args));
}
value = args[i + 1];
if (value == null)

View File

@@ -0,0 +1,45 @@
#region Copyright & License Information
/*
* Copyright 2007-2022 The OpenRA Developers (see AUTHORS)
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using Linguini.Shared.Types.Bundle;
namespace OpenRA
{
public static class TranslationExts
{
public static IFluentType ToFluentType(this object value)
{
switch (value)
{
case byte number:
return (FluentNumber)number;
case sbyte number:
return (FluentNumber)number;
case short number:
return (FluentNumber)number;
case uint number:
return (FluentNumber)number;
case int number:
return (FluentNumber)number;
case long number:
return (FluentNumber)number;
case ulong number:
return (FluentNumber)number;
case float number:
return (FluentNumber)number;
case double number:
return (FluentNumber)number;
default:
return (FluentString)value.ToString();
}
}
}
}