From ddbeccdb8c510953a2abdb30a456fd6810bc39ca Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Sat, 11 May 2019 23:59:48 +0200 Subject: [PATCH] Some work supporting localization. Far from complete, but a good start. --- Robust.Client/GameController.cs | 7 + .../GameController/GameController.IoC.cs | 2 + Robust.Server/BaseServer.cs | 9 ++ Robust.Server/Program.cs | 2 + .../Localization/ILocalizationManager.cs | 51 ++++++ .../Localization/LocalizationManager.cs | 150 ++++++++++++++++++ Robust.Shared/Prototypes/EntityPrototype.cs | 7 +- Robust.Shared/Prototypes/IPrototype.cs | 3 +- Robust.Shared/Prototypes/PrototypeManager.cs | 4 +- Robust.Shared/Robust.Shared.csproj | 3 + Robust.UnitTesting/RobustUnitTest.cs | 2 + 11 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 Robust.Shared/Localization/ILocalizationManager.cs create mode 100644 Robust.Shared/Localization/LocalizationManager.cs diff --git a/Robust.Client/GameController.cs b/Robust.Client/GameController.cs index 29294d831..bfe1dc7d6 100644 --- a/Robust.Client/GameController.cs +++ b/Robust.Client/GameController.cs @@ -33,6 +33,7 @@ using Robust.Shared.Utility; using System; using System.Collections.Generic; using System.ComponentModel; +using System.Globalization; using System.IO; using System.Reflection; using System.Threading; @@ -43,6 +44,7 @@ using Robust.Client.ViewVariables; using Robust.Shared; using Robust.Shared.Asynchronous; using Robust.Shared.Interfaces.Resources; +using Robust.Shared.Localization; namespace Robust.Client { @@ -74,6 +76,7 @@ namespace Robust.Client [Dependency] private readonly IDiscordRichPresence _discord; [Dependency] private readonly IClyde _clyde; [Dependency] private readonly IFontManagerInternal _fontManager; + [Dependency] private readonly ILocalizationManager _localizationManager; private void Startup() { @@ -103,6 +106,10 @@ namespace Robust.Client _resourceCache.Initialize(userDataDir); _resourceCache.LoadBaseResources(); + // Default to en-US. + // Perhaps in the future we could make a command line arg or something to change this default. + _localizationManager.LoadCulture(new CultureInfo("en-US")); + // Bring display up as soon as resources are mounted. _displayManager.Initialize(); _displayManager.SetWindowTitle("Space Station 14"); diff --git a/Robust.Client/GameController/GameController.IoC.cs b/Robust.Client/GameController/GameController.IoC.cs index 7f450124b..260d8d005 100644 --- a/Robust.Client/GameController/GameController.IoC.cs +++ b/Robust.Client/GameController/GameController.IoC.cs @@ -59,6 +59,7 @@ using Robust.Client.ViewVariables; using Robust.Shared.Asynchronous; using Robust.Shared.Interfaces.Resources; using Robust.Shared.Exceptions; +using Robust.Shared.Localization; using Robust.Shared.Map; namespace Robust.Client @@ -93,6 +94,7 @@ namespace Robust.Client IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); // Client stuff. IoCManager.Register(); diff --git a/Robust.Server/BaseServer.cs b/Robust.Server/BaseServer.cs index 38b63185b..936ab9e67 100644 --- a/Robust.Server/BaseServer.cs +++ b/Robust.Server/BaseServer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; @@ -44,6 +45,7 @@ using Robust.Shared.Utility; using Robust.Shared.Interfaces.Log; using Robust.Shared.Interfaces.Resources; using Robust.Shared.Exceptions; +using Robust.Shared.Localization; namespace Robust.Server { @@ -80,6 +82,8 @@ namespace Robust.Server private readonly ISystemConsoleManager _systemConsole; [Dependency] private readonly ITaskManager _taskManager; + [Dependency] + private readonly ILocalizationManager _localizationManager; private FileLogHandler fileLogHandler; private IGameLoop _mainLoop; @@ -187,6 +191,11 @@ namespace Robust.Server _resources.MountContentDirectory($@"{ContentRootDir}Resources/"); #endif + // Default to en-US. + // Perhaps in the future we could make a command line arg or something to change this default. + _localizationManager.LoadCulture(new CultureInfo("en-US")); + + //mount the engine content pack // _resources.MountContentPack(@"EngineContentPack.zip"); diff --git a/Robust.Server/Program.cs b/Robust.Server/Program.cs index 49cd31c65..78840cded 100644 --- a/Robust.Server/Program.cs +++ b/Robust.Server/Program.cs @@ -47,6 +47,7 @@ using Robust.Server.Timing; using Robust.Server.ViewVariables; using Robust.Shared.Asynchronous; using Robust.Shared.Exceptions; +using Robust.Shared.Localization; namespace Robust.Server { @@ -118,6 +119,7 @@ namespace Robust.Server IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); // Server stuff. IoCManager.Register(); diff --git a/Robust.Shared/Localization/ILocalizationManager.cs b/Robust.Shared/Localization/ILocalizationManager.cs new file mode 100644 index 000000000..dbbbf916d --- /dev/null +++ b/Robust.Shared/Localization/ILocalizationManager.cs @@ -0,0 +1,51 @@ +using System.Globalization; + +namespace Robust.Shared.Localization +{ + public interface ILocalizationManager + { + /// + /// Gets a string translated for the current culture. + /// + /// The string to get translated. + /// + /// The translated string if a translation is available, otherwise the string is returned. + /// + string GetString(string text); + + /// + /// Version of that also runs string formatting. + /// + string GetString(string text, params object[] args); + + /// + /// Gets a string inside a context or category. + /// + string GetParticularString(string context, string text); + + /// + /// Gets a string inside a context or category with formatting. + /// + string GetParticularString(string context, string text, params object[] args); + + string GetPluralString(string text, string pluralText, long n); + + string GetPluralString(string text, string pluralText, long n, params object[] args); + + string GetParticularPluralString(string context, string text, string pluralText, long n); + + string GetParticularPluralString(string context, string text, string pluralText, long n, params object[] args); + + /// + /// Default culture used by other methods when no culture is explicitly specified. + /// Changing this also changes the current thread's culture. + /// + CultureInfo DefaultCulture { get; set; } + + /// + /// Load data for a culture. + /// + /// + void LoadCulture(CultureInfo culture); + } +} diff --git a/Robust.Shared/Localization/LocalizationManager.cs b/Robust.Shared/Localization/LocalizationManager.cs new file mode 100644 index 000000000..bfb2714d4 --- /dev/null +++ b/Robust.Shared/Localization/LocalizationManager.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using NGettext; +using Robust.Shared.Interfaces.Resources; +using Robust.Shared.IoC; +using Robust.Shared.Utility; +using YamlDotNet.RepresentationModel; + +namespace Robust.Shared.Localization +{ + internal sealed class LocalizationManager : ILocalizationManager + { + [Dependency] private readonly IResourceManager _resourceManager; + + private readonly Dictionary _catalogs = new Dictionary(); + private CultureInfo _defaultCulture; + + public string GetString(string text) + { + var catalog = _catalogs[_defaultCulture]; + return catalog.GetString(text); + } + + public string GetString(string text, params object[] args) + { + var catalog = _catalogs[_defaultCulture]; + return catalog.GetString(text, args); + } + + public string GetParticularString(string context, string text) + { + var catalog = _catalogs[_defaultCulture]; + return catalog.GetParticularString(context, text); + } + + public string GetParticularString(string context, string text, params object[] args) + { + var catalog = _catalogs[_defaultCulture]; + return catalog.GetParticularString(context, text, args); + } + + public string GetPluralString(string text, string pluralText, long n) + { + var catalog = _catalogs[_defaultCulture]; + return catalog.GetParticularString(text, pluralText, n); + } + + public string GetPluralString(string text, string pluralText, long n, params object[] args) + { + var catalog = _catalogs[_defaultCulture]; + return catalog.GetParticularString(text, pluralText, n, args); + } + + public string GetParticularPluralString(string context, string text, string pluralText, long n) + { + var catalog = _catalogs[_defaultCulture]; + return catalog.GetParticularString(context, text, n, pluralText); + } + + public string GetParticularPluralString(string context, string text, string pluralText, long n, params object[] args) + { + var catalog = _catalogs[_defaultCulture]; + return catalog.GetParticularPluralString(context, text, pluralText, n, args); + } + + public CultureInfo DefaultCulture + { + get => _defaultCulture; + set + { + if (!_catalogs.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 catalog = new Catalog(culture); + _catalogs.Add(culture, catalog); + + _loadData(culture, catalog); + if (DefaultCulture == null) + { + DefaultCulture = culture; + } + } + + private void _loadData(CultureInfo culture, Catalog catalog) + { + // Load data from .yml files. + // Data is loaded from /Locale//* + + var root = new ResourcePath($"/Locale/{culture.IetfLanguageTag}/"); + + foreach (var file in _resourceManager.ContentFindFiles(root)) + { + var yamlFile = root / file; + _loadFromFile(yamlFile, catalog); + } + } + + private void _loadFromFile(ResourcePath filePath, Catalog catalog) + { + var yamlStream = new YamlStream(); + using (var fileStream = _resourceManager.ContentFileRead(filePath)) + using (var reader = new StreamReader(fileStream, EncodingHelpers.UTF8)) + { + yamlStream.Load(reader); + } + + foreach (var entry in yamlStream.Documents + .SelectMany(d => (YamlSequenceNode) d.RootNode) + .Cast()) + { + _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); + } + } +} diff --git a/Robust.Shared/Prototypes/EntityPrototype.cs b/Robust.Shared/Prototypes/EntityPrototype.cs index c00bd3d36..e0ae6ef64 100644 --- a/Robust.Shared/Prototypes/EntityPrototype.cs +++ b/Robust.Shared/Prototypes/EntityPrototype.cs @@ -7,6 +7,7 @@ using Robust.Shared.Utility; using System; using System.Collections.Generic; using System.Linq; +using Robust.Shared.Localization; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Maths; @@ -22,6 +23,8 @@ namespace Robust.Shared.GameObjects [Prototype("entity")] public class EntityPrototype : IPrototype, IIndexedPrototype, ISyncingPrototype { + [Dependency] private readonly ILocalizationManager _localization; + /// /// The type string of this prototype used in files. /// @@ -143,7 +146,7 @@ namespace Robust.Shared.GameObjects if (mapping.TryGetNode("name", out YamlNode node)) { - Name = node.AsString(); + Name = _localization.GetString(node.AsString()); } if (mapping.TryGetNode("class", out node)) @@ -163,7 +166,7 @@ namespace Robust.Shared.GameObjects // DESCRIPTION if (mapping.TryGetNode("description", out node)) { - Description = node.AsString(); + Description = _localization.GetString(node.AsString()); } // COMPONENTS diff --git a/Robust.Shared/Prototypes/IPrototype.cs b/Robust.Shared/Prototypes/IPrototype.cs index f6d138f60..c11e00c20 100644 --- a/Robust.Shared/Prototypes/IPrototype.cs +++ b/Robust.Shared/Prototypes/IPrototype.cs @@ -1,4 +1,5 @@ -using YamlDotNet.RepresentationModel; +using Robust.Shared.Localization; +using YamlDotNet.RepresentationModel; namespace Robust.Shared.Prototypes { diff --git a/Robust.Shared/Prototypes/PrototypeManager.cs b/Robust.Shared/Prototypes/PrototypeManager.cs index d298a9e82..2ce65a459 100644 --- a/Robust.Shared/Prototypes/PrototypeManager.cs +++ b/Robust.Shared/Prototypes/PrototypeManager.cs @@ -94,6 +94,8 @@ namespace Robust.Shared.Prototypes { [Dependency] private readonly IReflectionManager ReflectionManager; + [Dependency] + private readonly IDynamicTypeFactory _dynamicTypeFactory; private readonly Dictionary prototypeTypes = new Dictionary(); [Dependency] @@ -287,7 +289,7 @@ namespace Robust.Shared.Prototypes } var prototypeType = prototypeTypes[type]; - var prototype = (IPrototype)Activator.CreateInstance(prototypeType); + var prototype = (IPrototype)_dynamicTypeFactory.CreateInstance(prototypeType); prototype.LoadFrom(node); prototypes[prototypeType].Add(prototype); var indexedPrototype = prototype as IIndexedPrototype; diff --git a/Robust.Shared/Robust.Shared.csproj b/Robust.Shared/Robust.Shared.csproj index 662e00dbd..b995b850b 100644 --- a/Robust.Shared/Robust.Shared.csproj +++ b/Robust.Shared/Robust.Shared.csproj @@ -75,6 +75,7 @@ + @@ -171,6 +172,8 @@ + + diff --git a/Robust.UnitTesting/RobustUnitTest.cs b/Robust.UnitTesting/RobustUnitTest.cs index a2fecd5c7..ed374e442 100644 --- a/Robust.UnitTesting/RobustUnitTest.cs +++ b/Robust.UnitTesting/RobustUnitTest.cs @@ -68,6 +68,7 @@ using Robust.Shared.Interfaces.Serialization; using Robust.Shared.Interfaces.Timers; using Robust.Shared.Interfaces.Timing; using Robust.Shared.IoC; +using Robust.Shared.Localization; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Network; @@ -200,6 +201,7 @@ namespace Robust.UnitTesting IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); switch (Project) {