diff --git a/.idea/.idea.PetroglyphTools/.idea/copyright/Alamo_Engine_Tools_Copyright_Profile.xml b/.idea/.idea.PetroglyphTools/.idea/copyright/Alamo_Engine_Tools_Copyright_Profile.xml new file mode 100644 index 000000000..a57a5ce80 --- /dev/null +++ b/.idea/.idea.PetroglyphTools/.idea/copyright/Alamo_Engine_Tools_Copyright_Profile.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.PetroglyphTools/.idea/copyright/profiles_settings.xml b/.idea/.idea.PetroglyphTools/.idea/copyright/profiles_settings.xml new file mode 100644 index 000000000..690b7667c --- /dev/null +++ b/.idea/.idea.PetroglyphTools/.idea/copyright/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.PetroglyphTools/.idea/developer-tools.xml b/.idea/.idea.PetroglyphTools/.idea/developer-tools.xml new file mode 100644 index 000000000..37021a5a2 --- /dev/null +++ b/.idea/.idea.PetroglyphTools/.idea/developer-tools.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.PetroglyphTools/.idea/indexLayout.xml b/.idea/.idea.PetroglyphTools/.idea/indexLayout.xml new file mode 100644 index 000000000..7b08163ce --- /dev/null +++ b/.idea/.idea.PetroglyphTools/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.PetroglyphTools/.idea/projectSettingsUpdater.xml b/.idea/.idea.PetroglyphTools/.idea/projectSettingsUpdater.xml new file mode 100644 index 000000000..64af657f5 --- /dev/null +++ b/.idea/.idea.PetroglyphTools/.idea/projectSettingsUpdater.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/.idea/.idea.PetroglyphTools/.idea/vcs.xml b/.idea/.idea.PetroglyphTools/.idea/vcs.xml new file mode 100644 index 000000000..35eb1ddfb --- /dev/null +++ b/.idea/.idea.PetroglyphTools/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index cb972937c..0ea7e19cb 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,3 +1,8 @@ + + $(MSBuildThisFileDirectory) @@ -10,15 +15,15 @@ Alamo Engine Tools and Contributors Copyright © 2023 Alamo Engine Tools and contributors. All rights reserved. https://github.com/AlamoEngine-Tools/PetroglyphTools - $(MSBuildThisFileDirectory)LICENSE - MIT + $(MSBuildThisFileDirectory)LICENSE + MIT https://github.com/AlamoEngine-Tools/PetroglyphTools git Alamo Engine Tools README.md - aet.png - - + aet.png + + latest disable @@ -38,8 +43,8 @@ - - - - + + + + \ No newline at end of file diff --git a/PG.Commons/PG.Commons.Test/Data/IIdTestBase.cs b/PG.Commons/PG.Commons.Test/Data/IIdTestBase.cs new file mode 100644 index 000000000..e8c10839a --- /dev/null +++ b/PG.Commons/PG.Commons.Test/Data/IIdTestBase.cs @@ -0,0 +1,44 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.Collections.Generic; +using System.Linq; +using PG.Commons.Data; +using Xunit; + +namespace PG.Commons.Test.Data; + +// ReSharper disable once InconsistentNaming +public abstract class IIdTestBase where T : IId +{ + [Fact] + public void Test_NullIIdCreationReturnsNull() + { + foreach (var nullKeys in GetConfiguredNullIIds()) Assert.Null(nullKeys); + } + + [Fact] + public void Test_IIdBehavesAsExpectedWithHash() + { + var keyInitialisers = GetConfiguredValidIIdInitialisers(); + var dict = new Dictionary(); + foreach (var key in keyInitialisers.Select(keyInitialiser => CreateId(keyInitialiser))) + { + Assert.NotNull(key); + dict.Add(key, $"{key}: Hash: {key.GetHashCode()}"); + } + + foreach (var key in keyInitialisers.Select(keyInitialiser => CreateId(keyInitialiser))) + { + Assert.NotNull(key); + Assert.Contains(key, dict.Keys); + Assert.NotNull(dict[key]); + } + } + + protected abstract List GetConfiguredNullIIds(); + + protected abstract List GetConfiguredValidIIdInitialisers(); + + protected abstract T CreateId(object[] keyInitialiser); +} diff --git a/PG.Commons/PG.Commons.Test/Data/IIdTestCompletenessTestBase.cs b/PG.Commons/PG.Commons.Test/Data/IIdTestCompletenessTestBase.cs new file mode 100644 index 000000000..e41b80436 --- /dev/null +++ b/PG.Commons/PG.Commons.Test/Data/IIdTestCompletenessTestBase.cs @@ -0,0 +1,48 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using PG.Commons.Data; +using Xunit; + +namespace PG.Commons.Test.Data; + +// ReSharper disable once InconsistentNaming +public abstract class IIdTestCompletenessTestBase +{ + [Fact] + public void Test_IIdTestPresetForAllIIdsOfPackage() + { + var types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(x => x.GetTypes()).ToList(); + var iids = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(assemblyTypes => assemblyTypes.GetTypes()) + .Where(assemblyType => typeof(IId).IsAssignableFrom(assemblyType) + && assemblyType is { IsClass: true, IsAbstract: false } + && assemblyType.Namespace != null + && assemblyType.Namespace.StartsWith(GetConfiguredNamespaceBase())) + .ToList(); + var present = new Dictionary(); + + + foreach (var iid in iids) + { + present.Add(iid, false); + if (types.Any(type => iid.Name + "Test" == type.Name)) present[iid] = true; + } + + var o = new StringBuilder().Append("The following IIds have no corresponding test:\n"); + var missing = false; + foreach (var kvp in present.Where(kvp => !kvp.Value)) + { + o.Append($"\t{kvp.Key.FullName}\n"); + missing = true; + } + + Assert.False(missing, o.ToString()); + } + + protected abstract string GetConfiguredNamespaceBase(); +} diff --git a/PG.Commons/PG.Commons/Attributes/Platform.cs b/PG.Commons/PG.Commons/Attributes/Platform.cs new file mode 100644 index 000000000..2ce65ac65 --- /dev/null +++ b/PG.Commons/PG.Commons/Attributes/Platform.cs @@ -0,0 +1,40 @@ +using System; + +namespace PG.Commons.Attributes; + +/// +/// The platform a feature/... is supported on. +/// +[Flags] +public enum Platform +{ + /// + /// Disc version of the game / expansion + /// + Disc = 0b00001, + + /// + /// Steam version of the game / expansion + /// + Steam = 0b00010, + + /// + /// Origin version of the game / expansion - yep that abomination exists. + /// + Origin = 0b00100, + + /// + /// GoG.com version of the game. + /// + GoG = 0b01000, + + /// + /// MAC version of the game / expansion - yep that exists as well, only the base game though. + /// + Mac = 0b10000, + + /// + /// Versions that do no longer receive updates. + /// + Outdated = Disc | Origin | Steam | GoG | Mac +} \ No newline at end of file diff --git a/PG.Commons/PG.Commons/Attributes/PlatformAttribute.cs b/PG.Commons/PG.Commons/Attributes/PlatformAttribute.cs new file mode 100644 index 000000000..dd027bafd --- /dev/null +++ b/PG.Commons/PG.Commons/Attributes/PlatformAttribute.cs @@ -0,0 +1,24 @@ +using System; + +namespace PG.Commons.Attributes; + +/// +/// Simple annotation to attach platform info. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct)] +public class PlatformAttribute : Attribute +{ + /// + /// .ctor + /// + /// + public PlatformAttribute(Platform platform) + { + Platform = platform; + } + + /// + /// The flags. + /// + public Platform Platform { get; } +} \ No newline at end of file diff --git a/PG.Commons/PG.Commons/Data/DataTransferObjectMapperBase.cs b/PG.Commons/PG.Commons/Data/DataTransferObjectMapperBase.cs new file mode 100644 index 000000000..2635483ea --- /dev/null +++ b/PG.Commons/PG.Commons/Data/DataTransferObjectMapperBase.cs @@ -0,0 +1,58 @@ +using System; + +namespace PG.Commons.Data; + +/// +/// Container class allowing to map from a data objct to a peer and vice versa. +///
+/// @lgr: check if fully replaceable with AutoMapper +///
+/// +/// +public abstract class DataTransferObjectMapperBase +{ + private readonly DataTransferObjectMappings _mappings; + + /// + /// .ctor + /// + protected DataTransferObjectMapperBase() + { + var mappings = new DataTransferObjectMappings(); + // ReSharper disable once VirtualMemberCallInConstructor + InitMappings(mappings); + _mappings = mappings; + } + + /// + /// Method is overridden by subclasses to initialize the mappings. + /// + /// + protected abstract void InitMappings(DataTransferObjectMappings mappings); + + + /// + /// Applies the mapping from the source to the data object. + /// + /// + /// + /// + /// + protected bool ToDto(TPeer source, TDto dataObject) + { + return _mappings.ToDto(source ?? throw new ArgumentNullException(nameof(source)), dataObject); + } + + /// + /// Applies the mapping from the data object to the target. + /// + /// + /// + /// + /// + protected bool FromDto(TDto dataObject, TPeer target) + { + return _mappings.FromDto(dataObject ?? throw new ArgumentNullException(nameof(dataObject)), + target); + } +} \ No newline at end of file diff --git a/PG.Commons/PG.Commons/Data/DataTransferObjectMappings.cs b/PG.Commons/PG.Commons/Data/DataTransferObjectMappings.cs new file mode 100644 index 000000000..e66940aff --- /dev/null +++ b/PG.Commons/PG.Commons/Data/DataTransferObjectMappings.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace PG.Commons.Data; + +/// +/// Data mappings. +/// +/// +/// +public class DataTransferObjectMappings +{ + private List> Mappings { get; } = new(); + + // ReSharper disable once HeapView.ClosureAllocation + internal bool ToDto([DisallowNull] TPeer source, TDto dataObject) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + try + { + // ReSharper disable once HeapView.DelegateAllocation + Mappings.ForEach(m => m.ToDto(source, dataObject)); + } + catch (Exception) + { + return false; + } + + return true; + } + + // ReSharper disable once HeapView.ClosureAllocation + internal bool FromDto([DisallowNull] TDto dataObject, TPeer target) + { + if (dataObject == null) throw new ArgumentNullException(nameof(dataObject)); + try + { + // ReSharper disable once HeapView.DelegateAllocation + Mappings.ForEach(m => m.FromDto(dataObject, target)); + } + catch (Exception) + { + return false; + } + + return true; + } + + /// + /// Mapping builder. + /// + /// + /// + /// + /// + /// + /// + public DataTransferObjectMappings With(Func dataObjectValueGetter, + Action dataObjectValueSetter, + Func peerValueGetter, Action peerValueSetter) + { + // ReSharper disable once HeapView.ObjectAllocation.Evident + Mappings.Add(new DataTransferTransferObjectMapping(dataObjectValueGetter, + dataObjectValueSetter, + peerValueGetter, peerValueSetter)); + return this; + } +} \ No newline at end of file diff --git a/PG.Commons/PG.Commons/Data/DataTransferTransferObjectMapping.cs b/PG.Commons/PG.Commons/Data/DataTransferTransferObjectMapping.cs new file mode 100644 index 000000000..0317d1b93 --- /dev/null +++ b/PG.Commons/PG.Commons/Data/DataTransferTransferObjectMapping.cs @@ -0,0 +1,33 @@ +using System; + +namespace PG.Commons.Data; + +internal class DataTransferTransferObjectMapping( + Func dataObjectValueGetter, + Action dataObjectValueSetter, + Func peerValueGetter, + Action peerValueSetter) + : IDataTransferObjectMapping +{ + private Func DataObjectValueGetter { get; } = dataObjectValueGetter; + private Action DataObjectValueSetter { get; } = dataObjectValueSetter; + private Func PeerValueGetter { get; } = peerValueGetter; + private Action PeerValueSetter { get; } = peerValueSetter; + + public void ToDto(TPeer source, TDto dataObject) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + if (dataObject == null) throw new ArgumentNullException(nameof(dataObject)); + + var value = PeerValueGetter.Invoke(source); + DataObjectValueSetter.Invoke(dataObject, value); + } + + public void FromDto(TDto dataObject, TPeer target) + { + if (target == null) throw new ArgumentNullException(nameof(target)); + if (dataObject == null) throw new ArgumentNullException(nameof(dataObject)); + var value = DataObjectValueGetter.Invoke(dataObject); + PeerValueSetter.Invoke(target, value); + } +} \ No newline at end of file diff --git a/PG.Commons/PG.Commons/Data/IDataTransferObjectMapping.cs b/PG.Commons/PG.Commons/Data/IDataTransferObjectMapping.cs new file mode 100644 index 000000000..546cd54a5 --- /dev/null +++ b/PG.Commons/PG.Commons/Data/IDataTransferObjectMapping.cs @@ -0,0 +1,8 @@ +namespace PG.Commons.Data; + +internal interface IDataTransferObjectMapping +{ + internal void ToDto(TPeer source, TDataObject dataObject); + + internal void FromDto(TDataObject dataObject, TPeer target); +} \ No newline at end of file diff --git a/PG.Commons/PG.Commons/Data/IId.cs b/PG.Commons/PG.Commons/Data/IId.cs new file mode 100644 index 000000000..d85c34d2c --- /dev/null +++ b/PG.Commons/PG.Commons/Data/IId.cs @@ -0,0 +1,14 @@ +using System; + +namespace PG.Commons.Data; + +/// +/// A generic ID definition +/// +public interface IId : IEquatable, IComparable, IComparable +{ + /// + /// The arity of the ID. + /// + int Arity { get; } +} diff --git a/PG.Commons/PG.Commons/Data/IdBase.cs b/PG.Commons/PG.Commons/Data/IdBase.cs new file mode 100644 index 000000000..cff090f40 --- /dev/null +++ b/PG.Commons/PG.Commons/Data/IdBase.cs @@ -0,0 +1,105 @@ +using System; +using System.Linq; +using System.Text; +using PG.Commons.Exceptions; + +namespace PG.Commons.Data; + +/// +public abstract class IdBase : IId +{ + /// + /// The ID components. + /// + // ReSharper disable once TypeWithSuspiciousEqualityIsUsedInRecord.Global + protected readonly object?[] Components; + + /// + /// .ctor + /// + protected IdBase(params object[] components) + { + if (components.Length != Arity) throw new ArgumentException("Invalid number of components"); + Components = new object[Arity]; + for (var i = 0; i < Components.Length; i++) Components[i] = components[i]; + } + + /// + public bool Equals(IId? other) + { + return other != null + && GetType() == other.GetType() + && Arity == other.Arity + && GetHashCode().Equals(other.GetHashCode()); + } + + /// + public int Arity => GetConfiguredArity(); + + /// + public int CompareTo(object other) + { + if (other == null || GetType() != other.GetType()) throw new ArgumentException("Object must be of type IdBase"); + return CompareTo(other as IdBase); + } + + /// + public int CompareTo(IId? other) + { + return other == null ? 1 : GetHashCode().CompareTo(other.GetHashCode()); + } + + /// + public override string ToString() + { + var b = new StringBuilder(); + b.Append(GetType().Name).Append('['); + foreach (var idComponent in Components) b.Append(idComponent).Append(';'); + b.Remove(b.Length - 1, 1); // remove the last ";". + b.Append(']'); + return b.ToString(); + } + + /// + /// Convenience method to access components in a type-safe manner. + /// + /// + /// + /// + /// + /// If the provided index is out of bounds. + /// If no is provided. + /// + protected T? GetIdComponent(int idx, Type type) where T : class + { + if (idx > 0 || idx >= Arity) throw new ArgumentException("Invalid component index"); + if (type == null) throw new ArgumentNullException(nameof(type)); + var c = Components[idx]; + if (c?.GetType() != type) throw new TypeMismatchException($"Component {idx - 1} is not of type {type}"); + return c as T; + } + + /// + public override int GetHashCode() + { + var hash = Arity.GetHashCode(); + hash = Components.OfType() + .Aggregate(hash, (current, component) => current * 31 + component.GetHashCode()); + return hash; + } + + /// + /// Returns true if this ID is equivalent to null. + /// + /// + protected virtual bool IsNullId() + { + return Components.Any(o => o != null); + } + + /// + /// The arity of this ID type. + /// + /// + protected abstract int GetConfiguredArity(); +} diff --git a/PG.Commons/PG.Commons/Data/RootIdBase.cs b/PG.Commons/PG.Commons/Data/RootIdBase.cs new file mode 100644 index 000000000..4d8fa0300 --- /dev/null +++ b/PG.Commons/PG.Commons/Data/RootIdBase.cs @@ -0,0 +1,40 @@ +using System; + +namespace PG.Commons.Data; + +/// +public abstract class RootIdBase : IdBase, IComparable> where T : class, IComparable +{ + /// + protected RootIdBase(T rawId) : base(rawId) + { + } + + /// + public int CompareTo(RootIdBase? other) + { + return other == null ? 1 : Unwrap().CompareTo(other.Unwrap()); + } + + /// + protected override int GetConfiguredArity() + { + return 1; + } + + /// + protected override bool IsNullId() + { + return Components[0] == null; + } + + /// + /// Raw value. + /// + /// + /// + public T Unwrap() + { + return Components[0] as T ?? throw new InvalidOperationException(); + } +} diff --git a/PG.Commons/PG.Commons/Data/Serialization/IXmlSerializationSupportService.cs b/PG.Commons/PG.Commons/Data/Serialization/IXmlSerializationSupportService.cs new file mode 100644 index 000000000..16012ffc0 --- /dev/null +++ b/PG.Commons/PG.Commons/Data/Serialization/IXmlSerializationSupportService.cs @@ -0,0 +1,40 @@ +namespace PG.Commons.Data.Serialization; + +/// +/// Simple XML serialization helper service. +/// +public interface IXmlSerializationSupportService +{ + /// + /// Serializes an object to XML and stores it to the file path. + /// + /// + /// + /// + /// + bool SerializeObjectAndStoreToDisc(string filePath, T serializableObject, bool overwrite = true) where T : class; + + /// + /// Serializes the given object to an XML string. + /// + /// + /// + /// + string SerializeObject(T serializableObject) where T : class; + + /// + /// Deserializes an XML file read form disc to an object. + /// + /// + /// + /// + T? DeSerializeObjectFromDisc(string filePath) where T : class; + + /// + /// Deserializes an object from a XML string. + /// + /// + /// + /// + T? DeSerializeObject(string xmlString) where T : class; +} diff --git a/PG.Commons/PG.Commons/Data/Serialization/XmlSerializationSupportService.cs b/PG.Commons/PG.Commons/Data/Serialization/XmlSerializationSupportService.cs new file mode 100644 index 000000000..7ac7d2d1e --- /dev/null +++ b/PG.Commons/PG.Commons/Data/Serialization/XmlSerializationSupportService.cs @@ -0,0 +1,84 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.IO; +using System.Xml; +using System.Xml.Serialization; +using Microsoft.Extensions.Logging; +using PG.Commons.Services; + +namespace PG.Commons.Data.Serialization; + +/// +public sealed class XmlSerializationSupportService : ServiceBase, IXmlSerializationSupportService +{ + /// + public XmlSerializationSupportService(IServiceProvider services) : base(services) + { + } + + /// + public string SerializeObject(T serializableObject) where T : class + { + try + { + var xmlSerializer = new XmlSerializer(typeof(T)); + var stringWriter = new StringWriter(); + using var writer = XmlWriter.Create(stringWriter); + xmlSerializer.Serialize(writer, serializableObject); + return stringWriter.ToString(); + } + catch (Exception e) + { + Logger.LogError("An exception occurred whilst serializing: {}", e); + } + + return string.Empty; + } + + /// + public T? DeSerializeObjectFromDisc(string filePath) where T : class + { + var fileInfo = FileSystem.FileInfo.New(filePath); + if (!fileInfo.Exists) throw new FileNotFoundException("The file {} could not be found!", filePath); + return DeSerializeObject(FileSystem.File.ReadAllText(filePath)); + } + + /// + public T? DeSerializeObject(string xmlString) where T : class + { + if (string.IsNullOrEmpty(xmlString)) return null; + var xmlSerializer = new XmlSerializer(typeof(T)); + var stringReader = new StringReader(xmlString); + using var reader = XmlReader.Create(stringReader); + var obj = xmlSerializer.Deserialize(reader); + return obj as T; + } + + /// + public bool SerializeObjectAndStoreToDisc(string filePath, T serializableObject, bool overwrite = false) + where T : class + { + try + { + var fileInfo = FileSystem.FileInfo.New(filePath); + if (fileInfo.Exists && !overwrite) + throw new UnauthorizedAccessException( + $"The file already exists. Force overwrite with {nameof(overwrite)}=true."); + fileInfo.Delete(); + + var directoyInfo = fileInfo.Directory; + if (directoyInfo is { Exists: false }) FileSystem.Directory.CreateDirectory(directoyInfo.FullName); + + FileSystem.File.WriteAllText(filePath, SerializeObject(serializableObject)); + } + catch (Exception e) + { + Logger.LogError("An exception occurred whilst serializing: {}", e); + return false; + } + + return true; + } +} diff --git a/PG.Commons/PG.Commons/Exceptions/TypeMismatchException.cs b/PG.Commons/PG.Commons/Exceptions/TypeMismatchException.cs new file mode 100644 index 000000000..4fc526ed0 --- /dev/null +++ b/PG.Commons/PG.Commons/Exceptions/TypeMismatchException.cs @@ -0,0 +1,25 @@ +using System; + +namespace PG.Commons.Exceptions; + +/// +/// Thrown if the provided type does not match the requested type. Usually thrown when a generic request for a key +/// component is not logically sound. +/// +public class TypeMismatchException : Exception +{ + /// + public TypeMismatchException() + { + } + + /// + public TypeMismatchException(string message) : base(message) + { + } + + /// + public TypeMismatchException(string message, Exception inner) : base(message, inner) + { + } +} \ No newline at end of file diff --git a/PG.Commons/PG.Commons/PG.Commons.csproj.DotSettings b/PG.Commons/PG.Commons/PG.Commons.csproj.DotSettings index 13d95ad07..589e1da7f 100644 --- a/PG.Commons/PG.Commons/PG.Commons.csproj.DotSettings +++ b/PG.Commons/PG.Commons/PG.Commons.csproj.DotSettings @@ -1,2 +1,10 @@ - - True \ No newline at end of file + + + + True \ No newline at end of file diff --git a/PG.Commons/PG.Commons/PGServiceContribution.cs b/PG.Commons/PG.Commons/PGServiceContribution.cs index 7ec9b6b8e..2e2af95b5 100644 --- a/PG.Commons/PG.Commons/PGServiceContribution.cs +++ b/PG.Commons/PG.Commons/PGServiceContribution.cs @@ -1,6 +1,7 @@ using AnakinRaW.CommonUtilities.Hashing; using Microsoft.Extensions.DependencyInjection; using PG.Commons.Attributes; +using PG.Commons.Data.Serialization; using PG.Commons.Extensibility; using PG.Commons.Hashing; @@ -16,5 +17,6 @@ public void ContributeServices(IServiceCollection serviceCollection) { serviceCollection.AddSingleton(sp => new Crc32HashingProvider()); serviceCollection.AddSingleton(sp => new Crc32HashingService(sp)); + serviceCollection.AddSingleton(sp => new XmlSerializationSupportService(sp)); } } \ No newline at end of file diff --git a/PG.Commons/PG.Commons/Utilities/StringUtilities.cs b/PG.Commons/PG.Commons/Utilities/StringUtilities.cs index fe099d4a1..8c41a8563 100644 --- a/PG.Commons/PG.Commons/Utilities/StringUtilities.cs +++ b/PG.Commons/PG.Commons/Utilities/StringUtilities.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for details. using System; +using System.Linq; using System.Runtime.CompilerServices; using System.Text; #if NETSTANDARD2_0 @@ -12,13 +13,14 @@ namespace PG.Commons.Utilities; /// -/// Provides primitive helper methods for strings. +/// Provides primitive helper methods for strings. /// public static class StringUtilities { /// - /// Checks whether a given string, when converted to bytes, is not longer than the max value of an . - /// Throws an if the string is longer. + /// Checks whether a given string, when converted to bytes, is not longer than the max value of an + /// . + /// Throws an if the string is longer. /// /// The string to validate. /// The encoding that shall be used to get the string length. @@ -32,17 +34,18 @@ public static ushort ValidateStringByteSizeUInt16(ReadOnlySpan value, Enco if (encoding == null) throw new ArgumentNullException(nameof(encoding)); - var size = encoding.GetByteCount(value); + var size = encoding.GetByteCount(value); if (size is < 0 or > ushort.MaxValue) - throw new ArgumentException($"The value is longer than the expected {ushort.MaxValue} characters.", nameof(value)); + throw new ArgumentException($"The value is longer than the expected {ushort.MaxValue} characters.", + nameof(value)); return (ushort)size; } /// - /// Checks whether a given character sequence has no more characters than the max value of an . - /// Throws an if the string is longer. + /// Checks whether a given character sequence has no more characters than the max value of an . + /// Throws an if the string is longer. /// /// The string to validate. /// The actual length of the value in characters. @@ -55,13 +58,14 @@ public static ushort ValidateStringCharLengthUInt16(ReadOnlySpan value) var length = value.Length; if (length is < 0 or > ushort.MaxValue) - throw new ArgumentException($"The value is longer that the expected {ushort.MaxValue} characters.", nameof(value)); + throw new ArgumentException($"The value is longer that the expected {ushort.MaxValue} characters.", + nameof(value)); return (ushort)length; } /// - /// Throws an if the given character sequence contains non-ASCII characters. + /// Throws an if the given character sequence contains non-ASCII characters. /// /// The character sequence to validate. /// The character sequence contains non-ASCII characters. @@ -74,7 +78,7 @@ public static void ValidateIsAsciiOnly(ReadOnlySpan value) } /// - /// Throws an if the given character sequence contains non-ASCII characters. + /// Throws an if the given character sequence contains non-ASCII characters. /// /// The character sequence to validate. /// The character sequence contains non-ASCII characters. @@ -85,10 +89,27 @@ public static bool IsAsciiOnly(ReadOnlySpan value) throw new ArgumentNullException(nameof(value)); foreach (var ch in value) - { if ((uint)ch > '\x007f') return false; - } return true; } -} \ No newline at end of file + + /// + /// Basic validation algorithm. + /// + /// + /// + public static string Validate(string? s) + { + var stringBuilder = new StringBuilder(); + + if (string.IsNullOrEmpty(s)) return string.Empty; + + foreach (var t in s.Where(t => t == 0x9 || t == 0xA || t == 0xD || + (t >= 0x20 && t <= 0xD7FF) || + (t >= 0xE000 && t <= 0xFFFD))) + stringBuilder.Append(t); + + return stringBuilder.ToString(); + } +} diff --git a/PG.Commons/PG.Commons/Utilities/XmlUtilities.cs b/PG.Commons/PG.Commons/Utilities/XmlUtilities.cs new file mode 100644 index 000000000..a9b1e2d0f --- /dev/null +++ b/PG.Commons/PG.Commons/Utilities/XmlUtilities.cs @@ -0,0 +1,46 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +namespace PG.Commons.Utilities; + +/// +/// XML file utility. +/// +public static class XmlUtilities +{ + /// + /// Escape XML Content. + /// + /// + /// + public static string? EscapeXml(string? s) + { + if (string.IsNullOrEmpty(s)) return s; + + var returnString = s!; + returnString = returnString.Replace("&", "&"); + returnString = returnString.Replace("<", "<"); + returnString = returnString.Replace(">", ">"); + returnString = returnString.Replace("'", "'"); + returnString = returnString.Replace("\"", """); + return returnString; + } + + /// + /// Unescape XML content. + /// + /// + /// + public static string? UnescapeXml(string? s) + { + if (string.IsNullOrEmpty(s)) return s; + + var returnString = s!; + returnString = returnString.Replace("'", "'"); + returnString = returnString.Replace(""", "\""); + returnString = returnString.Replace(">", ">"); + returnString = returnString.Replace("<", "<"); + returnString = returnString.Replace("&", "&"); + return returnString; + } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Data/IIdCompletenessTest.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Data/IIdCompletenessTest.cs new file mode 100644 index 000000000..41c474b45 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Data/IIdCompletenessTest.cs @@ -0,0 +1,15 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using PG.Commons.Test.Data; + +namespace PG.StarWarsGame.Components.Localisation.Test.Data; + +// ReSharper disable once InconsistentNaming +public class IIdCompletenessTest : IIdTestCompletenessTestBase +{ + protected override string GetConfiguredNamespaceBase() + { + return "PG.StarWarsGame.Components.Localisation"; + } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Data/OrderedTranslationItemIdTest.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Data/OrderedTranslationItemIdTest.cs new file mode 100644 index 000000000..7e7e8a666 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Data/OrderedTranslationItemIdTest.cs @@ -0,0 +1,40 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.Collections.Generic; +using PG.Commons.Test.Data; +using PG.StarWarsGame.Components.Localisation.Repository.Content; + +namespace PG.StarWarsGame.Components.Localisation.Test.Data; + +public class OrderedTranslationItemIdTest : IIdTestBase +{ + protected override List GetConfiguredNullIIds() + { + return + [ + OrderedTranslationItemId.Of(string.Empty), + OrderedTranslationItemId.Of(""), + OrderedTranslationItemId.Of("\t") + ]; + } + + protected override List GetConfiguredValidIIdInitialisers() + { + return + [ + new object[] { "TEST_00" }, + new object[] { "TEST_01" }, + new object[] { "TEST_02" }, + new object[] { "TEST_03" }, + new object[] { "TEST_04" }, + new object[] { "TEST_05" } + ]; + } + + protected override OrderedTranslationItemId CreateId(object[] keyInitialiser) + { + return OrderedTranslationItemId.Of((keyInitialiser[0] as string)!) ?? throw new InvalidOperationException(); + } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/IO/Xml/XmlExportHandlerTest.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/IO/Xml/XmlExportHandlerTest.cs new file mode 100644 index 000000000..4eac65b88 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/IO/Xml/XmlExportHandlerTest.cs @@ -0,0 +1,75 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.IO.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using PG.Commons.Extensibility; +using PG.StarWarsGame.Components.Localisation.IO; +using PG.StarWarsGame.Components.Localisation.IO.Xml; +using PG.StarWarsGame.Components.Localisation.Languages.BuiltIn; +using PG.StarWarsGame.Components.Localisation.Repository.Builtin; +using PG.StarWarsGame.Components.Localisation.Repository.Content; +using Testably.Abstractions.Testing; +using Xunit; + +namespace PG.StarWarsGame.Components.Localisation.Test.IO.Xml; + +public class XmlExportHandlerTest +{ + private readonly MockFileSystem _fileSystem = new(); + private readonly IExportHandler _handler; + + public XmlExportHandlerTest() + { + var sc = new ServiceCollection(); + sc.AddSingleton(_fileSystem); + sc.CollectPgServiceContributions(); + _handler = sc.BuildServiceProvider().GetRequiredService>(); + } + + [Fact] + public void Test_Export_WithEmptyRepository_NoFileCreated() + { + var repository = new InMemoryOrderedTranslationRepository(); + Assert.Empty(repository.Content); + const string exportBase = "./test_export_empty"; + const string exportBaseFile = "EMPTY_EXPORT"; + var strategy = new XmlOutputStrategy(_fileSystem.DirectoryInfo.New(exportBase), exportBaseFile); + _handler.Export(strategy, repository); + var info = _fileSystem.FileInfo.New(strategy.FilePath); + Assert.False(info.Exists); + } + + [Fact] + public void Test_Export_WithRepositoryWithoutContentBesidesLanguages_NoFileCreated() + { + var repository = new InMemoryOrderedTranslationRepository(); + repository.AddLanguage(new EnglishAlamoLanguageDefinition()); + repository.AddLanguage(new GermanAlamoLanguageDefinition()); + const string exportBase = "./test_export_languages_only"; + const string exportBaseFile = "EMPTY_LANGUAGES"; + var strategy = new XmlOutputStrategy(_fileSystem.DirectoryInfo.New(exportBase), exportBaseFile); + _handler.Export(strategy, repository); + var info = _fileSystem.FileInfo.New(strategy.FilePath); + Assert.False(info.Exists); + } + + [Fact] + public void Test_Export_WithRepositoryWithSingleEntry_FileCreated() + { + var repository = new InMemoryOrderedTranslationRepository(); + repository.AddLanguage(new EnglishAlamoLanguageDefinition()); + repository.AddOrUpdateTranslationItem(new EnglishAlamoLanguageDefinition(), OrderedTranslationItem.Of( + new TranslationItemContent + { + Key = "TEST_00", + Value = "English translation for TEST_00" + })); + const string exportBase = "./test_single_entry"; + const string exportBaseFile = "VALID_FILE"; + var strategy = new XmlOutputStrategy(_fileSystem.DirectoryInfo.New(exportBase), exportBaseFile); + _handler.Export(strategy, repository); + var info = _fileSystem.FileInfo.New(strategy.FilePath); + Assert.True(info.Exists); + } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/IO/Xml/XmlImportHandlerTest.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/IO/Xml/XmlImportHandlerTest.cs new file mode 100644 index 000000000..90469db3b --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/IO/Xml/XmlImportHandlerTest.cs @@ -0,0 +1,179 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using PG.Commons.Extensibility; +using PG.StarWarsGame.Components.Localisation.IO; +using PG.StarWarsGame.Components.Localisation.IO.Xml; +using PG.StarWarsGame.Components.Localisation.Languages.BuiltIn; +using PG.StarWarsGame.Components.Localisation.Repository.Builtin; +using PG.StarWarsGame.Components.Localisation.Repository.Content; +using PG.StarWarsGame.Files.DAT.Services.Builder.Validation; +using PG.Testing; +using Testably.Abstractions.Testing; +using Xunit; + +namespace PG.StarWarsGame.Components.Localisation.Test.IO.Xml; + +public class XmlImportHandlerTest +{ + private const string EmptyXml = "empty.xml"; + private const string MultiKeysXml = "multi_keys.xml"; + private const string SingleKeyXml = "single_key.xml"; + private const string InvalidLanguage = "invalid_language.xml"; + private const string InvalidKey = "invalid_key.xml"; + private const string KeyWithoutTranslations = "key_without_translations.xml"; + + private readonly MockFileSystem _fileSystem = new(); + private readonly IImportHandler _handler; + + public XmlImportHandlerTest() + { + var sc = new ServiceCollection(); + sc.AddSingleton(_fileSystem); + sc.AddSingleton(new EmpireAtWarKeyValidator()); + sc.CollectPgServiceContributions(); + _handler = sc.BuildServiceProvider().GetRequiredService>(); + } + + private string CreateMockFilePath(string directory, string fileName) + { + return _fileSystem.Path.Combine(_fileSystem.Directory.CreateDirectory(directory).FullName, fileName); + } + + private static string GetResourcePath(string resourceName) + { + return $"IO.Xml.v1.Serializable.translation_manifest_{resourceName}"; + } + + [Fact] + public void Test_Import_WithEmptyXml() + { + var filePath = CreateMockFilePath("./empty_test", EmptyXml); + var resourcePath = GetResourcePath(EmptyXml); + TestUtility.CopyEmbeddedResourceToMockFilesystem(typeof(XmlImportHandlerTest), resourcePath, filePath, + _fileSystem); + var repository = new InMemoryOrderedTranslationRepository(); + _handler.Import(new XmlInputStrategy(filePath), repository); + Assert.Empty(repository.Content); + } + + [Fact] + public void Test_Import_WithSingleKeyXml() + { + var filePath = CreateMockFilePath("./single_key_test", SingleKeyXml); + var resourcePath = GetResourcePath(SingleKeyXml); + TestUtility.CopyEmbeddedResourceToMockFilesystem(typeof(XmlImportHandlerTest), resourcePath, filePath, + _fileSystem); + var repository = new InMemoryOrderedTranslationRepository(); + _handler.Import(new XmlInputStrategy(filePath), repository); + Assert.Equal(5, repository.Content.Keys.Count()); + Assert.Equal(5, repository.Content.Values.Count()); + foreach (var kvp in repository.Content) + { + Assert.Single(kvp.Value); + Assert.NotNull(kvp.Value.First()); + Assert.True(OrderedTranslationItemId.Of("TEST_KEY_00")?.Equals(kvp.Value.First().ItemId)); + Assert.Equal(new TranslationItemContent { Key = "TEST_KEY_00", Value = "Test text for key TEST_KEY_00" }, + kvp.Value.First().Content); + } + } + + [Fact] + public void Test_Import_WithMultiKeyXml() + { + var filePath = CreateMockFilePath("./multi_keys_test", MultiKeysXml); + var resourcePath = GetResourcePath(MultiKeysXml); + TestUtility.CopyEmbeddedResourceToMockFilesystem(typeof(XmlImportHandlerTest), resourcePath, filePath, + _fileSystem); + var repository = new InMemoryOrderedTranslationRepository(); + _handler.Import(new XmlInputStrategy(filePath), repository); + Assert.Equal(5, repository.Content.Keys.Count()); + + Assert.NotNull(repository.Content[new EnglishAlamoLanguageDefinition()]); + Assert.NotNull(repository.Content[new GermanAlamoLanguageDefinition()]); + Assert.NotNull(repository.Content[new FrenchAlamoLanguageDefinition()]); + Assert.NotNull(repository.Content[new SpanishAlamoLanguageDefinition()]); + Assert.NotNull(repository.Content[new ItalianAlamoLanguageDefinition()]); + + Assert.Equal(5, repository.Content.Values.Count()); + + foreach (var kvp in repository.Content) Assert.Equal(2, kvp.Value.Count); + } + + [Fact] + public void Test_Import_WithInvalidLanguageLenient_IsSkipped() + { + var filePath = CreateMockFilePath("./invalid_language_test", InvalidLanguage); + var resourcePath = GetResourcePath(InvalidLanguage); + TestUtility.CopyEmbeddedResourceToMockFilesystem(typeof(XmlImportHandlerTest), resourcePath, filePath, + _fileSystem); + var repository = new InMemoryOrderedTranslationRepository(); + _handler.Import(new XmlInputStrategy(filePath), repository); + Assert.Empty(repository.Content); + Assert.Empty(repository.Content.Keys); + Assert.Empty(repository.Content.Values); + } + + [Fact] + public void Test_Import_WithInvalidLanguageStrict_ThrowsInvalidDataException() + { + var filePath = CreateMockFilePath("./invalid_language_test", InvalidLanguage); + var resourcePath = GetResourcePath(InvalidLanguage); + TestUtility.CopyEmbeddedResourceToMockFilesystem(typeof(XmlImportHandlerTest), resourcePath, filePath, + _fileSystem); + var repository = new InMemoryOrderedTranslationRepository(); + Assert.Throws(() => + _handler.Import(new XmlInputStrategy(filePath, IInputStrategy.ValidationLevel.Strict), repository)); + } + + [Fact] + public void Test_Import_WithInvalidKeyLenient_IsSkipped() + { + var filePath = CreateMockFilePath("./invalid_key_test", InvalidKey); + var resourcePath = GetResourcePath(InvalidKey); + TestUtility.CopyEmbeddedResourceToMockFilesystem(typeof(XmlImportHandlerTest), resourcePath, filePath, + _fileSystem); + var repository = new InMemoryOrderedTranslationRepository(); + _handler.Import(new XmlInputStrategy(filePath), repository); + Assert.Empty(repository.Content); + Assert.Empty(repository.Content.Keys); + Assert.Empty(repository.Content.Values); + } + + [Fact] + public void Test_Import_WithInvalidKeyStrict_ThrowsInvalidDataException() + { + var filePath = CreateMockFilePath("./invalid_key_test", InvalidKey); + var resourcePath = GetResourcePath(InvalidKey); + TestUtility.CopyEmbeddedResourceToMockFilesystem(typeof(XmlImportHandlerTest), resourcePath, filePath, + _fileSystem); + var repository = new InMemoryOrderedTranslationRepository(); + Assert.Throws(() => + _handler.Import(new XmlInputStrategy(filePath, IInputStrategy.ValidationLevel.Strict), repository)); + } + + [Fact] + public void Test_Import_KeyWithoutTranslations_IsSkipped() + { + var filePath = CreateMockFilePath("./key_without_translations_test", KeyWithoutTranslations); + var resourcePath = GetResourcePath(KeyWithoutTranslations); + TestUtility.CopyEmbeddedResourceToMockFilesystem(typeof(XmlImportHandlerTest), resourcePath, filePath, + _fileSystem); + var repository = new InMemoryOrderedTranslationRepository(); + _handler.Import(new XmlInputStrategy(filePath), repository); + Assert.Equal(5, repository.Content.Keys.Count()); + Assert.Equal(5, repository.Content.Values.Count()); + foreach (var kvp in repository.Content) + { + Assert.Single(kvp.Value); + Assert.NotNull(kvp.Value.First()); + Assert.True(OrderedTranslationItemId.Of("TEST_KEY_01")?.Equals(kvp.Value.First().ItemId)); + Assert.Equal(new TranslationItemContent { Key = "TEST_KEY_01", Value = "Test text for key TEST_KEY_01" }, + kvp.Value.First().Content); + } + } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/IO/Xml/XmlInputStrategyTest.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/IO/Xml/XmlInputStrategyTest.cs new file mode 100644 index 000000000..fe228bd0c --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/IO/Xml/XmlInputStrategyTest.cs @@ -0,0 +1,36 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.Linq; +using PG.StarWarsGame.Components.Localisation.IO; +using PG.StarWarsGame.Components.Localisation.IO.Xml; +using Xunit; + +namespace PG.StarWarsGame.Components.Localisation.Test.IO.Xml; + +public class XmlInputStrategyTest +{ + [Fact] + public void Test_XmlInputStrategy_InvalidAttributesThrowOnAccess() + { + var strategy = new XmlInputStrategy("test.xml", IInputStrategy.ValidationLevel.Strict); + Assert.Throws(() => strategy.BaseDirectory); + Assert.Throws(() => strategy.FileFilter); + } + + [Fact] + public void Test_XmlInputStrategy_StaticValuesReturnExpectedAttributes() + { + var strategy = new XmlInputStrategy("test.xml", IInputStrategy.ValidationLevel.Strict); + Assert.Equal(IInputStrategy.FileImportGrouping.Single, strategy.ImportGrouping); + Assert.Equal(strategy.FilePath, strategy.FilePaths.First()); + } + + [Fact] + public void Test_XmlInputStrategy_InvalidArgumentsThrowIllegalArgumentException() + { + Assert.Throws(() => new XmlInputStrategy(null)); + Assert.Throws(() => new XmlInputStrategy(string.Empty)); + } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/IO/Xml/XmlOutputStrategyTest.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/IO/Xml/XmlOutputStrategyTest.cs new file mode 100644 index 000000000..f2548abc2 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/IO/Xml/XmlOutputStrategyTest.cs @@ -0,0 +1,22 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using PG.StarWarsGame.Components.Localisation.IO.Xml; +using Testably.Abstractions.Testing; +using Xunit; + +namespace PG.StarWarsGame.Components.Localisation.Test.IO.Xml; + +public class XmlOutputStrategyTest +{ + private readonly MockFileSystem _fileSystem = new(); + + [Fact] + public void Test_XmlOutputStrategy_InvalidArgumentsThrowIllegalArgumentException() + { + Assert.Throws(() => new XmlOutputStrategy(_fileSystem.DirectoryInfo.New("./"), null)); + Assert.Throws(() => + new XmlOutputStrategy(_fileSystem.DirectoryInfo.New("./"), string.Empty)); + } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/LocalisationTestConstants.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/LocalisationTestConstants.cs new file mode 100644 index 000000000..9ab4d5412 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/LocalisationTestConstants.cs @@ -0,0 +1,27 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.Collections.Generic; +using PG.StarWarsGame.Components.Localisation.Languages.BuiltIn; + +namespace PG.StarWarsGame.Components.Localisation.Test; + +public sealed class LocalisationTestConstants +{ + public static readonly IList RegisteredLanguageDefinitions = new List + { + //[gruenwaldlu, 2021-04-18-12:02:51+2]: All officially supported languages are listed below. + typeof(ChineseAlamoLanguageDefinition), typeof(EnglishAlamoLanguageDefinition), + typeof(FrenchAlamoLanguageDefinition), typeof(GermanAlamoLanguageDefinition), + typeof(ItalianAlamoLanguageDefinition), typeof(JapaneseAlamoLanguageDefinition), + typeof(KoreanAlamoLanguageDefinition), typeof(PolishAlamoLanguageDefinition), + typeof(RussianAlamoLanguageDefinition), typeof(SpanishAlamoLanguageDefinition), + typeof(ThaiAlamoLanguageDefinition) + }; + + public static readonly Type DefaultLanguage = typeof(EnglishAlamoLanguageDefinition); + + public static readonly string XmlV1ResourcePathBase = + $"{typeof(LocalisationTestConstants).Assembly.Location}.Resources.IO.Xml.v1.Seralizable.translation_manifest_"; +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/PG.StarWarsGame.Components.Localisation.Test.csproj b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/PG.StarWarsGame.Components.Localisation.Test.csproj new file mode 100644 index 000000000..1632972e1 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/PG.StarWarsGame.Components.Localisation.Test.csproj @@ -0,0 +1,45 @@ + + + + + false + net8.0 + $(TargetFrameworks);net48 + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Repository/Builtin/InMemoryOrderedTranslationRepositoryTest.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Repository/Builtin/InMemoryOrderedTranslationRepositoryTest.cs new file mode 100644 index 000000000..e994fe0a5 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Repository/Builtin/InMemoryOrderedTranslationRepositoryTest.cs @@ -0,0 +1,79 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.IO.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using PG.Commons.Extensibility; +using PG.StarWarsGame.Components.Localisation.Languages.BuiltIn; +using PG.StarWarsGame.Components.Localisation.Repository.Builtin; +using PG.StarWarsGame.Components.Localisation.Repository.Content; +using PG.StarWarsGame.Components.Localisation.Services; +using Testably.Abstractions.Testing; +using Xunit; + +namespace PG.StarWarsGame.Components.Localisation.Test.Repository.Builtin; + +public class InMemoryOrderedTranslationRepositoryTest +{ + private readonly MockFileSystem _fileSystem = new(); + private readonly IServiceProvider _serviceProvider; + + public InMemoryOrderedTranslationRepositoryTest() + { + var sc = new ServiceCollection(); + sc.AddSingleton(_fileSystem); + sc.CollectPgServiceContributions(); + _serviceProvider = sc.BuildServiceProvider(); + } + + [Fact] + public void Test_AddLanguage_AllPresent() + { + var repository = new InMemoryOrderedTranslationRepository(); + var service = _serviceProvider.GetService(); + Assert.NotNull(service); + foreach (var l in service.GetRegisteredLanguages()) Assert.True(repository.AddLanguage(l)); + Assert.NotEmpty(repository.Content); + Assert.Equal(service.GetRegisteredLanguages().Count, repository.Content.Count); + } + + [Fact] + public void Test_RemoveLanguage_AllPresentMinusOne() + { + var repository = new InMemoryOrderedTranslationRepository(); + var service = _serviceProvider.GetService(); + Assert.NotNull(service); + foreach (var l in service.GetRegisteredLanguages()) Assert.True(repository.AddLanguage(l)); + Assert.NotEmpty(repository.Content); + Assert.Equal(service.GetRegisteredLanguages().Count, repository.Content.Count); + + repository.RemoveLanguage(service.GetDefaultLanguageDefinition()); + Assert.NotEmpty(repository.Content); + Assert.Equal(service.GetRegisteredLanguages().Count - 1, repository.Content.Count); + } + + [Fact] + public void Test_AddOrUpdateTranslationItem_Add() + { + var repository = new InMemoryOrderedTranslationRepository(); + var service = _serviceProvider.GetService(); + Assert.NotNull(service); + foreach (var l in service.GetRegisteredLanguages()) Assert.True(repository.AddLanguage(l)); + Assert.NotEmpty(repository.Content); + Assert.Equal(service.GetRegisteredLanguages().Count, repository.Content.Count); + + repository.AddOrUpdateTranslationItem(new EnglishAlamoLanguageDefinition(), + OrderedTranslationItem.Of(new TranslationItemContent { Key = "TEST_00", Value = "Test translation" })); + + Assert.Single(repository.Content[new EnglishAlamoLanguageDefinition()]); + foreach (var l in service.GetRegisteredLanguages()) + { + if (l.Equals(new EnglishAlamoLanguageDefinition())) continue; + Assert.Empty(repository[l]); + } + + Assert.NotNull(repository.GetTranslationItem(new EnglishAlamoLanguageDefinition(), + OrderedTranslationItemId.Of("TEST_00")!)); + } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Resources/IO/Xml/v1/Serializable/translation_manifest_empty.xml b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Resources/IO/Xml/v1/Serializable/translation_manifest_empty.xml new file mode 100644 index 000000000..e68aace61 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Resources/IO/Xml/v1/Serializable/translation_manifest_empty.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Resources/IO/Xml/v1/Serializable/translation_manifest_invalid_key.xml b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Resources/IO/Xml/v1/Serializable/translation_manifest_invalid_key.xml new file mode 100644 index 000000000..9e20f1747 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Resources/IO/Xml/v1/Serializable/translation_manifest_invalid_key.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Resources/IO/Xml/v1/Serializable/translation_manifest_invalid_language.xml b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Resources/IO/Xml/v1/Serializable/translation_manifest_invalid_language.xml new file mode 100644 index 000000000..db27fc634 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Resources/IO/Xml/v1/Serializable/translation_manifest_invalid_language.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Resources/IO/Xml/v1/Serializable/translation_manifest_key_without_translations.xml b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Resources/IO/Xml/v1/Serializable/translation_manifest_key_without_translations.xml new file mode 100644 index 000000000..31dbaaadb --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Resources/IO/Xml/v1/Serializable/translation_manifest_key_without_translations.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Resources/IO/Xml/v1/Serializable/translation_manifest_multi_keys.xml b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Resources/IO/Xml/v1/Serializable/translation_manifest_multi_keys.xml new file mode 100644 index 000000000..c04d3139c --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Resources/IO/Xml/v1/Serializable/translation_manifest_multi_keys.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Resources/IO/Xml/v1/Serializable/translation_manifest_single_key.xml b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Resources/IO/Xml/v1/Serializable/translation_manifest_single_key.xml new file mode 100644 index 000000000..d55f2d961 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Resources/IO/Xml/v1/Serializable/translation_manifest_single_key.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Services/AlamoLanguageSupportServiceTest.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Services/AlamoLanguageSupportServiceTest.cs new file mode 100644 index 000000000..16292eb1d --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.Test/Services/AlamoLanguageSupportServiceTest.cs @@ -0,0 +1,132 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO.Abstractions; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using PG.Commons.Extensibility; +using PG.StarWarsGame.Components.Localisation.Attributes; +using PG.StarWarsGame.Components.Localisation.Languages; +using PG.StarWarsGame.Components.Localisation.Languages.BuiltIn; +using PG.StarWarsGame.Components.Localisation.Services; +using Testably.Abstractions.Testing; +using Xunit; + +namespace PG.StarWarsGame.Components.Localisation.Test.Services; + +public class AlamoLanguageSupportServiceTest +{ + private readonly HashSet _builtinTypes = new() + { + typeof(ChineseAlamoLanguageDefinition), + typeof(EnglishAlamoLanguageDefinition), + typeof(FrenchAlamoLanguageDefinition), + typeof(GermanAlamoLanguageDefinition), + typeof(ItalianAlamoLanguageDefinition), + typeof(JapaneseAlamoLanguageDefinition), + typeof(KoreanAlamoLanguageDefinition), + typeof(PolishAlamoLanguageDefinition), + typeof(RussianAlamoLanguageDefinition), + typeof(SpanishAlamoLanguageDefinition), + typeof(ThaiAlamoLanguageDefinition) + }; + + private readonly MockFileSystem _fileSystem = new(); + private readonly IAlamoLanguageSupportService _service; + + public AlamoLanguageSupportServiceTest() + { + var sc = new ServiceCollection(); + sc.AddSingleton(_fileSystem); + sc.CollectPgServiceContributions(); + _service = sc.BuildServiceProvider().GetRequiredService(); + } + + [Fact] + public void Test_DefaultLanguageDefinitionDefinedExactlyOnce() + { + var languageDefinitions = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(assemblyTypes => assemblyTypes.GetTypes()) + .Where(assemblyType => typeof(IAlamoLanguageDefinition).IsAssignableFrom(assemblyType) && + assemblyType is { IsClass: true, IsAbstract: false }) + .Where(t => t.GetCustomAttribute(typeof(DefaultLanguageAttribute)) != null) + .ToList(); + Assert.Single(languageDefinitions); + Assert.NotNull(languageDefinitions[0]); + } + + [Fact] + public void Test_GetDefaultLanguageDefinition_ReturnsEnglish() + { + var def = _service.GetDefaultLanguageDefinition(); + Assert.NotNull(def); + Assert.Equal(def, new EnglishAlamoLanguageDefinition()); + } + + [Theory] + [InlineData(null, typeof(EnglishAlamoLanguageDefinition))] + [InlineData(typeof(EnglishAlamoLanguageDefinition), typeof(EnglishAlamoLanguageDefinition))] + [InlineData(typeof(ChineseAlamoLanguageDefinition), typeof(ChineseAlamoLanguageDefinition))] + public void Test_GetFallbackLanguageDefinition_ReturnsDesired(Type? definition, Type expected) + { + var exp = (IAlamoLanguageDefinition)Activator.CreateInstance(expected)!; + if (definition != null) + _service.WithOverrideFallbackLanguage((IAlamoLanguageDefinition)Activator.CreateInstance(definition)!); + else + _service.WithOverrideFallbackLanguage(new EnglishAlamoLanguageDefinition()); + Assert.Equal(exp, _service.GetFallbackLanguageDefinition()); + } + + [Fact] + public void Test_IsBuiltInLanguageDefinition_ReturnsTrue() + { + foreach (var b in _builtinTypes) + Assert.True(_service.IsBuiltInLanguageDefinition((IAlamoLanguageDefinition)Activator.CreateInstance(b)!)); + } + + [Fact] + public void Test_IsBuiltInLanguageDefinition_ReturnsFalse() + { + Assert.False(_service.IsBuiltInLanguageDefinition(new TestAlamoLanguageDefinition())); + } + + [Fact] + public void Test_AllPresentLanguageIdentifiersAreValid() + { + var types = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(assemblyTypes => assemblyTypes.GetTypes()) + .Where(assemblyType => typeof(IAlamoLanguageDefinition).IsAssignableFrom(assemblyType) && + assemblyType is { IsClass: true, IsAbstract: false }) + .ToList(); + foreach (var type in types) Assert.True(typeof(AlamoLanguageDefinitionBase).IsAssignableFrom(type)); + } + + [Fact] + public void Test_CreateLanguageIdentifierMapping_AllPresent() + { + var defs = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(assemblyTypes => assemblyTypes.GetTypes()) + .Where(assemblyType => typeof(IAlamoLanguageDefinition).IsAssignableFrom(assemblyType) && + assemblyType is { IsClass: true, IsAbstract: false }) + .Select(t => (IAlamoLanguageDefinition)Activator.CreateInstance(t)) + .ToList(); + var map = _service.CreateLanguageIdentifierMapping(); + Assert.Equal(defs.Count, map.Count); + foreach (var def in defs) + { + Assert.True(typeof(AlamoLanguageDefinitionBase).IsAssignableFrom(def.GetType())); + Assert.NotNull(map[def.LanguageIdentifier]); + Assert.Equal(def, map[def.LanguageIdentifier]); + } + } + + private class TestAlamoLanguageDefinition : AlamoLanguageDefinitionBase + { + protected override string ConfiguredLanguageIdentifier => "TEST"; + protected override CultureInfo ConfiguredCulture => CultureInfo.CurrentCulture; + } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Attributes/DefaultLanguageAttribute.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Attributes/DefaultLanguageAttribute.cs new file mode 100644 index 000000000..7f8261830 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Attributes/DefaultLanguageAttribute.cs @@ -0,0 +1,37 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace PG.StarWarsGame.Components.Localisation.Attributes; + +/// +/// Marks the default (fallback) language. +/// +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +[ExcludeFromCodeCoverage] +public class DefaultLanguageAttribute : Attribute +{ + /// + /// .ctor + /// + public DefaultLanguageAttribute() + { + IsDefaultLanguage = true; + } + + /// + /// .ctor + /// + /// + public DefaultLanguageAttribute(bool isDefaultLanguage) + { + IsDefaultLanguage = isDefaultLanguage; + } + + /// + /// Returns true for the default language. + /// + public bool IsDefaultLanguage { get; } +} \ No newline at end of file diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Attributes/OfficiallySupportedLanguageAttribute.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Attributes/OfficiallySupportedLanguageAttribute.cs new file mode 100644 index 000000000..b2c9ce869 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Attributes/OfficiallySupportedLanguageAttribute.cs @@ -0,0 +1,37 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace PG.StarWarsGame.Components.Localisation.Attributes; + +/// +/// Marks a language as officially supported. +/// +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +[ExcludeFromCodeCoverage] +public sealed class OfficiallySupportedLanguageAttribute : Attribute +{ + /// + /// .ctor + /// + public OfficiallySupportedLanguageAttribute() + { + IsOfficiallySupported = true; + } + + /// + /// .ctor + /// + /// + public OfficiallySupportedLanguageAttribute(bool isOfficiallySupported) + { + IsOfficiallySupported = isOfficiallySupported; + } + + /// + /// Returns true if the language is officially supported. + /// + public bool IsOfficiallySupported { get; } +} \ No newline at end of file diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Exceptions/InvalidDefaultLanguageDefinitionException.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Exceptions/InvalidDefaultLanguageDefinitionException.cs new file mode 100644 index 000000000..60f449d9c --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Exceptions/InvalidDefaultLanguageDefinitionException.cs @@ -0,0 +1,27 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace PG.StarWarsGame.Components.Localisation.Exceptions; + +/// +[ExcludeFromCodeCoverage] +public class InvalidDefaultLanguageDefinitionException : Exception +{ + /// + public InvalidDefaultLanguageDefinitionException() + { + } + + /// + public InvalidDefaultLanguageDefinitionException(string message) : base(message) + { + } + + /// + public InvalidDefaultLanguageDefinitionException(string message, Exception inner) : base(message, inner) + { + } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/IExportHandler.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/IExportHandler.cs new file mode 100644 index 000000000..c0fca6f59 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/IExportHandler.cs @@ -0,0 +1,19 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using PG.StarWarsGame.Components.Localisation.Repository; + +namespace PG.StarWarsGame.Components.Localisation.IO; + +/// +/// The export handler associated with a given +/// +public interface IExportHandler where T : IOutputStrategy +{ + /// + /// Exports the given repository. + /// + /// + /// + void Export(T strategy, ITranslationRepository repository); +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/IImportHandler.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/IImportHandler.cs new file mode 100644 index 000000000..ea231c643 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/IImportHandler.cs @@ -0,0 +1,21 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using PG.StarWarsGame.Components.Localisation.Repository; + +namespace PG.StarWarsGame.Components.Localisation.IO; + +/// +/// The import handler associated with a given +/// +/// +public interface IImportHandler where T : IInputStrategy +{ + /// + /// Imports data into a + /// + /// + /// + /// + void Import(T strategy, ITranslationRepository targetRepository); +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/IInputStrategy.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/IInputStrategy.cs new file mode 100644 index 000000000..db84dfa65 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/IInputStrategy.cs @@ -0,0 +1,85 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.Collections.Generic; +using System.IO; + +namespace PG.StarWarsGame.Components.Localisation.IO; + +/// +/// The strategy used to load an +/// +/// +public interface IInputStrategy +{ + // TODO: implement DatInputStrategy, XmlInputStrategy, CsvInputStrategy -> this should cover most current community tools. + + /// + /// Determines if we are reading a single file, multiple files or all files from a base directory with an optional file + /// filter. + /// + enum FileImportGrouping + { + /// + /// Single file import. + /// + Single, + + /// + /// Multiple file import. + /// + Multi, + + /// + /// Imports all files form a directory matching the optional + /// + BaseDirectory + } + + /// + /// The validation level used against the provided source. + /// + enum ValidationLevel + { + /// + /// Invalid entires are skipped. + /// + Lenient, + + /// + /// Invalid enrties throw an exception. + /// + Strict + } + + /// + /// The + /// + FileImportGrouping ImportGrouping { get; } + + /// + /// The validation level used for the source files. + /// + ValidationLevel Validation { get; } + + /// + /// An optional file filter following the established FileDialog.Filter pattern. + /// For more info check + /// + /// FileDialog.Filter + /// Property + /// + /// + string FileFilter { get; } + + /// + /// A set of file paths. Can be empty. + /// + ISet FilePaths { get; } + + /// + /// The base directory. + /// + DirectoryInfo BaseDirectory { get; } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/IOutputStrategy.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/IOutputStrategy.cs new file mode 100644 index 000000000..7d5e832c8 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/IOutputStrategy.cs @@ -0,0 +1,52 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.IO.Abstractions; + +namespace PG.StarWarsGame.Components.Localisation.IO; + +/// +/// The strategy used to store an +/// +/// +public interface IOutputStrategy +{ + // TODO: implement DatOutputStrategy, XmlOutputStrategy, CsvOutputStrategy -> this should cover most current community tools. + + /// + /// Determines if the is being + /// exported to a single or multiple files. + /// + enum FileExportGrouping + { + /// + /// Single file export. + /// + Single, + + /// + /// Multiple fles export + /// + Multi + } + + /// + /// The desired export grouping. + /// + FileExportGrouping ExportGrouping { get; } + + /// + /// The export file extension + /// + string Extension { get; } + + /// + /// The file name without file extension + /// + string FileName { get; } + + /// + /// The export base path. + /// + IDirectoryInfo ExportBasePath { get; } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/Serializable/v1/Localisation.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/Serializable/v1/Localisation.cs new file mode 100644 index 000000000..e42ee933d --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/Serializable/v1/Localisation.cs @@ -0,0 +1,35 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.Diagnostics.CodeAnalysis; +using System.Xml.Serialization; + +namespace PG.StarWarsGame.Components.Localisation.IO.Xml.Serializable.v1; + +/// +/// XML file definition. +/// +[XmlRoot(ElementName = "LocalisationHolder")] +[ExcludeFromCodeCoverage] +public sealed class Localisation +{ + /// + /// .ctor + /// + public Localisation() + { + TranslationData ??= new TranslationData(); + } + + /// + /// Translation data associated with the key. + /// + [XmlElement(ElementName = "TranslationData")] + public TranslationData TranslationData { get; set; } + + /// + /// Translation Key + /// + [XmlAttribute(AttributeName = "Key")] + public required string Key { get; set; } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/Serializable/v1/LocalisationData.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/Serializable/v1/LocalisationData.cs new file mode 100644 index 000000000..cac05e5ac --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/Serializable/v1/LocalisationData.cs @@ -0,0 +1,29 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Xml.Serialization; + +namespace PG.StarWarsGame.Components.Localisation.IO.Xml.Serializable.v1; + +/// +/// +[XmlRoot(ElementName = "LocalisationData")] +[ExcludeFromCodeCoverage] +public sealed class LocalisationData +{ + /// + /// .ctor + /// + public LocalisationData() + { + LocalisationHolder ??= new List(); + } + + /// + /// Holds s + /// + [XmlElement(ElementName = "Localisation")] + public List LocalisationHolder { get; set; } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/Serializable/v1/Translation.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/Serializable/v1/Translation.cs new file mode 100644 index 000000000..5ec1315ab --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/Serializable/v1/Translation.cs @@ -0,0 +1,63 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Security; +using System.Xml; +using System.Xml.Serialization; +using PG.Commons.Utilities; + +namespace PG.StarWarsGame.Components.Localisation.IO.Xml.Serializable.v1; + +/// +/// XML file definition. +/// +[XmlRoot(ElementName = "TranslationHolder")] +[ExcludeFromCodeCoverage] +public class Translation +{ + private string _text = null!; + + /// + /// The content's language. + /// + [XmlAttribute(AttributeName = "Language")] + public required string Language { get; set; } + + /// + /// Raw translation string. + /// + [XmlIgnore] + public string? Text + { + get => _text; + set => _text = StringUtilities.Validate(value); + } + + /// + /// Translation content. + /// + /// + [XmlText] + public XmlNode[]? CDataContent + { + get + { + var dummy = new XmlDocument(); + return [dummy.CreateCDataSection(XmlUtilities.EscapeXml(Text))]; + } + set + { + if (value == null) + { + Text = "[MISSING]"; + return; + } + + if (value.Length != 1) throw new InvalidOperationException($"Invalid array length {value.Length}"); + var s = new SecurityElement("a", value[0].Value); + Text = XmlUtilities.UnescapeXml(s.Text); + } + } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/Serializable/v1/TranslationData.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/Serializable/v1/TranslationData.cs new file mode 100644 index 000000000..b49f4adff --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/Serializable/v1/TranslationData.cs @@ -0,0 +1,30 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Xml.Serialization; + +namespace PG.StarWarsGame.Components.Localisation.IO.Xml.Serializable.v1; + +/// +/// XML Element description +/// +[XmlRoot(ElementName = "TranslationData")] +[ExcludeFromCodeCoverage] +public class TranslationData +{ + /// + /// .ctor + /// + public TranslationData() + { + TranslationHolder ??= new List(); + } + + /// + /// Holds s. + /// + [XmlElement(ElementName = "Translation")] + public List TranslationHolder { get; set; } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/XmlExportHandler.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/XmlExportHandler.cs new file mode 100644 index 000000000..cdb5b9332 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/XmlExportHandler.cs @@ -0,0 +1,99 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.Commons.Data.Serialization; +using PG.Commons.Services; +using PG.StarWarsGame.Components.Localisation.IO.Xml.Serializable.v1; +using PG.StarWarsGame.Components.Localisation.Languages; +using PG.StarWarsGame.Components.Localisation.Repository; +using PG.StarWarsGame.Components.Localisation.Repository.Content; + +namespace PG.StarWarsGame.Components.Localisation.IO.Xml; + +/// +public class XmlExportHandler : ServiceBase, IExportHandler +{ + /// + public XmlExportHandler(IServiceProvider services) : base(services) + { + } + + /// + public void Export(XmlOutputStrategy strategy, ITranslationRepository repository) + { + if (!repository.Content.Any()) return; + FileSystem.Directory.CreateDirectory(strategy.ExportBasePath.FullName); + var xml = ToXml(repository); + if (xml.LocalisationHolder.Count == 0) + { + Logger.LogWarning("Empty xml export file, skipping export: {}", xml); + return; + } + + Services.GetService()?.SerializeObjectAndStoreToDisc(strategy.FilePath, xml); + } + + /// + /// Converts an to the corresponding XML file representation. + /// + /// + /// + /// + protected LocalisationData ToXml(ITranslationRepository repository) + { + var xml = new LocalisationData(); + var languages = GetLanguages(repository); + var keys = GetKeys(repository); + foreach (var key in keys) + { + var loc = new Serializable.v1.Localisation + { + Key = key.Unwrap().ToUpper(), + TranslationData = new TranslationData() + }; + foreach (var t in languages.Select(lang => new Translation + { + Language = lang.LanguageIdentifier.ToUpper(), + Text = repository.GetTranslationItem(lang, key) + .Content + .Value + })) + loc.TranslationData.TranslationHolder.Add(t); + + xml.LocalisationHolder.Add(loc); + } + + return xml; + } + + /// + /// Fetches all text keys from the repository. + /// + /// + /// + protected ISet GetKeys(ITranslationRepository repository) + { + var keySet = new HashSet(); + foreach (var items in repository.Content.Values) + foreach (var item in items) + keySet.Add(item.ItemId as OrderedTranslationItemId ?? + throw new InvalidOperationException( + $"No valid {nameof(OrderedTranslationItemId)} could be created from {item.ItemId}")); + return keySet; + } + + /// + /// Returns a set of all contained language definitions. + /// + /// + /// + protected HashSet GetLanguages(ITranslationRepository repository) + { + return new HashSet(repository.Content.Keys); + } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/XmlImportHandler.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/XmlImportHandler.cs new file mode 100644 index 000000000..bd634e99f --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/XmlImportHandler.cs @@ -0,0 +1,108 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using PG.Commons; +using PG.Commons.Data.Serialization; +using PG.Commons.Services; +using PG.StarWarsGame.Components.Localisation.IO.Xml.Serializable.v1; +using PG.StarWarsGame.Components.Localisation.Languages; +using PG.StarWarsGame.Components.Localisation.Repository; +using PG.StarWarsGame.Components.Localisation.Repository.Content; +using PG.StarWarsGame.Components.Localisation.Services; +using PG.StarWarsGame.Files.DAT.Services.Builder.Validation; + +namespace PG.StarWarsGame.Components.Localisation.IO.Xml; + +/// +/// The Import handler for the +/// +public class XmlImportHandler : ServiceBase, IImportHandler +{ + /// + public XmlImportHandler(IServiceProvider services) : base(services) + { + } + + /// + public void Import(XmlInputStrategy strategy, ITranslationRepository targetRepository) + { + var data = Services.GetService() + ?.DeSerializeObjectFromDisc(strategy.FilePath); + if (data == null) return; + + FromXml(data, targetRepository, strategy.Validation); + } + + /// + /// Translates a + /// + /// + /// + /// + /// + protected void FromXml(LocalisationData data, ITranslationRepository targetRepository, + IInputStrategy.ValidationLevel validationLevel) + { + var languageMap = + Services.GetService()?.CreateLanguageIdentifierMapping() ?? + throw new LibraryInitialisationException( + $"Required service {nameof(IAlamoLanguageSupportService)} has not bee initialised properly."); + + var contentMap = + languageMap.Values + .ToDictionary>(d => d, + d => new HashSet()); + + foreach (var loc in data.LocalisationHolder) + { + var id = OrderedTranslationItemId.Of(loc.Key); + if (id == null || !Services.GetService()!.Validate(loc.Key)) + { + if (validationLevel != IInputStrategy.ValidationLevel.Lenient) + throw new InvalidDataException( + $"Invalid data found: Could not create a valid {nameof(OrderedTranslationItemId)} from {loc.Key}"); + + Logger.LogWarning("Could not create a valid {} from {}. Skipping.", + nameof(OrderedTranslationItemId), + loc.Key); + continue; + } + + + foreach (var t in loc.TranslationData.TranslationHolder) + { + if (t == null) + { + Logger.LogWarning("Empty translation for key {} found! Skipping.", loc.Key); + continue; + } + + if (!languageMap.TryGetValue(t.Language, out var lang)) + { + if (validationLevel != IInputStrategy.ValidationLevel.Lenient) + throw new InvalidDataException( + $"Invalid data found: Could not determine the {nameof(IAlamoLanguageDefinition)} for {t.Language}"); + + Logger.LogWarning("Could not determine the language for {}. Skipping.", t.Language); + continue; + } + + var c = new TranslationItemContent + { + Key = id.Unwrap(), + Value = t.Text + }; + contentMap[lang].Add(OrderedTranslationItem.Of(c)); + } + } + + foreach (var kvp in contentMap.Where(kvp => kvp.Value.Count != 0)) + targetRepository.AddOrUpdateLanguage(kvp.Key, kvp.Value); + } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/XmlInputStrategy.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/XmlInputStrategy.cs new file mode 100644 index 000000000..71c573d2e --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/XmlInputStrategy.cs @@ -0,0 +1,54 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; + +namespace PG.StarWarsGame.Components.Localisation.IO.Xml; + +/// +/// The XML input strategy. +/// +public readonly record struct XmlInputStrategy : IInputStrategy +{ + /// + /// .ctor + /// + /// + /// + [ExcludeFromCodeCoverage] + public XmlInputStrategy(string filePath, + IInputStrategy.ValidationLevel validation = IInputStrategy.ValidationLevel.Lenient) + { + if (string.IsNullOrWhiteSpace(filePath)) + throw new ArgumentException($"'{filePath}' is not a valid file path.", nameof(filePath)); + + FilePaths = new HashSet { filePath }; + Validation = validation; + } + + /// + /// Convenience method for accessing the only file path required for this strategy. + /// + public string FilePath => FilePaths.Single(); + + /// + public IInputStrategy.FileImportGrouping ImportGrouping => IInputStrategy.FileImportGrouping.Single; + + /// + public IInputStrategy.ValidationLevel Validation { get; } + + /// + public string FileFilter => throw new InvalidOperationException( + $"{nameof(XmlInputStrategy)}.{nameof(FileFilter)} is not supported for {nameof(ImportGrouping)}={nameof(IInputStrategy.FileImportGrouping.Single)}"); + + /// + public ISet FilePaths { get; } + + /// + public DirectoryInfo BaseDirectory => throw new InvalidOperationException( + $"{nameof(XmlInputStrategy)}.{nameof(DirectoryInfo)} is not supported for {nameof(ImportGrouping)}={nameof(IInputStrategy.FileImportGrouping.Single)}"); +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/XmlOutputStrategy.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/XmlOutputStrategy.cs new file mode 100644 index 000000000..50608e3ed --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/IO/Xml/XmlOutputStrategy.cs @@ -0,0 +1,47 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Abstractions; + +namespace PG.StarWarsGame.Components.Localisation.IO.Xml; + +/// +/// The XML output strategy. +/// +[ExcludeFromCodeCoverage] +public readonly record struct XmlOutputStrategy : IOutputStrategy +{ + /// + /// .ctor + /// + /// + /// + /// + public XmlOutputStrategy(IDirectoryInfo exportBasePath, string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(fileName)); + FileName = fileName; + ExportBasePath = exportBasePath; + } + + /// + /// Convenience accessor for the full file path as string. + /// + public string FilePath => $"{Path.Combine(ExportBasePath.FullName, FileName)}.{Extension}"; + + /// + public IDirectoryInfo ExportBasePath { get; } + + /// + public IOutputStrategy.FileExportGrouping ExportGrouping => IOutputStrategy.FileExportGrouping.Single; + + /// + public string Extension => "xml"; + + /// + public string FileName { get; } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/AlamoLanguageDefinitionBase.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/AlamoLanguageDefinitionBase.cs new file mode 100644 index 000000000..f7fb30206 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/AlamoLanguageDefinitionBase.cs @@ -0,0 +1,102 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.Globalization; + +namespace PG.StarWarsGame.Components.Localisation.Languages; + +/// +/// Abstract language definition +/// +public abstract class AlamoLanguageDefinitionBase : IAlamoLanguageDefinition, IComparable +{ + /// + /// A string that is being used to identify the language of the *.DAT file, e.g. a language identifier + /// "english" would produce the file "mastertextfile_english.dat" + /// + protected abstract string ConfiguredLanguageIdentifier { get; } + + /// + /// The .NET Culture that best describes the language. This culture can be used for spell checking, + /// auto-translation between languages, etc. + /// + protected abstract CultureInfo ConfiguredCulture { get; } + + /// + public string LanguageIdentifier => ConfiguredLanguageIdentifier; + + /// + public CultureInfo Culture => ConfiguredCulture; + + /// + public int CompareTo(object obj) + { + if (obj is not IAlamoLanguageDefinition other) + throw new ArgumentException( + $"The type of {obj.GetType()} is not assignable to {typeof(IAlamoLanguageDefinition)}. The two objects cannot be compared."); + + return CompareToInternal(other); + } + + /// + /// Compares the current instance with another object of the same type and returns an integer that indicates + /// whether the current instance precedes, follows, or occurs in the same position in the sort order as the + /// other object. + /// + /// + /// The specific override differs in its sort order in such a way, + /// that all elements are ordered by their t. + /// + /// + /// + protected virtual int CompareToInternal(IAlamoLanguageDefinition other) + { + return string.Compare(Culture.TwoLetterISOLanguageName, other.Culture.TwoLetterISOLanguageName, + StringComparison.Ordinal); + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// + /// + protected virtual bool EqualsInternal(IAlamoLanguageDefinition other) + { + return string.Compare(Culture.TwoLetterISOLanguageName, other.Culture.TwoLetterISOLanguageName, + StringComparison.Ordinal) == 0 && string.Compare(LanguageIdentifier, other.LanguageIdentifier, + StringComparison.Ordinal) == 0; + } + + /// + /// Serves as the default hash function. + /// + /// + /// The default implementation does intentionally create hash conflicts between identical language definitions, + /// e.g. using the same and for two different derived classes. + /// + /// + protected virtual int GetHashCodeInternal() + { + unchecked + { + return (ConfiguredLanguageIdentifier.GetHashCode() * 397) ^ Culture.GetHashCode(); + } + } + + /// + public override bool Equals(object? obj) + { + if (obj is null) return false; + + if (ReferenceEquals(this, obj)) return true; + + return obj is IAlamoLanguageDefinition alamoLanguageDefinition && EqualsInternal(alamoLanguageDefinition); + } + + /// + public override int GetHashCode() + { + return GetHashCodeInternal(); + } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/ChineseAlamoLanguageDefinition.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/ChineseAlamoLanguageDefinition.cs new file mode 100644 index 000000000..80cd38308 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/ChineseAlamoLanguageDefinition.cs @@ -0,0 +1,25 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using PG.StarWarsGame.Components.Localisation.Attributes; + +namespace PG.StarWarsGame.Components.Localisation.Languages.BuiltIn; + +/// +/// The language definition for the Chinese language. +/// +/// +/// Officially supported by the Alamo Engine. +/// +[ExcludeFromCodeCoverage] +[OfficiallySupportedLanguage] +public sealed class ChineseAlamoLanguageDefinition : AlamoLanguageDefinitionBase +{ + /// + protected override string ConfiguredLanguageIdentifier => "CHINESE"; + + /// + protected override CultureInfo ConfiguredCulture => CultureInfo.GetCultureInfo("zh-CN"); +} \ No newline at end of file diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/EnglishAlamoLanguageDefinition.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/EnglishAlamoLanguageDefinition.cs new file mode 100644 index 000000000..9f6483742 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/EnglishAlamoLanguageDefinition.cs @@ -0,0 +1,27 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using PG.StarWarsGame.Components.Localisation.Attributes; + +namespace PG.StarWarsGame.Components.Localisation.Languages.BuiltIn; + +/// +/// The language definition for the English (US) language. +/// +/// +/// Officially supported by the Alamo Engine.
+/// This language is the default game language and development language of all Alamo Engine games. +///
+[ExcludeFromCodeCoverage] +[DefaultLanguage] +[OfficiallySupportedLanguage] +public sealed class EnglishAlamoLanguageDefinition : AlamoLanguageDefinitionBase +{ + /// + protected override string ConfiguredLanguageIdentifier => "ENGLISH"; + + /// + protected override CultureInfo ConfiguredCulture => CultureInfo.GetCultureInfo("en-US"); +} \ No newline at end of file diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/FrenchAlamoLanguageDefinition.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/FrenchAlamoLanguageDefinition.cs new file mode 100644 index 000000000..b4c070a3c --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/FrenchAlamoLanguageDefinition.cs @@ -0,0 +1,25 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using PG.StarWarsGame.Components.Localisation.Attributes; + +namespace PG.StarWarsGame.Components.Localisation.Languages.BuiltIn; + +/// +/// The language definition for the French language. +/// +/// +/// Officially supported by the Alamo Engine. +/// +[ExcludeFromCodeCoverage] +[OfficiallySupportedLanguage] +public sealed class FrenchAlamoLanguageDefinition : AlamoLanguageDefinitionBase +{ + /// + protected override string ConfiguredLanguageIdentifier => "FRENCH"; + + /// + protected override CultureInfo ConfiguredCulture => CultureInfo.GetCultureInfo("fr-FR"); +} \ No newline at end of file diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/GermanAlamoLanguageDefinition.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/GermanAlamoLanguageDefinition.cs new file mode 100644 index 000000000..52a9b483d --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/GermanAlamoLanguageDefinition.cs @@ -0,0 +1,25 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using PG.StarWarsGame.Components.Localisation.Attributes; + +namespace PG.StarWarsGame.Components.Localisation.Languages.BuiltIn; + +/// +/// The language definition for the German language. +/// +/// +/// Officially supported by the Alamo Engine. +/// +[ExcludeFromCodeCoverage] +[OfficiallySupportedLanguage] +public sealed class GermanAlamoLanguageDefinition : AlamoLanguageDefinitionBase +{ + /// + protected override string ConfiguredLanguageIdentifier => "GERMAN"; + + /// + protected override CultureInfo ConfiguredCulture => CultureInfo.GetCultureInfo("de-DE"); +} \ No newline at end of file diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/ItalianAlamoLanguageDefinition.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/ItalianAlamoLanguageDefinition.cs new file mode 100644 index 000000000..4a526bde9 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/ItalianAlamoLanguageDefinition.cs @@ -0,0 +1,25 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using PG.StarWarsGame.Components.Localisation.Attributes; + +namespace PG.StarWarsGame.Components.Localisation.Languages.BuiltIn; + +/// +/// The language definition for the Italian language. +/// +/// +/// Officially supported by the Alamo Engine. +/// +[ExcludeFromCodeCoverage] +[OfficiallySupportedLanguage] +public sealed class ItalianAlamoLanguageDefinition : AlamoLanguageDefinitionBase +{ + /// + protected override string ConfiguredLanguageIdentifier => "ITALIAN"; + + /// + protected override CultureInfo ConfiguredCulture => CultureInfo.GetCultureInfo("it-IT"); +} \ No newline at end of file diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/JapaneseAlamoLanguageDefinition.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/JapaneseAlamoLanguageDefinition.cs new file mode 100644 index 000000000..908f3f268 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/JapaneseAlamoLanguageDefinition.cs @@ -0,0 +1,25 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using PG.StarWarsGame.Components.Localisation.Attributes; + +namespace PG.StarWarsGame.Components.Localisation.Languages.BuiltIn; + +/// +/// The language definition for the Japanese language. +/// +/// +/// Officially supported by the Alamo Engine. +/// +[ExcludeFromCodeCoverage] +[OfficiallySupportedLanguage] +public sealed class JapaneseAlamoLanguageDefinition : AlamoLanguageDefinitionBase +{ + /// + protected override string ConfiguredLanguageIdentifier => "JAPANESE"; + + /// + protected override CultureInfo ConfiguredCulture => CultureInfo.GetCultureInfo("ja"); +} \ No newline at end of file diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/KoreanAlamoLanguageDefinition.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/KoreanAlamoLanguageDefinition.cs new file mode 100644 index 000000000..743b7ba48 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/KoreanAlamoLanguageDefinition.cs @@ -0,0 +1,25 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using PG.StarWarsGame.Components.Localisation.Attributes; + +namespace PG.StarWarsGame.Components.Localisation.Languages.BuiltIn; + +/// +/// The language definition for the Korean language. +/// +/// +/// Officially supported by the Alamo Engine. +/// +[ExcludeFromCodeCoverage] +[OfficiallySupportedLanguage] +public sealed class KoreanAlamoLanguageDefinition : AlamoLanguageDefinitionBase +{ + /// + protected override string ConfiguredLanguageIdentifier => "KOREAN"; + + /// + protected override CultureInfo ConfiguredCulture => CultureInfo.GetCultureInfo("ko"); +} \ No newline at end of file diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/PolishAlamoLanguageDefinition.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/PolishAlamoLanguageDefinition.cs new file mode 100644 index 000000000..e2a75e201 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/PolishAlamoLanguageDefinition.cs @@ -0,0 +1,25 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using PG.StarWarsGame.Components.Localisation.Attributes; + +namespace PG.StarWarsGame.Components.Localisation.Languages.BuiltIn; + +/// +/// The language definition for the Polish language. +/// +/// +/// Officially supported by the Alamo Engine. +/// +[ExcludeFromCodeCoverage] +[OfficiallySupportedLanguage] +public sealed class PolishAlamoLanguageDefinition : AlamoLanguageDefinitionBase +{ + /// + protected override string ConfiguredLanguageIdentifier => "POLISH"; + + /// + protected override CultureInfo ConfiguredCulture => CultureInfo.GetCultureInfo("pl"); +} \ No newline at end of file diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/RussianAlamoLanguageDefinition.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/RussianAlamoLanguageDefinition.cs new file mode 100644 index 000000000..3645fb143 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/RussianAlamoLanguageDefinition.cs @@ -0,0 +1,25 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using PG.StarWarsGame.Components.Localisation.Attributes; + +namespace PG.StarWarsGame.Components.Localisation.Languages.BuiltIn; + +/// +/// The language definition for the Russian language. +/// +/// +/// Officially supported by the Alamo Engine. +/// +[ExcludeFromCodeCoverage] +[OfficiallySupportedLanguage] +public sealed class RussianAlamoLanguageDefinition : AlamoLanguageDefinitionBase +{ + /// + protected override string ConfiguredLanguageIdentifier => "RUSSIAN"; + + /// + protected override CultureInfo ConfiguredCulture => CultureInfo.GetCultureInfo("ru"); +} \ No newline at end of file diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/SpanishAlamoLanguageDefinition.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/SpanishAlamoLanguageDefinition.cs new file mode 100644 index 000000000..7b019a8df --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/SpanishAlamoLanguageDefinition.cs @@ -0,0 +1,25 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using PG.StarWarsGame.Components.Localisation.Attributes; + +namespace PG.StarWarsGame.Components.Localisation.Languages.BuiltIn; + +/// +/// The language definition for the Spanish language. +/// +/// +/// Officially supported by the Alamo Engine. +/// +[ExcludeFromCodeCoverage] +[OfficiallySupportedLanguage] +public sealed class SpanishAlamoLanguageDefinition : AlamoLanguageDefinitionBase +{ + /// + protected override string ConfiguredLanguageIdentifier => "SPANISH"; + + /// + protected override CultureInfo ConfiguredCulture => CultureInfo.GetCultureInfo("es-ES"); +} \ No newline at end of file diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/ThaiAlamoLanguageDefinition.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/ThaiAlamoLanguageDefinition.cs new file mode 100644 index 000000000..8a968549d --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/BuiltIn/ThaiAlamoLanguageDefinition.cs @@ -0,0 +1,25 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using PG.StarWarsGame.Components.Localisation.Attributes; + +namespace PG.StarWarsGame.Components.Localisation.Languages.BuiltIn; + +/// +/// The language definition for the Thai language. +/// +/// +/// Officially supported by the Alamo Engine. +/// +[ExcludeFromCodeCoverage] +[OfficiallySupportedLanguage] +public sealed class ThaiAlamoLanguageDefinition : AlamoLanguageDefinitionBase +{ + /// + protected override string ConfiguredLanguageIdentifier => "THAI"; + + /// + protected override CultureInfo ConfiguredCulture => CultureInfo.GetCultureInfo("th"); +} \ No newline at end of file diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/IAlamoLanguageDefinition.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/IAlamoLanguageDefinition.cs new file mode 100644 index 000000000..e56dc15d0 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Languages/IAlamoLanguageDefinition.cs @@ -0,0 +1,24 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.Globalization; + +namespace PG.StarWarsGame.Components.Localisation.Languages; + +/// +/// An interface exposing all relevant data to describe a language to be used in the Alamo Engine. +/// +public interface IAlamoLanguageDefinition +{ + /// + /// A string that is being used to identify the language of the *.DAT file, e.g. a language identifier + /// "english" would produce the file "mastertextfile_english.dat" + /// + string LanguageIdentifier { get; } + + /// + /// The .NET Culture that best describes the language. This culture can be used for spell checking, + /// auto-translation between languages, etc. + /// + CultureInfo Culture { get; } +} \ No newline at end of file diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/LocalisationServiceContribution.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/LocalisationServiceContribution.cs new file mode 100644 index 000000000..75e31e80a --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/LocalisationServiceContribution.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.DependencyInjection; +using PG.Commons.Attributes; +using PG.Commons.Data.Serialization; +using PG.Commons.Extensibility; +using PG.StarWarsGame.Components.Localisation.IO; +using PG.StarWarsGame.Components.Localisation.IO.Xml; +using PG.StarWarsGame.Components.Localisation.Services; +using PG.StarWarsGame.Files.DAT.Services.Builder.Validation; + +namespace PG.StarWarsGame.Components.Localisation; + +/// +[Order(1200)] +public class LocalisationServiceContribution : IServiceContribution +{ + /// + public void ContributeServices(IServiceCollection serviceCollection) + { + serviceCollection + .AddSingleton(_ => new EmpireAtWarKeyValidator()) + .AddSingleton(sp => new XmlSerializationSupportService(sp)) + .AddSingleton(sp => new AlamoLanguageSupportService(sp)) + .AddTransient>(sp => new XmlImportHandler(sp)) + .AddTransient>(sp => new XmlExportHandler(sp)); + } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.csproj b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.csproj new file mode 100644 index 000000000..25323b38b --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation.csproj @@ -0,0 +1,43 @@ + + + + + netstandard2.0;netstandard2.1 + PG.StarWarsGame.Components.Localisation + PG.StarWarsGame.Components.Localisation + AlamoEngineTools.PG.StarWarsGame.Components.Localisation + alamo,petroglyph,glyphx + + + true + true + true + + + true + snupkg + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Builtin/InMemoryOrderedTranslationRepository.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Builtin/InMemoryOrderedTranslationRepository.cs new file mode 100644 index 000000000..d85ff882a --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Builtin/InMemoryOrderedTranslationRepository.cs @@ -0,0 +1,208 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using PG.StarWarsGame.Components.Localisation.Languages; +using PG.StarWarsGame.Components.Localisation.Repository.Content; + +namespace PG.StarWarsGame.Components.Localisation.Repository.Builtin; + +/// +/// A simple ordered in-memory translation repository +/// +public class InMemoryOrderedTranslationRepository : ITranslationRepository +{ + private IDictionary> + Repository { get; } = + new Dictionary>(); + + /// + /// Indexer + /// + /// + public IEnumerable this[IAlamoLanguageDefinition alamoLanguageDefinition] => Content[alamoLanguageDefinition]; + + /// + public IReadOnlyDictionary> Content => ToContent(); + + /// + public bool AddLanguage(IAlamoLanguageDefinition language) + { + try + { + Repository.Add(language, new Dictionary()); + return true; + } + catch (ArgumentException) + { + return false; + } + } + + /// + public bool AddOrUpdateLanguage(IAlamoLanguageDefinition language, + ICollection translationItems, + ITranslationRepository.MergeStrategy strategy = ITranslationRepository.MergeStrategy.MergeByKey) + { + return strategy switch + { + ITranslationRepository.MergeStrategy.Replace => AddOrUpdateLanguageReplace(language, translationItems), + ITranslationRepository.MergeStrategy.MergeByKey => AddOrUpdateLanguageMergeByKey(language, + translationItems), + ITranslationRepository.MergeStrategy.Append => AddOrUpdateLanguageAppend(language, translationItems), + _ => throw new ArgumentOutOfRangeException(nameof(strategy), strategy, null) + }; + } + + /// + public bool RemoveLanguage(IAlamoLanguageDefinition language) + { + return Repository.Remove(language); + } + + /// + public ITranslationItem GetTranslationItem(IAlamoLanguageDefinition language, ITranslationItemId id) + { + if (id is not OrderedTranslationItemId key) + throw new ArgumentException( + $"The passed id is of type {id.GetType()} but should be of type {nameof(OrderedTranslationItemId)}", + nameof(id)); + if (!Repository.TryGetValue(language, out var translationItems)) + throw new KeyNotFoundException($"The language {language} was not found"); + if (!translationItems.TryGetValue(key, out var item)) + throw new KeyNotFoundException($"The item {id} was not found"); + return item; + } + + /// + public bool RemoveTranslationItem(ITranslationItemId id) + { + if (id is not OrderedTranslationItemId key) + throw new ArgumentException( + $"The passed id is of type {id.GetType()} but should be of type {nameof(OrderedTranslationItemId)}", + nameof(id)); + return Repository.Values.Where(i => i.ContainsKey(key)) + .Aggregate(false, (current, i) => current | i.Remove(key)); + } + + /// + public void AddOrUpdateTranslationItem(IAlamoLanguageDefinition language, ITranslationItem item) + { + if (item.ItemId is not OrderedTranslationItemId key) + throw new ArgumentException( + $"The passed id is of type {item.ItemId} but should be of type {nameof(OrderedTranslationItemId)}", + nameof(item.ItemId)); + if (!Repository.TryGetValue(language, out var translationItems)) + throw new KeyNotFoundException($"The language {language} was not found"); + translationItems[key] = item; + } + + /// + public ITranslationDiff CreateTranslationDiff(IAlamoLanguageDefinition diffBase) + { + throw new NotImplementedException(); + } + + /// + public bool ApplyTranslationDiff(ITranslationDiff diff) + { + throw new NotImplementedException(); + } + + /// + public IReadOnlyCollection GetTranslationItemsByLanguage( + IAlamoLanguageDefinition language) + { + if (!Repository.TryGetValue(language, out var value)) + throw new KeyNotFoundException($"The provided language {language} could not be found."); + + return value.Values.ToImmutableList(); + } + + private IReadOnlyDictionary> ToContent() + { + return Repository + .ToImmutableDictionary< + KeyValuePair>, + IAlamoLanguageDefinition, ICollection>(pair => pair.Key, + pair => new List(pair.Value.Values)); + } + + private bool AddOrUpdateLanguageAppend(IAlamoLanguageDefinition language, + ICollection translationItems) + { + if (!Repository.TryGetValue(language, out var repositoryItems)) + { + Repository.Add(language, new Dictionary()); + repositoryItems = Repository[language]; + } + + var itemsToAdd = new Dictionary(); + foreach (var item in translationItems) + { + if (item.ItemId is not OrderedTranslationItemId key) + throw new ArgumentException( + $"The passed id is of type {item.ItemId} but should be of type {nameof(OrderedTranslationItemId)}", + nameof(item.ItemId)); + if (!repositoryItems.ContainsKey(key)) + itemsToAdd.Add(key, item); + } + + foreach (var i in itemsToAdd) repositoryItems.Add(i.Key, i.Value); + + return translationItems.Count == itemsToAdd.Count; + } + + private bool AddOrUpdateLanguageMergeByKey(IAlamoLanguageDefinition language, + ICollection translationItems) + { + if (!Repository.TryGetValue(language, out var repositoryItems)) + { + Repository.Add(language, new Dictionary()); + repositoryItems = Repository[language]; + } + + var itemsToAdd = new Dictionary(); + foreach (var item in translationItems) + { + if (item.ItemId is not OrderedTranslationItemId key) + throw new ArgumentException( + $"The passed id is of type {item.ItemId} but should be of type {nameof(OrderedTranslationItemId)}", + nameof(item.ItemId)); + if (repositoryItems.ContainsKey(key)) + repositoryItems[key] = item; + else + itemsToAdd.Add(key, item); + } + + foreach (var i in itemsToAdd) repositoryItems.Add(i.Key, i.Value); + + return true; + } + + private bool AddOrUpdateLanguageReplace(IAlamoLanguageDefinition language, + ICollection translationItems) + { + if (!Repository.TryGetValue(language, out var repositoryItems)) + { + Repository.Add(language, new Dictionary()); + repositoryItems = Repository[language]; + } + + repositoryItems.Clear(); + foreach (var item in translationItems) + { + if (item.ItemId is not OrderedTranslationItemId key) + throw new ArgumentException( + $"The passed id is of type {item.ItemId} but should be of type {nameof(OrderedTranslationItemId)}", + nameof(item.ItemId)); + repositoryItems.Add(key, item); + } + + return true; + } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/IReadOnlyTranslationItem.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/IReadOnlyTranslationItem.cs new file mode 100644 index 000000000..08b0bdea8 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/IReadOnlyTranslationItem.cs @@ -0,0 +1,10 @@ +namespace PG.StarWarsGame.Components.Localisation.Repository.Content; + +/// +/// Readonly representation of an +/// +public interface IReadOnlyTranslationItem : ITranslationItem +{ + /// + new ITranslationItemContent Content { get; } +} \ No newline at end of file diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/ITranslationItem.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/ITranslationItem.cs new file mode 100644 index 000000000..5e41269eb --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/ITranslationItem.cs @@ -0,0 +1,17 @@ +namespace PG.StarWarsGame.Components.Localisation.Repository.Content; + +/// +/// The basic representation of a translation item. +/// +public interface ITranslationItem +{ + /// + /// ID + /// + ITranslationItemId? ItemId { get; } + + /// + /// The acutal translation content. + /// + ITranslationItemContent Content { get; set; } +} \ No newline at end of file diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/ITranslationItemContent.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/ITranslationItemContent.cs new file mode 100644 index 000000000..40011ee0e --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/ITranslationItemContent.cs @@ -0,0 +1,17 @@ +namespace PG.StarWarsGame.Components.Localisation.Repository.Content; + +/// +/// The raw content of a translation item. +/// +public interface ITranslationItemContent +{ + /// + /// The string key, usually mapped to + /// + string Key { get; init; } + + /// + /// The string value, usually mapped to + /// + string? Value { get; init; } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/ITranslationItemId.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/ITranslationItemId.cs new file mode 100644 index 000000000..139e84cf9 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/ITranslationItemId.cs @@ -0,0 +1,11 @@ +using System; +using PG.Commons.Data; + +namespace PG.StarWarsGame.Components.Localisation.Repository.Content; + +/// +/// The unique item ID, could be the string key, or any other unique ID. +/// +public interface ITranslationItemId : IId, IEquatable, IComparable +{ +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/OrderedTranslationItem.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/OrderedTranslationItem.cs new file mode 100644 index 000000000..c5bc8b990 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/OrderedTranslationItem.cs @@ -0,0 +1,35 @@ +using System; + +namespace PG.StarWarsGame.Components.Localisation.Repository.Content; + +/// +public class OrderedTranslationItem : TranslationItemBase +{ + /// + protected OrderedTranslationItem(OrderedTranslationItemId? itemId, TranslationItemContent content) : base(itemId, + content) + { + } + + /// + /// Convenience method to create a new + /// + /// + /// + /// + public static OrderedTranslationItem Of(OrderedTranslationItemId? itemId, TranslationItemContent content) + { + return new OrderedTranslationItem(itemId, content); + } + + /// + /// Convenience method to create a new + /// + /// + /// + /// + public static OrderedTranslationItem Of(TranslationItemContent content) + { + return Of(OrderedTranslationItemId.Of(content.Key) ?? throw new InvalidOperationException(), content); + } +} \ No newline at end of file diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/OrderedTranslationItemId.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/OrderedTranslationItemId.cs new file mode 100644 index 000000000..15cf2e3c0 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/OrderedTranslationItemId.cs @@ -0,0 +1,38 @@ +using PG.Commons.Data; + +namespace PG.StarWarsGame.Components.Localisation.Repository.Content; + +/// +/// Item ID for ordered translation items. Equivalent to the raw string key from a sorted DAT file. +/// +public class OrderedTranslationItemId : RootIdBase, ITranslationItemId +{ + /// + protected OrderedTranslationItemId(string rawId) : base(rawId) + { + } + + /// + public bool Equals(ITranslationItemId? other) + { + if (other == null) return false; + return base.Equals(other); + } + + /// + public int CompareTo(ITranslationItemId? other) + { + if (other == null) return 1; + return base.CompareTo(other); + } + + /// + /// Convenience method to create a new + /// + /// + /// + public static OrderedTranslationItemId? Of(string rawId) + { + return string.IsNullOrWhiteSpace(rawId) ? null : new OrderedTranslationItemId(rawId); + } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/TranslationItemBase.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/TranslationItemBase.cs new file mode 100644 index 000000000..28ddd2066 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/TranslationItemBase.cs @@ -0,0 +1,28 @@ +namespace PG.StarWarsGame.Components.Localisation.Repository.Content; + +/// +public abstract class TranslationItemBase : ITranslationItem +{ + /// + /// .ctor + /// + /// + /// + protected TranslationItemBase(ITranslationItemId? itemId, TranslationItemContent content) + { + ItemId = itemId; + Content = content; + } + + /// + public ITranslationItemContent Content { get; set; } + + /// + public ITranslationItemId? ItemId { get; } + + /// + public override string ToString() + { + return $"{GetType().Name}[{ItemId}: {Content}]"; + } +} \ No newline at end of file diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/TranslationItemContent.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/TranslationItemContent.cs new file mode 100644 index 000000000..aa7505f8f --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/Content/TranslationItemContent.cs @@ -0,0 +1,11 @@ +namespace PG.StarWarsGame.Components.Localisation.Repository.Content; + +/// +public record TranslationItemContent : ITranslationItemContent +{ + /// + public required string Key { get; init; } + + /// + public string? Value { get; init; } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/ITranslationDiff.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/ITranslationDiff.cs new file mode 100644 index 000000000..7dcf01c70 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/ITranslationDiff.cs @@ -0,0 +1,40 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.Collections.Immutable; +using PG.StarWarsGame.Components.Localisation.Languages; +using PG.StarWarsGame.Components.Localisation.Repository.Content; + +namespace PG.StarWarsGame.Components.Localisation.Repository; + +/// +/// A Diff between all language contents of a given . +/// +public interface ITranslationDiff +{ + /// + /// The base language for the diff. + /// + IAlamoLanguageDefinition DiffBase { get; } + + /// + /// All languages present in the diff. + /// + IImmutableSet Languages { get; } + + /// + /// Collects all s that are missing from a language when compared to the + /// + /// + /// + /// + IImmutableSet GetMissingItemIdsForLanguage(IAlamoLanguageDefinition language); + + /// + /// Collects all s that are still present for a language when compared to the + /// + /// + /// + /// + IImmutableSet GetDanglingItemIdsForLanguage(IAlamoLanguageDefinition language); +} \ No newline at end of file diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/ITranslationRepository.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/ITranslationRepository.cs new file mode 100644 index 000000000..1059edcc4 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Repository/ITranslationRepository.cs @@ -0,0 +1,114 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.Collections.Generic; +using PG.StarWarsGame.Components.Localisation.Languages; +using PG.StarWarsGame.Components.Localisation.Repository.Content; + +namespace PG.StarWarsGame.Components.Localisation.Repository; + +/// +/// The base interface representing a repository containing translations. +/// +public interface ITranslationRepository +{ + /// + /// The merge strategy used when adding a new language to the repository. + /// + enum MergeStrategy + { + /// + /// If the already exists, the new items will replace ALL existing + /// items. This is equivalent to calling , + /// and + /// for the new items. + /// + Replace, + + /// + /// If the already exists, the will be updated, + /// otherwise added. + /// + MergeByKey, + + /// + /// The s will be appended if possible. If there is a key already present in the + /// repository, the associated item will NOT be updated, use if you want to update + /// exiting items. + /// + Append + } + + /// + /// Readonly dictionary of all contained s separated by language. + /// + IReadOnlyDictionary> Content { get; } + + /// + /// Readonly collection of all contained s. + /// + IReadOnlyCollection GetTranslationItemsByLanguage(IAlamoLanguageDefinition language); + + /// + /// Adds a new language to the repository. + /// + /// + /// True, if the language was added successfully. + bool AddLanguage(IAlamoLanguageDefinition language); + + /// + /// Adds a new language with initial values to the repository. If the language is already present, the translation + /// items will be added to the existing set according to the + /// + /// + /// + /// The merge strategy used if the language already exists. + /// True, if the language was added successfully. + bool AddOrUpdateLanguage(IAlamoLanguageDefinition language, ICollection translationItems, + MergeStrategy strategy = MergeStrategy.MergeByKey); + + /// + /// Removes a language from the repository. + /// + /// + /// + bool RemoveLanguage(IAlamoLanguageDefinition language); + + /// + /// Fetches a given translation item + /// + /// + /// + /// + ITranslationItem GetTranslationItem(IAlamoLanguageDefinition language, ITranslationItemId id); + + /// + /// Removes a given translation item from the repository. This operation applies to ALL languages in the + /// repository. + /// + /// + /// + bool RemoveTranslationItem(ITranslationItemId id); + + /// + /// Adds a translation item to a given language. + /// + /// + /// + void AddOrUpdateTranslationItem(IAlamoLanguageDefinition language, ITranslationItem item); + + /// + /// Creates a diff between the present languages. + /// + /// + /// + ITranslationDiff CreateTranslationDiff(IAlamoLanguageDefinition diffBase); + + /// + /// Applies a diff to the repository. Missing items in languages will be inserted, items not present in the master + /// language will be removed. + /// + /// + /// + bool ApplyTranslationDiff(ITranslationDiff diff); +} \ No newline at end of file diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Services/AlamoLanguageSupportService.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Services/AlamoLanguageSupportService.cs new file mode 100644 index 000000000..bf205dd55 --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Services/AlamoLanguageSupportService.cs @@ -0,0 +1,76 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using PG.Commons.Services; +using PG.StarWarsGame.Components.Localisation.Attributes; +using PG.StarWarsGame.Components.Localisation.Exceptions; +using PG.StarWarsGame.Components.Localisation.Languages; + +namespace PG.StarWarsGame.Components.Localisation.Services; + +/// +public class AlamoLanguageSupportService : ServiceBase, IAlamoLanguageSupportService +{ + private IAlamoLanguageDefinition _fallbackAlamoLanguageDefinition; + + /// + public AlamoLanguageSupportService(IServiceProvider services) : base(services) + { + _fallbackAlamoLanguageDefinition = GetDefaultLanguageDefinition(); + } + + /// + public bool IsBuiltInLanguageDefinition(IAlamoLanguageDefinition definition) + { + return definition.GetType().GetCustomAttribute(typeof(OfficiallySupportedLanguageAttribute)) != null; + } + + /// + public IAlamoLanguageDefinition GetFallbackLanguageDefinition() + { + return _fallbackAlamoLanguageDefinition; + } + + /// + public IAlamoLanguageSupportService WithOverrideFallbackLanguage(IAlamoLanguageDefinition definition) + { + _fallbackAlamoLanguageDefinition = definition; + return this; + } + + /// + public IAlamoLanguageDefinition GetDefaultLanguageDefinition() + { + var languageDefinitions = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(assemblyTypes => assemblyTypes.GetTypes()) + .Where(assemblyType => typeof(IAlamoLanguageDefinition).IsAssignableFrom(assemblyType) && + assemblyType is { IsClass: true, IsAbstract: false }) + .Where(t => t.GetCustomAttribute(typeof(DefaultLanguageAttribute)) != null) + .ToList(); + if (languageDefinitions.Count != 1) + throw new InvalidDefaultLanguageDefinitionException(languageDefinitions.Count > 1 + ? "Multiple default languages have been defined." + : "No default language has been defined."); + return (IAlamoLanguageDefinition)Activator.CreateInstance(languageDefinitions[0]); + } + + /// + public IDictionary CreateLanguageIdentifierMapping() + { + return GetRegisteredLanguages().ToDictionary(d => d.LanguageIdentifier, d => d); + } + + /// + public ISet GetRegisteredLanguages() + { + return new HashSet(AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(assemblyTypes => assemblyTypes.GetTypes()) + .Where(assemblyType => typeof(IAlamoLanguageDefinition).IsAssignableFrom(assemblyType) && + assemblyType is { IsClass: true, IsAbstract: false }) + .Select(t => (IAlamoLanguageDefinition)Activator.CreateInstance(t))); + } +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Services/IAlamoLanguageSupportService.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Services/IAlamoLanguageSupportService.cs new file mode 100644 index 000000000..6eb0bfc8c --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Services/IAlamoLanguageSupportService.cs @@ -0,0 +1,55 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using System.Collections.Generic; +using PG.StarWarsGame.Components.Localisation.Languages; + +namespace PG.StarWarsGame.Components.Localisation.Services; + +/// +/// Helper service for handling s. +/// +public interface IAlamoLanguageSupportService +{ + /// + /// Checks if a given is a builtin languge. + /// + /// + /// + bool IsBuiltInLanguageDefinition(IAlamoLanguageDefinition definition); + + /// + /// Gets the fallback language if none can be resolved. By default, this is the return value of + /// but can be + /// overridden with + /// + /// + IAlamoLanguageDefinition GetFallbackLanguageDefinition(); + + /// + /// Allows for overriding the fallback language. + /// + /// + /// + IAlamoLanguageSupportService WithOverrideFallbackLanguage(IAlamoLanguageDefinition definition); + + /// + /// Returns the default language marked with the + /// + /// + /// + IAlamoLanguageDefinition GetDefaultLanguageDefinition(); + + /// + /// Creates a lookup map for s by their + /// + /// + /// + IDictionary CreateLanguageIdentifierMapping(); + + /// + /// Collects all registered languages. + /// + /// + ISet GetRegisteredLanguages(); +} diff --git a/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Services/ITranslationRepositoryService.cs b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Services/ITranslationRepositoryService.cs new file mode 100644 index 000000000..1e15b3d5b --- /dev/null +++ b/PG.StarWarsGame.Components.Localisation/PG.StarWarsGame.Components.Localisation/Services/ITranslationRepositoryService.cs @@ -0,0 +1,35 @@ +// Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +using PG.StarWarsGame.Components.Localisation.IO; +using PG.StarWarsGame.Components.Localisation.Repository; + +namespace PG.StarWarsGame.Components.Localisation.Services; + +/// +/// Base service for handling a +/// +public interface ITranslationRepositoryService +{ + /// + /// Loads a given file or set of files as a singular based on the + /// + /// + /// + /// + /// + /// + /// + void LoadAs(IImportHandler handler, T strategy, ITranslationRepository repository) + where T : IInputStrategy; // TODO: Handler-factory based on the strategy. + + /// + /// Stores a as defined in the + /// + /// + /// + /// + /// + void StoreAs(IExportHandler handler, T strategy, ITranslationRepository repository) + where T : IOutputStrategy; // TODO: Handler-factory based on the strategy. +} diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/PG.StarWarsGame.Files.DAT.Test.csproj.DotSettings b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/PG.StarWarsGame.Files.DAT.Test.csproj.DotSettings index b8c8f99cd..2286a6e88 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/PG.StarWarsGame.Files.DAT.Test.csproj.DotSettings +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.Test/PG.StarWarsGame.Files.DAT.Test.csproj.DotSettings @@ -1,2 +1,10 @@ - - True \ No newline at end of file + + + + True \ No newline at end of file diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.csproj.DotSettings b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.csproj.DotSettings index 1daeeb935..57c0bfa6f 100644 --- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.csproj.DotSettings +++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT.csproj.DotSettings @@ -1,5 +1,16 @@ - - True - True - True - True \ No newline at end of file + + + + True + True + True + True \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/PG.StarWarsGame.Files.MEG.Test.csproj.DotSettings b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/PG.StarWarsGame.Files.MEG.Test.csproj.DotSettings index 6f52e575f..c419e82ec 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/PG.StarWarsGame.Files.MEG.Test.csproj.DotSettings +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.Test/PG.StarWarsGame.Files.MEG.Test.csproj.DotSettings @@ -1,4 +1,14 @@ - - True - True - True \ No newline at end of file + + + + True + True + True \ No newline at end of file diff --git a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.csproj.DotSettings b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.csproj.DotSettings index c756b64e0..ee7687ed2 100644 --- a/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.csproj.DotSettings +++ b/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG/PG.StarWarsGame.Files.MEG.csproj.DotSettings @@ -1,15 +1,35 @@ - - True - True - True - True - True - True - True - True - True - True - True - True - True - True \ No newline at end of file + + + + True + True + True + True + True + True + True + True + True + True + True + True + True + True \ No newline at end of file diff --git a/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml.Test/PG.StarWarsGame.Files.Xml.Test.csproj b/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml.Test/PG.StarWarsGame.Files.Xml.Test.csproj index 7ea1d29a6..f11932f79 100644 --- a/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml.Test/PG.StarWarsGame.Files.Xml.Test.csproj +++ b/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml.Test/PG.StarWarsGame.Files.Xml.Test.csproj @@ -1,20 +1,25 @@ - - - net8.0 - $(TargetFrameworks);net48 - false - false - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + + net8.0 + $(TargetFrameworks);net48 + false + false + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml.csproj b/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml.csproj index 9ff294031..560f2af7e 100644 --- a/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml.csproj +++ b/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml.csproj @@ -1,27 +1,32 @@ - - - false - netstandard2.0 - PG.StarWarsGame.Files.Xml - AlamoEngineTools.PG.StarWarsGame.Files.Xml - PG.StarWarsGame.Files.Xml - - - true - true - true - - - true - snupkg - - - - ResXFileCodeGenerator - LocalizableTexts.Designer.cs - - - - - + + + + + false + netstandard2.0 + PG.StarWarsGame.Files.Xml + AlamoEngineTools.PG.StarWarsGame.Files.Xml + PG.StarWarsGame.Files.Xml + + + true + true + true + + + true + snupkg + + + + ResXFileCodeGenerator + LocalizableTexts.Designer.cs + + + + + diff --git a/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml/Resources/LocalizableTexts.Designer.cs b/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml/Resources/LocalizableTexts.Designer.cs index 8f995c054..6bf107c66 100644 --- a/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml/Resources/LocalizableTexts.Designer.cs +++ b/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml/Resources/LocalizableTexts.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. diff --git a/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml/Resources/LocalizableTexts.de.resx b/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml/Resources/LocalizableTexts.de.resx index 91ada13e8..2a4977e89 100644 --- a/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml/Resources/LocalizableTexts.de.resx +++ b/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml/Resources/LocalizableTexts.de.resx @@ -1,3 +1,8 @@ + + text/microsoft-resx @@ -6,10 +11,14 @@ 1.3 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + Keine Beschreibung verfügbar. diff --git a/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml/Resources/LocalizableTexts.resx b/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml/Resources/LocalizableTexts.resx index 2664d848b..9e790811e 100644 --- a/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml/Resources/LocalizableTexts.resx +++ b/PG.StarWarsGame.Files.Xml/PG.StarWarsGame.Files.Xml/Resources/LocalizableTexts.resx @@ -1,9 +1,15 @@ + + - + - + @@ -13,10 +19,14 @@ 1.3 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + No description available. diff --git a/PG.Testing/TestUtility.cs b/PG.Testing/TestUtility.cs index d1685b257..e4bb5e538 100644 --- a/PG.Testing/TestUtility.cs +++ b/PG.Testing/TestUtility.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using Xunit; namespace PG.Testing; @@ -11,14 +12,11 @@ namespace PG.Testing; public static class TestUtility { private static readonly Random RandomGenerator = new(); - + public static void AssertAreBinaryEquivalent(IReadOnlyList expected, IReadOnlyList actual) { Assert.Equal(expected.Count, actual.Count); - for (var i = 0; i < expected.Count; i++) - { - Assert.Equal(expected[i], actual[i]); - } + for (var i = 0; i < expected.Count; i++) Assert.Equal(expected[i], actual[i]); } public static string GetRandomStringOfLength(int length) @@ -26,10 +24,7 @@ public static string GetRandomStringOfLength(int length) const string allowedChars = "ABCDEFGHJKLMNOPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz0123456789!@$?_-"; var chars = new char[length]; - for (var i = 0; i < length; i++) - { - chars[i] = allowedChars[RandomGenerator.Next(0, allowedChars.Length)]; - } + for (var i = 0; i < length; i++) chars[i] = allowedChars[RandomGenerator.Next(0, allowedChars.Length)]; return new string(chars); } @@ -49,4 +44,10 @@ public static byte[] GetEmbeddedResourceAsByteArray(Type type, string path) stream.CopyTo(ms); return ms.ToArray(); } -} \ No newline at end of file + + public static void CopyEmbeddedResourceToMockFilesystem(Type type, string resourcePath, string mockFilePath, + IFileSystem fileSystem) + { + fileSystem.File.WriteAllBytes(mockFilePath, GetEmbeddedResourceAsByteArray(type, resourcePath)); + } +} diff --git a/PetroglyphTools.sln b/PetroglyphTools.sln index 863eb544d..3ef89cda7 100644 --- a/PetroglyphTools.sln +++ b/PetroglyphTools.sln @@ -27,6 +27,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PG.Testing", "PG.Testing\PG EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PG.Benchmarks", "PG.Benchmarks\PG.Benchmarks.csproj", "{469BAFA4-7C54-4736-B349-3D6E84AB33FA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PG.StarWarsGame.Components.Localisation", "PG.StarWarsGame.Components.Localisation\PG.StarWarsGame.Components.Localisation\PG.StarWarsGame.Components.Localisation.csproj", "{C74397F8-9C20-47A0-AD1F-EA276BC9BA63}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PG.StarWarsGame.Components.Localisation.Test", "PG.StarWarsGame.Components.Localisation\PG.StarWarsGame.Components.Localisation.Test\PG.StarWarsGame.Components.Localisation.Test.csproj", "{E825F2AF-978E-41AF-9E14-F1D6984018FA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -77,6 +81,14 @@ Global {469BAFA4-7C54-4736-B349-3D6E84AB33FA}.Debug|Any CPU.Build.0 = Debug|Any CPU {469BAFA4-7C54-4736-B349-3D6E84AB33FA}.Release|Any CPU.ActiveCfg = Release|Any CPU {469BAFA4-7C54-4736-B349-3D6E84AB33FA}.Release|Any CPU.Build.0 = Release|Any CPU + {C74397F8-9C20-47A0-AD1F-EA276BC9BA63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C74397F8-9C20-47A0-AD1F-EA276BC9BA63}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C74397F8-9C20-47A0-AD1F-EA276BC9BA63}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C74397F8-9C20-47A0-AD1F-EA276BC9BA63}.Release|Any CPU.Build.0 = Release|Any CPU + {E825F2AF-978E-41AF-9E14-F1D6984018FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E825F2AF-978E-41AF-9E14-F1D6984018FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E825F2AF-978E-41AF-9E14-F1D6984018FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E825F2AF-978E-41AF-9E14-F1D6984018FA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/PetroglyphTools.sln.DotSettings b/PetroglyphTools.sln.DotSettings new file mode 100644 index 000000000..afc589933 --- /dev/null +++ b/PetroglyphTools.sln.DotSettings @@ -0,0 +1,125 @@ + + <?xml version="1.0" encoding="utf-16"?><Profile name="Update Copyright"><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><CSOptimizeUsings></CSOptimizeUsings><CSUpdateFileHeader>True</CSUpdateFileHeader><IDEA_SETTINGS>&lt;profile version="1.0"&gt; + &lt;option name="myName" value="Update Copyright" /&gt; + &lt;inspection_tool class="ES6ShorthandObjectProperty" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="JSArrowFunctionBracesCanBeRemoved" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="JSRemoveUnnecessaryParentheses" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="JSUnnecessarySemicolon" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="UnnecessaryContinueJS" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="UnnecessaryLabelJS" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="UnnecessaryLabelOnBreakStatementJS" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="UnnecessaryLabelOnContinueStatementJS" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="UnnecessaryReturnJS" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="WrongPropertyKeyValueDelimiter" enabled="false" level="WEAK WARNING" enabled_by_default="false" /&gt; +&lt;/profile&gt;</IDEA_SETTINGS><RIDER_SETTINGS>&lt;profile&gt; + &lt;Language id="CSS"&gt; + &lt;Rearrange&gt;false&lt;/Rearrange&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="EditorConfig"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="HTML"&gt; + &lt;Rearrange&gt;false&lt;/Rearrange&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;OptimizeImports&gt;false&lt;/OptimizeImports&gt; + &lt;/Language&gt; + &lt;Language id="HTTP Request"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Handlebars"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Ini"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="JSON"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Jade"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="JavaScript"&gt; + &lt;Rearrange&gt;false&lt;/Rearrange&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;OptimizeImports&gt;false&lt;/OptimizeImports&gt; + &lt;/Language&gt; + &lt;Language id="Markdown"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Properties"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="RELAX-NG"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="SQL"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="VueExpr"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="XML"&gt; + &lt;Rearrange&gt;false&lt;/Rearrange&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;OptimizeImports&gt;false&lt;/OptimizeImports&gt; + &lt;/Language&gt; + &lt;Language id="yaml"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; +&lt;/profile&gt;</RIDER_SETTINGS></Profile> + // Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +$HEADER$namespace $NAMESPACE$ +{ + public class $CLASS$ {$END$} +} + // Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +$HEADER$namespace $NAMESPACE$ +{ + public partial class $CLASS$ : System.ComponentModel.Component + { + public $CLASS$() + { + InitializeComponent(); + } + + public $CLASS$(System.ComponentModel.IContainer container) + { + container.Add(this); + + InitializeComponent(); + } + } +} + // Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +$HEADER$namespace $NAMESPACE$ +{ + public record $RECORD$($END$); +} + // Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +$HEADER$namespace $NAMESPACE$ +{ + public interface $INTERFACE$ {$END$} +} + // Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +$HEADER$namespace $NAMESPACE$ +{ + public enum $ENUM$ {$END$} +} + // Copyright (c) Alamo Engine Tools and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +$HEADER$namespace $NAMESPACE$ +{ + public struct $STRUCT$ {$END$} +} \ No newline at end of file