Fluent Localization (#1584)

This commit is contained in:
Remie Richards
2021-02-21 23:36:02 +00:00
committed by GitHub
parent a3190a8aca
commit 460cf57d7c
22 changed files with 144 additions and 1086 deletions

View File

@@ -574,29 +574,22 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
- name: NGetText
- name: Fluent.Net
license: |
The MIT License (MIT)
blushingpenguin and Contributors
Copyright (c) 2012 Vitaly Zilnik
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
http://www.apache.org/licenses/LICENSE-2.0
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Unless required by applicable law or agreed to in writing, software distributed
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
- name: NetSerializer
license: |

View File

@@ -0,0 +1,11 @@
namespace Robust.Shared.Enums
{
public enum Gender : byte
{
Epicene,
Female,
Male,
Neuter,
}
}

View File

@@ -1,61 +0,0 @@
using System;
using Robust.Shared.Localization.Macros;
using Robust.Shared.Players;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.GameObjects
{
/// <summary>
/// Holds the necessary information to generate text related to the entity.
/// </summary>
[RegisterComponent]
public class GrammarComponent: Component, IProperNamable
{
public sealed override string Name => "Grammar";
public sealed override uint? NetID => NetIDs.GRAMMAR;
private bool _proper;
[ViewVariables(VVAccess.ReadWrite)]
public bool Proper
{
get => _proper;
set
{
_proper = value;
Dirty();
}
}
public override void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(this, x => x.Proper, "proper", false);
}
public override ComponentState GetComponentState(ICommonSession player)
{
return new GrammarComponentState(Proper);
}
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
if (!(curState is GrammarComponentState cast))
return;
Proper = cast.Proper;
}
[Serializable, NetSerializable]
protected sealed class GrammarComponentState : ComponentState
{
public GrammarComponentState(bool proper) : base(NetIDs.GRAMMAR)
{
Proper = proper;
}
public bool Proper { get; }
}
}
}

View File

@@ -1,4 +1,4 @@
namespace Robust.Shared.GameObjects
namespace Robust.Shared.GameObjects
{
public static class NetIDs
{
@@ -14,7 +14,6 @@
public const uint USERINTERFACE = 24;
public const uint CONTAINER_MANAGER = 25;
public const uint OCCLUDER = 26;
public const uint GRAMMAR = 27;
public const uint EYE = 28;
}
}

View File

@@ -1,73 +0,0 @@
using System;
using System.Globalization;
using System.IO;
using NGettext;
using NGettext.Loaders;
namespace Robust.Shared.Localization
{
/// <summary>
/// Catalog with custom IFormatProvider
/// </summary>
public class CustomFormatCatalog : Catalog
{
public IFormatProvider? CustomFormatProvider;
public CustomFormatCatalog()
: base()
{ }
public CustomFormatCatalog(CultureInfo cultureInfo)
: base(cultureInfo)
{ }
public CustomFormatCatalog(ILoader loader)
: base(loader)
{ }
public CustomFormatCatalog(Stream moStream)
: base(moStream)
{ }
public CustomFormatCatalog(ILoader loader, CultureInfo cultureInfo)
: base(loader, cultureInfo)
{ }
public CustomFormatCatalog(Stream moStream, CultureInfo cultureInfo)
: base(moStream, cultureInfo)
{ }
public CustomFormatCatalog(string domain, string localeDir)
: base(domain, localeDir)
{ }
public CustomFormatCatalog(string domain, string localeDir, CultureInfo cultureInfo)
: base(domain, localeDir, cultureInfo)
{ }
public override string GetString(string text, params object[] args)
{
return string.Format(GetFormatProviderOrDefault(), GetStringDefault(text, text), args);
}
public override string GetPluralString(string text, string pluralText, long n, params object[] args)
{
return string.Format(GetFormatProviderOrDefault(), GetPluralStringDefault(text, text, pluralText, n), args);
}
public override string GetParticularString(string context, string text, params object[] args)
{
return string.Format(GetFormatProviderOrDefault(), GetStringDefault(context + CONTEXT_GLUE + text, text), args);
}
public override string GetParticularPluralString(string context, string text, string pluralText, long n, params object[] args)
{
return string.Format(GetFormatProviderOrDefault(), GetPluralStringDefault(context + CONTEXT_GLUE + text, text, pluralText, n), args);
}
private IFormatProvider GetFormatProviderOrDefault()
{
return CustomFormatProvider ?? CultureInfo;
}
}
}

View File

@@ -1,20 +1,21 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using JetBrains.Annotations;
using Robust.Shared.ContentPack;
using Robust.Shared.Localization.Macros;
using Robust.Shared.Serialization;
namespace Robust.Shared.Localization
{
// ReSharper disable once CommentTypo
/// <summary>
/// Provides facilities to automatically translate in-game text.
/// Provides facilities to obtain language appropriate in-game text.
/// </summary>
/// <remarks>
/// <para>
/// The translation API is similar to GNU gettext.
/// You pass a string through it (most often the English version),
/// and when the game is ran in another language with adequate translation, the translation is returned instead.
/// Translation is handled using Project Fluent (https://www.projectfluent.org/)
/// You pass a Fluent 'identifier' as a string and the localization manager will fetch the message
/// matching that identifier from the currently loaded language's Fluent files.
/// </para>
/// </remarks>
/// <seealso cref="Loc"/>
@@ -22,40 +23,18 @@ namespace Robust.Shared.Localization
public interface ILocalizationManager
{
/// <summary>
/// Gets a string translated for the current culture.
/// Gets a language approrpiate string represented by the supplied messageId.
/// </summary>
/// <param name="text">The string to get translated.</param>
/// <param name="messageId">Unique Identifier for a translated message.</param>
/// <returns>
/// The translated string if a translation is available, otherwise the string is returned.
/// The language appropriate message if available, otherwise the messageId is returned.
/// </returns>
string GetString(string text);
string GetString(string messageId);
/// <summary>
/// Version of <see cref="GetString(string)"/> that also runs string formatting.
/// Version of <see cref="GetString(string)"/> that supports arguments.
/// </summary>
[StringFormatMethod("text")]
string GetString(string text, params object[] args);
/// <summary>
/// Gets a string inside a context or category.
/// </summary>
string GetParticularString(string context, string text);
/// <summary>
/// Gets a string inside a context or category with formatting.
/// </summary>
[StringFormatMethod("text")]
string GetParticularString(string context, string text, params object[] args);
string GetPluralString(string text, string pluralText, long n);
[StringFormatMethod("pluralText")]
string GetPluralString(string text, string pluralText, long n, params object[] args);
string GetParticularPluralString(string context, string text, string pluralText, long n);
[StringFormatMethod("pluralText")]
string GetParticularPluralString(string context, string text, string pluralText, long n, params object[] args);
string GetString(string messageId, params (string, object)[] args);
/// <summary>
/// Default culture used by other methods when no culture is explicitly specified.
@@ -67,9 +46,19 @@ namespace Robust.Shared.Localization
/// Load data for a culture.
/// </summary>
/// <param name="resourceManager"></param>
/// <param name="textMacroFactory"></param>
/// <param name="culture"></param>
void LoadCulture(IResourceManager resourceManager, ITextMacroFactory textMacroFactory, CultureInfo culture);
void LoadCulture(IResourceManager resourceManager, CultureInfo culture);
/// <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")]
string GetString(string text, params object[] args);
}
internal interface ILocalizationManagerInternal : ILocalizationManager

View File

@@ -1,8 +1,9 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using JetBrains.Annotations;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Localization.Macros;
namespace Robust.Shared.Localization
{
@@ -24,66 +25,23 @@ namespace Robust.Shared.Localization
private static ILocalizationManager LocalizationManager => IoCManager.Resolve<ILocalizationManager>();
/// <summary>
/// Gets a string translated for the current culture.
/// Gets a language approrpiate string represented by the supplied messageId.
/// </summary>
/// <param name="text">The string to get translated.</param>
/// <param name="messageId">Unique Identifier for a translated message.</param>
/// <returns>
/// The translated string if a translation is available, otherwise the string is returned.
/// The language appropriate message if available, otherwise the messageId is returned.
/// </returns>
public static string GetString(string text)
public static string GetString(string messageId)
{
return LocalizationManager.GetString(text);
return LocalizationManager.GetString(messageId);
}
/// <summary>
/// Version of <see cref="GetString(string)"/> that also runs string formatting.
/// Version of <see cref="GetString(string)"/> that supports arguments.
/// </summary>
[StringFormatMethod("text")]
public static string GetString(string text, params object[] args)
public static string GetString(string messageId, params (string,object)[] args)
{
return LocalizationManager.GetString(text, args);
}
/// <summary>
/// Gets a string inside a context or category.
/// </summary>
public static string GetParticularString(string context, string text)
{
return LocalizationManager.GetParticularString(context, text);
}
/// <summary>
/// Gets a string inside a context or category with formatting.
/// </summary>
[StringFormatMethod("text")]
public static string GetParticularString(string context, string text, params object[] args)
{
return LocalizationManager.GetParticularString(context, text, args);
}
public static string GetPluralString(string text, string pluralText, long n)
{
return LocalizationManager.GetPluralString(text, pluralText, n);
}
[StringFormatMethod("pluralText")]
public static string GetPluralString(string text, string pluralText, long n, params object[] args)
{
return LocalizationManager.GetPluralString(text, pluralText, n, args);
}
public static string GetParticularPluralString(string context, string text, string pluralText, long n)
{
return LocalizationManager.GetParticularString(context, text, pluralText, n);
}
[StringFormatMethod("pluralText")]
public static string GetParticularPluralString(string context, string text, string pluralText, long n,
params object[] args)
{
return LocalizationManager.GetParticularString(context, text, pluralText, n, args);
return LocalizationManager.GetString(messageId, args);
}
/// <summary>
@@ -92,9 +50,21 @@ namespace Robust.Shared.Localization
/// <param name="resourceManager"></param>
/// <param name="macroFactory"></param>
/// <param name="culture"></param>
public static void LoadCulture(IResourceManager resourceManager, ITextMacroFactory macroFactory, CultureInfo culture)
public static void LoadCulture(IResourceManager resourceManager, CultureInfo culture)
{
LocalizationManager.LoadCulture(resourceManager, macroFactory, culture);
LocalizationManager.LoadCulture(resourceManager, culture);
}
/// <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 static string GetString(string text, params object[] args)
{
return LocalizationManager.GetString(text, args);
}
}
}

View File

@@ -2,99 +2,71 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using NGettext;
using Fluent.Net;
using JetBrains.Annotations;
using Robust.Shared.ContentPack;
using Robust.Shared.Localization.Macros;
using Robust.Shared.Log;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
namespace Robust.Shared.Localization
{
internal sealed class LocalizationManager : ILocalizationManagerInternal
{
private readonly Dictionary<CultureInfo, Catalog> _catalogs = new();
private readonly Dictionary<CultureInfo, MessageContext> _contexts = new();
private CultureInfo? _defaultCulture;
public string GetString(string text)
public string GetString(string messageId)
{
if (_defaultCulture == null)
{
return text;
return messageId;
}
var catalog = _catalogs[_defaultCulture];
return catalog.GetString(text);
var context = _contexts[_defaultCulture];
var message = context.GetMessage(messageId);
if (message == null)
{
Logger.WarningS("Loc", $"Unknown messageId ({_defaultCulture.IetfLanguageTag}): {messageId}");
return messageId;
}
return context.Format(message, null, null);
}
public string GetString(string messageId, params (string, object)[] args0)
{
if (_defaultCulture == null)
{
return messageId;
}
var context = _contexts[_defaultCulture];
var message = context.GetMessage(messageId);
var args = new Dictionary<string, object>();
foreach (var vari in args0)
{
args.Add(vari.Item1, vari.Item2);
}
if (message == null)
{
Logger.WarningS("Loc", $"Unknown messageId ({_defaultCulture.IetfLanguageTag}): {messageId}");
return messageId;
}
return context.Format(message, args, null);
}
/// <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)
{
if (_defaultCulture == null)
{
return string.Format(text, args);
}
var catalog = _catalogs[_defaultCulture];
return catalog.GetString(text, args);
}
public string GetParticularString(string context, string text)
{
if (_defaultCulture == null)
{
return text;
}
var catalog = _catalogs[_defaultCulture];
return catalog.GetParticularString(context, text);
}
public string GetParticularString(string context, string text, params object[] args)
{
if (_defaultCulture == null)
{
return string.Format(text, args);
}
var catalog = _catalogs[_defaultCulture];
return catalog.GetParticularString(context, text, args);
}
public string GetPluralString(string text, string pluralText, long n)
{
if (_defaultCulture == null)
{
return n == 1 ? text : pluralText;
}
var catalog = _catalogs[_defaultCulture];
return catalog.GetPluralString(text, pluralText, n);
}
public string GetPluralString(string text, string pluralText, long n, params object[] args)
{
if (_defaultCulture == null)
{
return string.Format(n == 1 ? text : pluralText, args);
}
var catalog = _catalogs[_defaultCulture];
return catalog.GetPluralString(text, pluralText, n, args);
}
public string GetParticularPluralString(string context, string text, string pluralText, long n)
{
if (_defaultCulture == null)
{
return n == 1 ? text : pluralText;
}
var catalog = _catalogs[_defaultCulture];
return catalog.GetParticularPluralString(context, text, pluralText, n, pluralText);
}
public string GetParticularPluralString(string context, string text, string pluralText, long n, params object[] args)
{
if (_defaultCulture == null)
{
return string.Format(n == 1 ? text : pluralText, args);
}
var catalog = _catalogs[_defaultCulture];
return catalog.GetParticularPluralString(context, text, pluralText, n, args);
return string.Format(text, args);
}
public CultureInfo? DefaultCulture
@@ -107,7 +79,7 @@ namespace Robust.Shared.Localization
throw new ArgumentNullException(nameof(value));
}
if (!_catalogs.ContainsKey(value))
if (!_contexts.ContainsKey(value))
{
throw new ArgumentException("That culture is not yet loaded and cannot be used.", nameof(value));
}
@@ -118,13 +90,16 @@ namespace Robust.Shared.Localization
}
}
public void LoadCulture(IResourceManager resourceManager, ITextMacroFactory textMacroFactory, CultureInfo culture)
public void LoadCulture(IResourceManager resourceManager, CultureInfo culture)
{
var catalog = new CustomFormatCatalog(culture);
_catalogs.Add(culture, catalog);
var context = new MessageContext(
culture.Name,
new MessageContextOptions { UseIsolating = false }
);
_loadData(resourceManager, culture, catalog);
_loadMacros(textMacroFactory, culture, catalog);
_contexts.Add(culture, context);
_loadData(resourceManager, culture, context);
if (DefaultCulture == null)
{
DefaultCulture = culture;
@@ -133,13 +108,15 @@ namespace Robust.Shared.Localization
public void AddLoadedToStringSerializer(IRobustMappedStringSerializer serializer)
{
/*
* TODO: need to expose Messages on MessageContext in Fluent.NET
serializer.AddStrings(StringIterator());
IEnumerable<string> StringIterator()
{
foreach (var catalog in _catalogs.Values)
foreach (var context in _contexts.Values)
{
foreach (var (key, translations) in catalog.Translations)
foreach (var (key, translations) in _context)
{
yield return key;
@@ -150,65 +127,34 @@ namespace Robust.Shared.Localization
}
}
}
*/
}
private static void _loadData(IResourceManager resourceManager, CultureInfo culture, Catalog catalog)
private static void _loadData(IResourceManager resourceManager, CultureInfo culture, MessageContext context)
{
// Load data from .yml files.
// Load data from .ftl files.
// Data is loaded from /Locale/<language-code>/*
var root = new ResourcePath($"/Locale/{culture.IetfLanguageTag}/");
foreach (var file in resourceManager.ContentFindFiles(root))
{
var yamlFile = root / file;
_loadFromFile(resourceManager, yamlFile, catalog);
var ftlFile = root / file;
_loadFromFile(resourceManager, ftlFile, context);
}
}
private static void _loadFromFile(IResourceManager resourceManager, ResourcePath filePath, Catalog catalog)
private static void _loadFromFile(IResourceManager resourceManager, ResourcePath filePath, MessageContext context)
{
var yamlStream = new YamlStream();
using (var fileStream = resourceManager.ContentFileRead(filePath))
using (var reader = new StreamReader(fileStream, EncodingHelpers.UTF8))
{
yamlStream.Load(reader);
var errors = context.AddMessages(reader);
foreach (var error in errors)
{
Logger.WarningS("Loc", error.Message);
}
}
foreach (var entry in yamlStream.Documents
.SelectMany(d => (YamlSequenceNode) d.RootNode)
.Cast<YamlMappingNode>())
{
_readEntry(entry, catalog);
}
}
private static void _readEntry(YamlMappingNode entry, Catalog catalog)
{
var id = entry.GetNode("msgid").AsString();
var str = entry.GetNode("msgstr");
string[] strings;
if (str is YamlScalarNode scalar)
{
strings = new[] {scalar.AsString()};
}
else if (str is YamlSequenceNode sequence)
{
strings = sequence.Children.Select(c => c.AsString()).ToArray();
}
else
{
// TODO: Improve error reporting here.
throw new Exception("Invalid format in translation file.");
}
catalog.Translations.Add(id, strings);
}
private static void _loadMacros(ITextMacroFactory textMacroFactory, CultureInfo culture, CustomFormatCatalog catalog)
{
var macros = textMacroFactory.GetMacrosForLanguage(culture.IetfLanguageTag);
catalog.CustomFormatProvider = new MacroFormatProvider(new MacroFormatter(macros), culture);
}
}
}

View File

@@ -1,53 +0,0 @@
using System.Linq;
using Robust.Shared.GameObjects;
namespace Robust.Shared.Localization.Macros
{
/// <summary>
/// Genders for grammatical usage only.
/// </summary>
public enum Gender : byte
{
Epicene,
Female,
Male,
Neuter,
}
public interface IGenderable
{
public Gender Gender => Gender.Epicene;
/// <summary>
/// Helper function to get the gender of an object, or Epicene if the object is not IGenderable
/// </summary>
public static Gender GetGenderOrEpicene(object? argument)
{
// FIXME The Entity special case is not really good
if (argument is IEntity entity)
{
// FIXME And this is not really better.
return Enumerable.FirstOrDefault(entity.GetAllComponents<IGenderable>())?.Gender ?? Gender.Epicene;
}
else
return (argument as IGenderable)?.Gender ?? Gender.Epicene;
}
}
public interface IProperNamable
{
public bool Proper => false;
public static bool GetProperOrFalse(object? argument)
{
// FIXME The Entity special case is not really good
if (argument is IEntity entity)
{
// FIXME And this is not really better.
return entity.GetAllComponents<IProperNamable>().FirstOrDefault()?.Proper ?? false;
}
return (argument as IProperNamable)?.Proper ?? false;
}
}
}

View File

@@ -1,136 +0,0 @@
namespace Robust.Shared.Localization.Macros.English
{
[RegisterTextMacro("they", "en")]
public class They : ITextMacro
{
public string Format(object? argument)
{
return IGenderable.GetGenderOrEpicene(argument) switch
{
Gender.Female => "she",
Gender.Male => "he",
Gender.Neuter => "it",
_ => "they",
};
}
}
[RegisterTextMacro("their", "en")]
public class Their : ITextMacro
{
public string Format(object? argument)
{
return IGenderable.GetGenderOrEpicene(argument) switch
{
Gender.Female => "her",
Gender.Male => "his",
Gender.Neuter => "its",
_ => "their",
};
}
}
[RegisterTextMacro("theirs", "en")]
public class Theirs : ITextMacro
{
public string Format(object? argument)
{
return IGenderable.GetGenderOrEpicene(argument) switch
{
Gender.Female => "hers",
Gender.Male => "his",
Gender.Neuter => "its",
_ => "theirs",
};
}
}
[RegisterTextMacro("them", "en")]
public class Them : ITextMacro
{
public string Format(object? argument)
{
return IGenderable.GetGenderOrEpicene(argument) switch
{
Gender.Female => "her",
Gender.Male => "him",
Gender.Neuter => "it",
_ => "them",
};
}
}
[RegisterTextMacro("themself", "en")]
public class Themself : ITextMacro
{
public string Format(object? argument)
{
return IGenderable.GetGenderOrEpicene(argument) switch
{
Gender.Female => "herself",
Gender.Male => "himself",
Gender.Neuter => "itself",
_ => "themself",
};
}
}
[RegisterTextMacro("theyre", "en")]
public class Theyre : ITextMacro
{
public string Format(object? argument)
{
return IGenderable.GetGenderOrEpicene(argument) switch
{
Gender.Female => "she's",
Gender.Male => "he's",
Gender.Neuter => "it's",
_ => "they're",
};
}
}
[RegisterTextMacro("theName", "en")]
public class TheName : ITextMacro
{
private readonly NameMacro _nameMacro = new();
public string Format(object? argument)
{
var name = _nameMacro.Format(argument);
return IProperNamable.GetProperOrFalse(argument)
? name
: "the " + name;
}
}
[RegisterTextMacro("are", "en")]
public class ToBe : ITextMacro
{
public string Format(object? argument)
{
return IGenderable.GetGenderOrEpicene(argument) switch
{
Gender.Female => "is",
Gender.Male => "is",
Gender.Neuter => "is",
_ => "are",
};
}
}
[RegisterTextMacro("have", "en")]
public class Have : ITextMacro
{
public string Format(object? argument)
{
return IGenderable.GetGenderOrEpicene(argument) switch
{
Gender.Female => "has",
Gender.Male => "has",
Gender.Neuter => "has",
_ => "have",
};
}
}
}

View File

@@ -1,14 +0,0 @@
using Robust.Shared.GameObjects;
namespace Robust.Shared.Localization.Macros
{
[RegisterTextMacro("name")]
public class NameMacro: ITextMacro
{
public string Format(object? argument)
{
// TODO Make entity inherit "INameable" something?
return (argument as IEntity)?.Name ?? argument?.ToString() ?? "<null>";
}
}
}

View File

@@ -1,13 +0,0 @@
namespace Robust.Shared.Localization.Macros
{
public interface ITextMacro
{
public string Format(object? argument);
public string CapitalizedFormat(object? arg)
{
string result = Format(arg);
return char.ToUpper(result[0]) + result.Substring(1);
}
}
}

View File

@@ -1,32 +0,0 @@
using System;
using System.Collections.Generic;
namespace Robust.Shared.Localization.Macros
{
public interface ITextMacroFactory
{
/// <summary>
/// Get registered macros from
/// </summary>
/// <param name="languageTag">IEFT language tag. Might be composed of one or two subtags. for instance, "en" or "en-US".</param>
/// <returns>A dictionnary of macros, indexed by lower-cased macro name.</returns>
public IDictionary<string, ITextMacro> GetMacrosForLanguage(string languageTag);
/// <summary>
/// Register a text macro for all languages.
/// </summary>
/// <param name="name">Macro name</param>
/// <param name="macroType">The type to register</param>
public void Register(string name, Type macroType);
/// <summary>
/// Register a text macro.
/// </summary>
/// <param name="name">Macro name</param>
/// <param name="languageTag">IEFT tag for the language the macro applies to.</param>
/// <param name="macroType">The type to register</param>
public void Register(string name, string languageTag, Type macroType);
void DoAutoRegistrations();
}
}

View File

@@ -1,26 +0,0 @@
using System;
using System.Globalization;
namespace Robust.Shared.Localization.Macros
{
public class MacroFormatProvider : IFormatProvider
{
public MacroFormatter Formatter;
public CultureInfo CultureInfo;
public MacroFormatProvider(MacroFormatter formatter, CultureInfo cultureInfo)
{
Formatter = formatter;
CultureInfo = cultureInfo;
}
public object? GetFormat(Type? formatType)
{
if (formatType == typeof(ICustomFormatter))
return Formatter;
else
return CultureInfo.GetFormat(formatType);
}
}
}

View File

@@ -1,41 +0,0 @@
using System;
using System.Collections.Generic;
namespace Robust.Shared.Localization.Macros
{
public class MacroFormatter : ICustomFormatter
{
private readonly IDictionary<string, ITextMacro> Macros;
public MacroFormatter(IDictionary<string, ITextMacro> macros)
{
Macros = macros;
}
public string Format(string? format, object? arg, IFormatProvider? formatProvider)
{
IFormatProvider? fallbackProvider = GetFallbackFormatProvider(formatProvider);
if (format == null || format == "")
return string.Format(fallbackProvider, "{0}", arg);
bool capitalized = char.IsUpper(format[0]);
string lowerCasedFunctionName = char.ToLower(format[0]) + format.Substring(1);
if (!Macros.TryGetValue(lowerCasedFunctionName, out var grammarFunction))
return string.Format(fallbackProvider, "{0:" + format + '}', arg);
return capitalized
? grammarFunction.CapitalizedFormat(arg)
: grammarFunction.Format(arg);
}
private static IFormatProvider? GetFallbackFormatProvider(IFormatProvider? formatProvider)
{
if (formatProvider is MacroFormatProvider macroFormatProvider)
return macroFormatProvider.CultureInfo;
else
return formatProvider;
}
}
}

View File

@@ -1,33 +0,0 @@
using System;
using JetBrains.Annotations;
namespace Robust.Shared.Localization.Macros
{
/// <summary>
/// Register a text macro. The parameter must be the name of the macro,
/// and a an IEFT language tag might be given as a second parameter so specify the language compatible with the macro.
/// [RegisterTextMacro("they", "en")], [RegisterTextMacro("they", "en-US")].
///
/// Afterward, the macro can be use by its name.
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
[BaseTypeRequired(typeof(ITextMacro))]
[MeansImplicitUse]
public sealed class RegisterTextMacroAttribute : Attribute
{
public readonly string MacroName;
public readonly string? LanguageTag;
public RegisterTextMacroAttribute(string name)
{
MacroName = name;
}
public RegisterTextMacroAttribute(string name, string languageTag)
{
MacroName = name;
LanguageTag = languageTag;
}
}
}

View File

@@ -1,79 +0,0 @@
using System;
using System.Collections.Generic;
using Robust.Shared.IoC;
using Robust.Shared.Reflection;
using Logger = Robust.Shared.Log.Logger;
namespace Robust.Shared.Localization.Macros
{
public class TextMacroFactory : ITextMacroFactory
{
[Dependency] private readonly IDynamicTypeFactoryInternal _typeFactory = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
private struct TextMacroRegistration
{
public Type MacroType;
public string MacroName;
public string? LanguageTag;
}
private IList<TextMacroRegistration> Macros = new List<TextMacroRegistration>();
public IDictionary<string, ITextMacro> GetMacrosForLanguage(string languageTag)
{
var languageMacros = new Dictionary<string, ITextMacro>();
foreach (var registeredMacro in Macros)
{
if (IsMacroForLanguage(languageTag, registeredMacro))
{
// TODO Handle duplicate macros?
languageMacros.Add(registeredMacro.MacroName, _typeFactory.CreateInstanceUnchecked<ITextMacro>(registeredMacro.MacroType));
}
}
return languageMacros;
}
private bool IsMacroForLanguage(string languageTag, TextMacroRegistration macro)
{
int dashIndex = languageTag.IndexOf('-');
var firstSubTag = dashIndex != -1 ? languageTag.Substring(0, dashIndex) : languageTag;
return macro.LanguageTag == null || macro.LanguageTag == firstSubTag || macro.LanguageTag == languageTag;
}
public void Register(string name, Type macroType)
{
Register(name, null, macroType);
}
public void Register(string name, string? languageTag, Type macroType)
{
Macros.Add(new TextMacroRegistration
{
MacroType = macroType,
MacroName = name,
LanguageTag = languageTag,
});
}
public void DoAutoRegistrations()
{
var iComponent = typeof(ITextMacro);
foreach (var type in _reflectionManager.FindTypesWithAttribute<RegisterTextMacroAttribute>())
{
if (!iComponent.IsAssignableFrom(type))
{
Logger.Error("Type {0} has RegisterTextMacroAttribute but does not implement ITextMacro.", type);
continue;
}
RegisterTextMacroAttribute registerAttribute = (RegisterTextMacroAttribute)type.GetCustomAttributes(typeof(RegisterTextMacroAttribute), false)[0];
Register(registerAttribute.MacroName, registerAttribute.LanguageTag, type);
}
}
}
}

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\MSBuild\Robust.Properties.targets" />
<Import Project="..\MSBuild\Robust.Engine.props" />
<PropertyGroup>
@@ -14,7 +14,7 @@
<PackageReference Include="Nett" Version="0.15.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="nfluidsynth" Version="0.3.1" />
<PackageReference Include="NGettext" Version="0.6.6" />
<PackageReference Include="Fluent.Net" Version="1.0.50" />
<PackageReference Include="Pidgin" Version="2.5.0" />
<PackageReference Include="prometheus-net" Version="4.0.0" />
<PackageReference Include="Robust.Shared.AuthLib" Version="0.1.2" />

View File

@@ -5,7 +5,6 @@ using Robust.Shared.Exceptions;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Localization.Macros;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
@@ -45,7 +44,6 @@ namespace Robust.Shared
IoCManager.Register<ITaskManager, TaskManager>();
IoCManager.Register<ITimerManager, TimerManager>();
IoCManager.Register<IRobustRandom, RobustRandom>();
IoCManager.Register<ITextMacroFactory, TextMacroFactory>();
IoCManager.Register<IRobustMappedStringSerializer, RobustMappedStringSerializer>();
IoCManager.Register<IComponentDependencyManager, ComponentDependencyManager>();
IoCManager.Register<ISandboxHelper, SandboxHelper>();

View File

@@ -1,105 +0,0 @@
using NUnit.Framework;
using Robust.Shared.Localization.Macros;
using Robust.Shared.Localization.Macros.English;
namespace Robust.UnitTesting.Shared.Localization.Macros
{
[TestFixture, Parallelizable]
public class EnglishMacros_test
{
private struct Subject : IGenderable, IProperNamable
{
public string Name;
public Gender Gender { get; set; }
public bool Proper { get; set; }
public Subject(string name, Gender gender, bool proper)
{
Name = name;
Gender = gender;
Proper = proper;
}
public override string ToString()
{
return Name;
}
}
private readonly Subject female = new("Lisa", Gender.Female, true);
private readonly Subject male = new("Bob", Gender.Male, true);
private readonly Subject epicene = new("Michel", Gender.Epicene, true);
private readonly Subject neuter = new("D.O.O.R.K.N.O.B.", Gender.Neuter, true);
public void TestThey()
{
ITextMacro sut = new They();
Assert.That(sut.CapitalizedFormat(female), Is.EqualTo("She"));
Assert.That(sut.Format(male), Is.EqualTo("he"));
Assert.That(sut.Format(epicene), Is.EqualTo("they"));
Assert.That(sut.Format(neuter), Is.EqualTo("it"));
}
[Test]
public void TestTheir()
{
var sut = new Their();
Assert.That(sut.Format(female), Is.EqualTo("her"));
Assert.That(sut.Format(male), Is.EqualTo("his"));
Assert.That(sut.Format(epicene), Is.EqualTo("their"));
Assert.That(sut.Format(neuter), Is.EqualTo("its"));
}
[Test]
public void TestTheirs()
{
var sut = new Theirs();
Assert.That(sut.Format(female), Is.EqualTo("hers"));
Assert.That(sut.Format(male), Is.EqualTo("his"));
Assert.That(sut.Format(epicene), Is.EqualTo("theirs"));
Assert.That(sut.Format(neuter), Is.EqualTo("its"));
}
[Test]
public void TestThem()
{
var sut = new Them();
Assert.That(sut.Format(female), Is.EqualTo("her"));
Assert.That(sut.Format(male), Is.EqualTo("him"));
Assert.That(sut.Format(epicene), Is.EqualTo("them"));
Assert.That(sut.Format(neuter), Is.EqualTo("it"));
}
[Test]
public void TestThemself()
{
var sut = new Themself();
Assert.That(sut.Format(female), Is.EqualTo("herself"));
Assert.That(sut.Format(male), Is.EqualTo("himself"));
Assert.That(sut.Format(epicene), Is.EqualTo("themself"));
Assert.That(sut.Format(neuter), Is.EqualTo("itself"));
}
[Test]
public void TestTheyre()
{
ITextMacro sut = new Theyre();
Assert.That(sut.CapitalizedFormat(female), Is.EqualTo("She's"));
Assert.That(sut.Format(male), Is.EqualTo("he's"));
Assert.That(sut.Format(epicene), Is.EqualTo("they're"));
Assert.That(sut.Format(neuter), Is.EqualTo("it's"));
}
[Test]
public void TestTheName()
{
var cpu = new Subject("CPU", Gender.Neuter, false);
ITextMacro sut = new TheName();
Assert.That(sut.CapitalizedFormat(cpu), Is.EqualTo("The CPU"));
Assert.That(sut.Format(cpu), Is.EqualTo("the CPU"));
Assert.That(sut.Format(male), Is.EqualTo(male.Name));
}
}
}

View File

@@ -1,125 +0,0 @@
using System.Collections.Generic;
using System.Globalization;
using NUnit.Framework;
using Robust.Shared.Localization.Macros;
using Robust.Shared.Localization.Macros.English;
namespace Robust.UnitTesting.Shared.Localization.Macros
{
[TestFixture, Parallelizable, TestOf(typeof(MacroFormatProvider))]
internal class MacroFormatProvider_tests
{
private MacroFormatProvider sut = default!;
private class GenderedPerson : IGenderable
{
public string Name;
public Gender Gender { get; set; }
public GenderedPerson(string name, Gender gender)
{
Name = name;
Gender = gender;
}
public override string ToString()
{
return Name;
}
}
private readonly GenderedPerson female = new("Lisa", Gender.Female);
private readonly GenderedPerson male = new("Bob", Gender.Male);
private readonly GenderedPerson epicene = new("Michel", Gender.Epicene);
private readonly GenderedPerson neuter = new("D.O.O.R.K.N.O.B.", Gender.Neuter);
[SetUp]
public void SetUp()
{
var macros = new Dictionary<string, ITextMacro>
{
{ "they", new They() },
{ "their", new Their() },
{ "theirs", new Theirs() },
{ "them", new Them() },
{ "themself", new Themself() },
{ "theyre", new Theyre() }
};
sut = new MacroFormatProvider(new MacroFormatter(macros), CultureInfo.CurrentCulture);
}
[Test]
public void CanFormatNormally()
{
AssertFormatNormally("Hello {0}", "world");
AssertFormatNormally("PI is roughly {0}", 3.1415);
AssertFormatNormally("Scientific notation: {0:#.##E+0}", 1234.5678);
}
private void AssertFormatNormally(string format, params object[] args)
{
Assert.That(string.Format(sut, format, args), Is.EqualTo(string.Format(format, args)));
}
[Test]
public void TestInsertThey()
{
Assert.That(string.Format(sut, "{0:They} protects", female), Is.EqualTo("She protects"));
Assert.That(string.Format(sut, "{0:They} attacks", male), Is.EqualTo("He attacks"));
Assert.That(string.Format(sut, "{0:They} plasmaflood", neuter), Is.EqualTo("It plasmaflood"));
Assert.That(string.Format(sut, "But most importantly, {0:they} do grammar right", epicene), Is.EqualTo("But most importantly, they do grammar right"));
}
[Test]
public void TestInsertTheir()
{
Assert.That(string.Format(sut, "{0:Their} toolbox", female), Is.EqualTo("Her toolbox"));
Assert.That(string.Format(sut, "{0:Their} toolbox", male), Is.EqualTo("His toolbox"));
Assert.That(string.Format(sut, "{0:Their} toolbox", neuter), Is.EqualTo("Its toolbox"));
Assert.That(string.Format(sut, "Grab {0:their} toolbox", epicene), Is.EqualTo("Grab their toolbox"));
}
[Test]
public void TestInsertTheirs()
{
Assert.That(string.Format(sut, "{0:Theirs} toolboxs", female), Is.EqualTo("Hers toolboxs"));
Assert.That(string.Format(sut, "{0:Theirs} toolboxs", male), Is.EqualTo("His toolboxs"));
Assert.That(string.Format(sut, "{0:Theirs} toolboxs", neuter), Is.EqualTo("Its toolboxs"));
Assert.That(string.Format(sut, "Grab {0:theirs} toolboxs", epicene), Is.EqualTo("Grab theirs toolboxs"));
}
[Test]
public void TestInsertThem()
{
Assert.That(string.Format(sut, "Robust {0:them}", female), Is.EqualTo("Robust her"));
Assert.That(string.Format(sut, "Robust {0:them}", male), Is.EqualTo("Robust him"));
Assert.That(string.Format(sut, "Robust {0:them}", neuter), Is.EqualTo("Robust it"));
Assert.That(string.Format(sut, "Robust {0:them}", epicene), Is.EqualTo("Robust them"));
}
[Test]
public void TestInsertThemself()
{
Assert.That(string.Format(sut, "Robust {0:themself}", female), Is.EqualTo("Robust herself"));
Assert.That(string.Format(sut, "Robust {0:themself}", male), Is.EqualTo("Robust himself"));
Assert.That(string.Format(sut, "Robust {0:themself}", neuter), Is.EqualTo("Robust itself"));
Assert.That(string.Format(sut, "Robust {0:themself}", epicene), Is.EqualTo("Robust themself"));
}
[Test]
public void TestInsertTheyre()
{
Assert.That(string.Format(sut, "{0:Theyre} robust", female), Is.EqualTo("She's robust"));
Assert.That(string.Format(sut, "{0:Theyre} robust", male), Is.EqualTo("He's robust"));
Assert.That(string.Format(sut, "{0:Theyre} robust", neuter), Is.EqualTo("It's robust"));
Assert.That(string.Format(sut, "{0:Theyre} robust", epicene), Is.EqualTo("They're robust"));
}
[Test]
public void TestUseToString()
{
Assert.That(string.Format(sut, "{0} uses {0:their} toolbox", male), Is.EqualTo("Bob uses his toolbox"));
}
}
}

View File

@@ -1,57 +0,0 @@
using NUnit.Framework;
using Robust.Shared.IoC;
using Robust.Shared.Localization.Macros;
namespace Robust.UnitTesting.Shared.Localization.Macros
{
[TestFixture, Parallelizable, TestOf(typeof(TextMacroFactory))]
internal class TextMacroFactory_tests : RobustUnitTest
{
private TextMacroFactory sut = default!;
[SetUp]
public void SetUp()
{
sut = new TextMacroFactory();
IoCManager.InjectDependencies(sut);
}
[RegisterTextMacro("mock_macro", "test-TE")]
private class MockTextMacro : ITextMacro
{
public string Format(object? argument)
{
throw new System.NotImplementedException();
}
}
[Test]
public void TestResolveLanguageMacro()
{
sut.Register("my_macro", typeof(MockTextMacro));
sut.Register("my_macro_en", "en", typeof(MockTextMacro));
sut.Register("my_macro_us", "en-US", typeof(MockTextMacro));
sut.Register("my_macro_gb", "en-GB", typeof(MockTextMacro));
sut.Register("my_macro_t", "ent", typeof(MockTextMacro));
var macros = sut.GetMacrosForLanguage("en-US");
Assert.IsTrue(macros.ContainsKey("my_macro"));
Assert.IsTrue(macros.ContainsKey("my_macro_en"));
Assert.IsTrue(macros.ContainsKey("my_macro_us"));
Assert.IsFalse(macros.ContainsKey("my_macro_gb"));
Assert.IsFalse(macros.ContainsKey("my_macro_t"));
}
[Test]
public void TestAutoRegistrations()
{
sut.DoAutoRegistrations();
var macros = sut.GetMacrosForLanguage("test-TE");
Assert.IsTrue(macros.ContainsKey("mock_macro"));
Assert.IsInstanceOf<MockTextMacro>(macros["mock_macro"]);
}
}
}