diff --git a/PG.Commons/PG.Commons/PetroglyphCommons.cs b/PG.Commons/PG.Commons/PetroglyphCommons.cs
index 40cea4e9e..ebfd1c91f 100644
--- a/PG.Commons/PG.Commons/PetroglyphCommons.cs
+++ b/PG.Commons/PG.Commons/PetroglyphCommons.cs
@@ -3,6 +3,7 @@
using AnakinRaW.CommonUtilities.Hashing;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
using PG.Commons.Hashing;
namespace PG.Commons;
@@ -10,7 +11,7 @@ namespace PG.Commons;
///
/// Provides initialization routines for this library.
///
-public static class PetroglyphCommons
+public static class PetroglyphCommons
{
///
/// Adds all necessary services provided by this library to the specified .
@@ -18,7 +19,8 @@ public static class PetroglyphCommons
/// The to add services to.
public static void ContributeServices(IServiceCollection serviceCollection)
{
- serviceCollection.AddSingleton(new Crc32HashingProvider());
- serviceCollection.AddSingleton(sp => new Crc32HashingService(sp));
+ serviceCollection.TryAddSingleton(new Crc32HashingProvider());
+ serviceCollection.TryAddSingleton(sp => new HashingService(sp));
+ serviceCollection.TryAddSingleton(sp => new Crc32HashingService(sp));
}
-}
\ No newline at end of file
+}
diff --git a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/DatServiceContribution.cs b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/DatServiceContribution.cs
index 7dd1ad6a9..67a8b8a25 100644
--- a/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/DatServiceContribution.cs
+++ b/PG.StarWarsGame.Files.DAT/PG.StarWarsGame.Files.DAT/DatServiceContribution.cs
@@ -1,9 +1,13 @@
// 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 AnakinRaW.CommonUtilities.Hashing;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using PG.Commons;
using PG.StarWarsGame.Files.DAT.Binary;
using PG.StarWarsGame.Files.DAT.Services;
+
namespace PG.StarWarsGame.Files.DAT;
///
@@ -18,10 +22,13 @@ public static class DatServiceContribution
/// The to add services to.
public static void SupportDAT(this IServiceCollection serviceCollection)
{
+ PetroglyphCommons.ContributeServices(serviceCollection);
+ serviceCollection.TryAddSingleton(sp => new HashingService(sp));
+
serviceCollection
.AddSingleton(sp => new DatFileService(sp))
.AddSingleton(sp => new DatModelService(sp))
.AddTransient(sp => new DatFileReader(sp))
.AddTransient(sp => new DatBinaryConverter(sp));
}
-}
\ No newline at end of file
+}
diff --git a/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline.Test/BaselineTranslationProviderTest.cs b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline.Test/BaselineTranslationProviderTest.cs
new file mode 100644
index 000000000..cab76e729
--- /dev/null
+++ b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline.Test/BaselineTranslationProviderTest.cs
@@ -0,0 +1,68 @@
+// 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 Microsoft.Extensions.DependencyInjection;
+using PG.StarWarsGame.Localisation.Languages;
+using PG.Testing;
+using Xunit;
+
+namespace PG.StarWarsGame.Localisation.Baseline.Test;
+
+public class BaselineTranslationProviderTest : PGTestBase
+{
+ protected override void SetupServices(IServiceCollection serviceCollection)
+ {
+ base.SetupServices(serviceCollection);
+ serviceCollection.SupportLocalisationBaseline();
+ }
+
+ private IBaselineTranslationProvider Provider =>
+ ServiceProvider.GetRequiredService();
+
+ [Theory]
+ [InlineData(GameContext.EaW)]
+ [InlineData(GameContext.FoC)]
+ public void GetMasterText_SingleLanguage_IsNonEmpty(GameContext game)
+ {
+ var lang = new EnglishAlamoLanguageDefinition();
+ var db = Provider.GetMasterText(game, lang);
+ Assert.NotEmpty(db);
+ }
+
+ [Theory]
+ [InlineData(GameContext.EaW)]
+ [InlineData(GameContext.FoC)]
+ public void GetCreditsText_SingleLanguage_IsNonEmpty(GameContext game)
+ {
+ var lang = new EnglishAlamoLanguageDefinition();
+ var db = Provider.GetCreditsText(game, lang);
+ Assert.NotEmpty(db);
+ }
+
+ [Fact]
+ public void GetMasterText_MultiLanguage_ContainsAllRequestedLanguages()
+ {
+ var langs = new IAlamoLanguageDefinition[]
+ {
+ new EnglishAlamoLanguageDefinition(),
+ new GermanAlamoLanguageDefinition(),
+ };
+ var db = Provider.GetMasterText(GameContext.EaW, langs);
+ Assert.Equal(2, db.Languages.Count);
+ }
+
+ [Fact]
+ public void GetMasterText_EaW_English_ContainsKnownKey()
+ {
+ var lang = new EnglishAlamoLanguageDefinition();
+ var db = Provider.GetMasterText(GameContext.EaW, lang);
+ Assert.True(db.Count > 100);
+ }
+
+ [Fact]
+ public void GetMasterText_NullLanguage_Throws()
+ {
+ Assert.Throws(() => Provider.GetMasterText(GameContext.EaW, (IAlamoLanguageDefinition)null!));
+ }
+}
diff --git a/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline.Test/PG.StarWarsGame.Localisation.Baseline.Test.csproj b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline.Test/PG.StarWarsGame.Localisation.Baseline.Test.csproj
new file mode 100644
index 000000000..57c5afbaf
--- /dev/null
+++ b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline.Test/PG.StarWarsGame.Localisation.Baseline.Test.csproj
@@ -0,0 +1,34 @@
+
+
+ net8.0;net10.0
+ $(TargetFrameworks);net481
+
+
+ false
+ true
+ Exe
+
+
+
+
+
+
+
+ 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.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/BaselineServiceContribution.cs b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/BaselineServiceContribution.cs
new file mode 100644
index 000000000..43243cd3c
--- /dev/null
+++ b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/BaselineServiceContribution.cs
@@ -0,0 +1,28 @@
+// 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 Microsoft.Extensions.DependencyInjection;
+using PG.StarWarsGame.Files.DAT;
+
+namespace PG.StarWarsGame.Localisation.Baseline
+{
+ ///
+ /// Provides initialization routines for the PG.StarWarsGame.Localisation.Baseline library.
+ ///
+ public static class BaselineServiceContribution
+ {
+ ///
+ /// Adds all services required by this library, including DAT and localisation core services.
+ ///
+ public static IServiceCollection SupportLocalisationBaseline(this IServiceCollection serviceCollection)
+ {
+ serviceCollection.SupportDAT();
+ serviceCollection.SupportLocalisation();
+
+ serviceCollection.AddSingleton(sp =>
+ new BaselineTranslationProvider(sp));
+
+ return serviceCollection;
+ }
+ }
+}
diff --git a/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/BaselineTranslationProvider.cs b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/BaselineTranslationProvider.cs
new file mode 100644
index 000000000..46420396e
--- /dev/null
+++ b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/BaselineTranslationProvider.cs
@@ -0,0 +1,126 @@
+// 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.Reflection;
+using Microsoft.Extensions.DependencyInjection;
+using PG.Commons.Services;
+using PG.StarWarsGame.Files.DAT.Services;
+using PG.StarWarsGame.Localisation.Data;
+using PG.StarWarsGame.Localisation.IO.Dat;
+using PG.StarWarsGame.Localisation.Languages;
+
+namespace PG.StarWarsGame.Localisation.Baseline
+{
+ internal sealed class BaselineTranslationProvider : ServiceBase, IBaselineTranslationProvider
+ {
+ // The 5 western languages shipped with the original games have embedded baseline data.
+ // The remaining officially supported languages are mapped for future resource additions.
+ private static readonly IReadOnlyDictionary LanguageMap =
+ new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["ENGLISH"] = ("en", "english"),
+ ["GERMAN"] = ("de", "german"),
+ ["SPANISH"] = ("es", "spanish"),
+ ["FRENCH"] = ("fr", "french"),
+ ["ITALIAN"] = ("it", "italian"),
+ ["CHINESE"] = ("zh", "chinese"),
+ ["POLISH"] = ("pl", "polish"),
+ ["RUSSIAN"] = ("ru", "russian"),
+ ["JAPANESE"] = ("ja", "japanese"),
+ ["KOREAN"] = ("ko", "korean"),
+ ["THAI"] = ("th", "thai"),
+ };
+
+ private readonly IDatFileService _datFileService;
+ private readonly IDatTranslationImporter _importer;
+ private readonly ITranslationDatabaseFactory _factory;
+
+ public BaselineTranslationProvider(IServiceProvider services) : base(services)
+ {
+ _datFileService = services.GetRequiredService();
+ _importer = services.GetRequiredService();
+ _factory = services.GetRequiredService();
+ }
+
+ ///
+ public IKeyedTranslationDatabase GetMasterText(GameContext game, IAlamoLanguageDefinition language)
+ {
+ if (language is null) throw new ArgumentNullException(nameof(language));
+ var db = _factory.CreateKeyed(new[] { language });
+ LoadInto(db, game, "mastertextfile", language);
+ return db;
+ }
+
+ ///
+ public IKeyedTranslationDatabase GetMasterText(GameContext game, IReadOnlyList languages)
+ {
+ if (languages is null) throw new ArgumentNullException(nameof(languages));
+ var db = _factory.CreateKeyed(languages);
+ foreach (var lang in languages)
+ LoadInto(db, game, "mastertextfile", lang);
+ return db;
+ }
+
+ ///
+ public IOrderedTranslationDatabase GetCreditsText(GameContext game, IAlamoLanguageDefinition language)
+ {
+ if (language is null) throw new ArgumentNullException(nameof(language));
+ var db = _factory.CreateOrdered(new[] { language });
+ LoadInto(db, game, "creditstext", language);
+ return db;
+ }
+
+ ///
+ public IOrderedTranslationDatabase GetCreditsText(GameContext game, IReadOnlyList languages)
+ {
+ if (languages is null) throw new ArgumentNullException(nameof(languages));
+ var db = _factory.CreateOrdered(languages);
+ foreach (var lang in languages)
+ LoadInto(db, game, "creditstext", lang);
+ return db;
+ }
+
+ private void LoadInto(ITranslationDatabase db, GameContext game, string filePrefix, IAlamoLanguageDefinition language)
+ {
+ if (!LanguageMap.TryGetValue(language.LanguageIdentifier, out var map))
+ return;
+
+ var resourceName = BuildResourceName(game, map.Folder, $"{filePrefix}_{map.NameSuffix}.dat");
+ using var resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName);
+ if (resourceStream is null) return;
+
+ var tempPath = FileSystem.Path.Combine(
+ FileSystem.Path.GetTempPath(),
+ $"pg_baseline_{Guid.NewGuid():N}.dat");
+
+ try
+ {
+ using (var fs = FileSystem.FileStream.New(tempPath, FileMode.Create, FileAccess.Write))
+ resourceStream.CopyTo(fs);
+
+ var datFile = _datFileService.Load(tempPath);
+ _importer.Import(datFile.Content, language, db);
+ }
+ finally
+ {
+ try
+ {
+ FileSystem.File.Delete(tempPath);
+ }
+ catch
+ {
+ // NOP
+ }
+ }
+ }
+
+ private static string BuildResourceName(GameContext game, string langFolder, string fileName)
+ {
+ var gameFolder = game == GameContext.EaW ? "EaW" : "FoC";
+ return $"PG.StarWarsGame.Localisation.Baseline.Resources.{gameFolder}.{langFolder}.{fileName}";
+ }
+ }
+}
diff --git a/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/IBaselineTranslationProvider.cs b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/IBaselineTranslationProvider.cs
new file mode 100644
index 000000000..527085373
--- /dev/null
+++ b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/IBaselineTranslationProvider.cs
@@ -0,0 +1,51 @@
+// 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.Localisation.Data;
+using PG.StarWarsGame.Localisation.Languages;
+
+namespace PG.StarWarsGame.Localisation.Baseline
+{
+ ///
+ /// Provides pre-loaded translation databases built from the official EaW and FoC game localisations.
+ ///
+ public interface IBaselineTranslationProvider
+ {
+ ///
+ /// Returns a keyed translation database (MasterText) for the given game and single language.
+ ///
+ ///
+ /// Languages with no baseline data contribute no entries to the returned database.
+ ///
+ /// is .
+ IKeyedTranslationDatabase GetMasterText(GameContext game, IAlamoLanguageDefinition language);
+
+ ///
+ /// Returns a keyed translation database (MasterText) merged from the given game and languages.
+ ///
+ ///
+ /// Languages with no baseline data contribute no entries to the returned database.
+ ///
+ /// is .
+ IKeyedTranslationDatabase GetMasterText(GameContext game, IReadOnlyList languages);
+
+ ///
+ /// Returns an ordered translation database (CreditsText) for the given game and single language.
+ ///
+ ///
+ /// Languages with no baseline data contribute no entries to the returned database.
+ ///
+ /// is .
+ IOrderedTranslationDatabase GetCreditsText(GameContext game, IAlamoLanguageDefinition language);
+
+ ///
+ /// Returns an ordered translation database (CreditsText) merged from the given game and languages.
+ ///
+ ///
+ /// Languages with no baseline data contribute no entries to the returned database.
+ ///
+ /// is .
+ IOrderedTranslationDatabase GetCreditsText(GameContext game, IReadOnlyList languages);
+ }
+}
diff --git a/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline.csproj b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline.csproj
new file mode 100644
index 000000000..f9f155c35
--- /dev/null
+++ b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline.csproj
@@ -0,0 +1,61 @@
+
+
+ netstandard2.0;netstandard2.1;net10.0
+ PG.StarWarsGame.Localisation.Baseline
+ Provides baseline translation databases pre-loaded with official EaW and FoC game localisations.
+ PG.StarWarsGame.Localisation.Baseline
+ AlamoEngineTools.PG.StarWarsGame.Localisation.Baseline
+ alamo;petroglyph;glyphx;localisation;translation;baseline
+
+
+ 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.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/EaW/de/creditstext_german.dat b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/EaW/de/creditstext_german.dat
similarity index 100%
rename from PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/EaW/de/creditstext_german.dat
rename to PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/EaW/de/creditstext_german.dat
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/EaW/de/mastertextfile_german.dat b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/EaW/de/mastertextfile_german.dat
similarity index 100%
rename from PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/EaW/de/mastertextfile_german.dat
rename to PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/EaW/de/mastertextfile_german.dat
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/EaW/en/creditstext_english.dat b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/EaW/en/creditstext_english.dat
similarity index 100%
rename from PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/EaW/en/creditstext_english.dat
rename to PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/EaW/en/creditstext_english.dat
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/EaW/en/mastertextfile_english.dat b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/EaW/en/mastertextfile_english.dat
similarity index 100%
rename from PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/EaW/en/mastertextfile_english.dat
rename to PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/EaW/en/mastertextfile_english.dat
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/EaW/es/creditstext_spanish.dat b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/EaW/es/creditstext_spanish.dat
similarity index 100%
rename from PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/EaW/es/creditstext_spanish.dat
rename to PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/EaW/es/creditstext_spanish.dat
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/EaW/es/mastertextfile_spanish.dat b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/EaW/es/mastertextfile_spanish.dat
similarity index 100%
rename from PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/EaW/es/mastertextfile_spanish.dat
rename to PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/EaW/es/mastertextfile_spanish.dat
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/EaW/fr/creditstext_french.dat b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/EaW/fr/creditstext_french.dat
similarity index 100%
rename from PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/EaW/fr/creditstext_french.dat
rename to PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/EaW/fr/creditstext_french.dat
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/EaW/fr/mastertextfile_french.dat b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/EaW/fr/mastertextfile_french.dat
similarity index 100%
rename from PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/EaW/fr/mastertextfile_french.dat
rename to PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/EaW/fr/mastertextfile_french.dat
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/EaW/it/creditstext_italian.dat b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/EaW/it/creditstext_italian.dat
similarity index 100%
rename from PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/EaW/it/creditstext_italian.dat
rename to PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/EaW/it/creditstext_italian.dat
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/EaW/it/mastertextfile_italian.dat b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/EaW/it/mastertextfile_italian.dat
similarity index 100%
rename from PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/EaW/it/mastertextfile_italian.dat
rename to PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/EaW/it/mastertextfile_italian.dat
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/FoC/de/creditstext_german.dat b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/FoC/de/creditstext_german.dat
similarity index 100%
rename from PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/FoC/de/creditstext_german.dat
rename to PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/FoC/de/creditstext_german.dat
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/FoC/de/mastertextfile_german.dat b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/FoC/de/mastertextfile_german.dat
similarity index 100%
rename from PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/FoC/de/mastertextfile_german.dat
rename to PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/FoC/de/mastertextfile_german.dat
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/FoC/en/creditstext_english.dat b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/FoC/en/creditstext_english.dat
similarity index 100%
rename from PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/FoC/en/creditstext_english.dat
rename to PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/FoC/en/creditstext_english.dat
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/FoC/en/mastertextfile_english.dat b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/FoC/en/mastertextfile_english.dat
similarity index 100%
rename from PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/FoC/en/mastertextfile_english.dat
rename to PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/FoC/en/mastertextfile_english.dat
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/FoC/es/creditstext_spanish.dat b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/FoC/es/creditstext_spanish.dat
similarity index 100%
rename from PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/FoC/es/creditstext_spanish.dat
rename to PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/FoC/es/creditstext_spanish.dat
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/FoC/es/mastertextfile_spanish.dat b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/FoC/es/mastertextfile_spanish.dat
similarity index 100%
rename from PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/FoC/es/mastertextfile_spanish.dat
rename to PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/FoC/es/mastertextfile_spanish.dat
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/FoC/fr/creditstext_french.dat b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/FoC/fr/creditstext_french.dat
similarity index 100%
rename from PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/FoC/fr/creditstext_french.dat
rename to PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/FoC/fr/creditstext_french.dat
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/FoC/fr/mastertextfile_french.dat b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/FoC/fr/mastertextfile_french.dat
similarity index 100%
rename from PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/FoC/fr/mastertextfile_french.dat
rename to PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/FoC/fr/mastertextfile_french.dat
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/FoC/it/creditstext_italian.dat b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/FoC/it/creditstext_italian.dat
similarity index 100%
rename from PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/FoC/it/creditstext_italian.dat
rename to PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/FoC/it/creditstext_italian.dat
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/FoC/it/mastertextfile_italian.dat b/PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/FoC/it/mastertextfile_italian.dat
similarity index 100%
rename from PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Resources/Dat/FoC/it/mastertextfile_italian.dat
rename to PG.StarWarsGame.Localisation.Baseline/PG.StarWarsGame.Localisation.Baseline/Resources/FoC/it/mastertextfile_italian.dat
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/CommonLocalisationTestBase.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/CommonLocalisationTestBase.cs
new file mode 100644
index 000000000..19fc2ddd7
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/CommonLocalisationTestBase.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 Microsoft.Extensions.DependencyInjection;
+using PG.StarWarsGame.Files.DAT;
+using PG.StarWarsGame.Localisation.IO.Csv;
+using PG.StarWarsGame.Localisation.IO.Dat;
+using PG.StarWarsGame.Localisation.IO.Properties;
+using PG.StarWarsGame.Localisation.IO.Xml;
+using PG.StarWarsGame.Localisation.Languages;
+using PG.StarWarsGame.Localisation.Services;
+using PG.Testing;
+
+namespace PG.StarWarsGame.Localisation.Test;
+
+public abstract class CommonLocalisationTestBase : PGTestBase
+{
+ protected override void SetupServices(IServiceCollection serviceCollection)
+ {
+ base.SetupServices(serviceCollection);
+ serviceCollection.SupportDAT();
+
+ serviceCollection.AddSingleton(new EnglishAlamoLanguageDefinition());
+ serviceCollection.AddSingleton(new GermanAlamoLanguageDefinition());
+ serviceCollection.AddSingleton(new FrenchAlamoLanguageDefinition());
+ serviceCollection.AddSingleton(new SpanishAlamoLanguageDefinition());
+ serviceCollection.AddSingleton(new ItalianAlamoLanguageDefinition());
+ serviceCollection.AddSingleton(new ChineseAlamoLanguageDefinition());
+ serviceCollection.AddSingleton(new JapaneseAlamoLanguageDefinition());
+ serviceCollection.AddSingleton(new KoreanAlamoLanguageDefinition());
+ serviceCollection.AddSingleton(new PolishAlamoLanguageDefinition());
+ serviceCollection.AddSingleton(new RussianAlamoLanguageDefinition());
+ serviceCollection.AddSingleton(new ThaiAlamoLanguageDefinition());
+
+ serviceCollection.AddSingleton(sp =>
+ new LanguageService(sp.GetServices()));
+
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Data/KeyedTranslationDatabaseTest.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Data/KeyedTranslationDatabaseTest.cs
new file mode 100644
index 000000000..6ab783ed2
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Data/KeyedTranslationDatabaseTest.cs
@@ -0,0 +1,227 @@
+// 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.Localisation.Data;
+using PG.StarWarsGame.Localisation.Languages;
+using Xunit;
+
+namespace PG.StarWarsGame.Localisation.Test.Data;
+
+public class KeyedTranslationDatabaseTest
+{
+ private static readonly IAlamoLanguageDefinition En = new EnglishAlamoLanguageDefinition();
+ private static readonly IAlamoLanguageDefinition De = new GermanAlamoLanguageDefinition();
+
+ private static IKeyedTranslationDatabase Create(params IAlamoLanguageDefinition[] langs) =>
+ new TranslationDatabaseFactory().CreateKeyed(langs);
+
+ // --- empty state ---
+
+ [Fact]
+ public void Empty_HasZeroEntries()
+ {
+ var db = Create(En);
+ Assert.Empty(db);
+ }
+
+ [Fact]
+ public void Empty_ContainsKey_ReturnsFalse()
+ {
+ var db = Create(En);
+ Assert.False(db.ContainsKey("KEY"));
+ }
+
+ [Fact]
+ public void Empty_TryGetEntry_ReturnsFalse()
+ {
+ var db = Create(En);
+ Assert.False(db.TryGetEntry("KEY", out _));
+ }
+
+ // --- single-language CRUD ---
+
+ [Fact]
+ public void SetTranslation_AddsNewEntry()
+ {
+ var db = Create(En);
+ db.SetTranslation("HELLO", En, "Hello");
+ Assert.True(db.ContainsKey("HELLO"));
+ Assert.Single(db);
+ }
+
+ [Fact]
+ public void SetTranslation_UpdatesExistingValue()
+ {
+ var db = Create(En);
+ db.SetTranslation("HELLO", En, "Hello");
+ db.SetTranslation("HELLO", En, "Hi");
+ Assert.Single(db);
+ Assert.True(db.TryGetEntry("HELLO", out var entry));
+ Assert.True(entry!.TryGetTranslation(En, out var val));
+ Assert.Equal("Hi", val);
+ }
+
+ [Fact]
+ public void RemoveEntry_RemovesKey()
+ {
+ var db = Create(En);
+ db.SetTranslation("HELLO", En, "Hello");
+ Assert.True(db.RemoveEntry("HELLO"));
+ Assert.False(db.ContainsKey("HELLO"));
+ Assert.Empty(db);
+ }
+
+ [Fact]
+ public void RemoveEntry_ReturnsFalse_WhenKeyMissing()
+ {
+ var db = Create(En);
+ Assert.False(db.RemoveEntry("MISSING"));
+ }
+
+ [Fact]
+ public void Clear_RemovesAllEntries()
+ {
+ var db = Create(En);
+ db.SetTranslation("A", En, "a");
+ db.SetTranslation("B", En, "b");
+ db.Clear();
+ Assert.Empty(db);
+ }
+
+ // --- multi-language ---
+
+ [Fact]
+ public void SetTranslation_MultipleLanguages_StoredPerLanguage()
+ {
+ var db = Create(En, De);
+ db.SetTranslation("HELLO", En, "Hello");
+ db.SetTranslation("HELLO", De, "Hallo");
+
+ Assert.True(db.TryGetEntry("HELLO", out var entry));
+ Assert.True(entry!.TryGetTranslation(En, out var en));
+ Assert.True(entry.TryGetTranslation(De, out var de));
+ Assert.Equal("Hello", en);
+ Assert.Equal("Hallo", de);
+ }
+
+ [Fact]
+ public void TryGetTranslation_ReturnsFalse_ForMissingLanguage()
+ {
+ var db = Create(En);
+ db.SetTranslation("HELLO", En, "Hello");
+ Assert.True(db.TryGetEntry("HELLO", out var entry));
+ Assert.False(entry!.TryGetTranslation(De, out _));
+ }
+
+ // --- uniqueness ---
+
+ [Fact]
+ public void SetTranslation_SameKey_DoesNotCreateDuplicate()
+ {
+ var db = Create(En, De);
+ db.SetTranslation("KEY", En, "English");
+ db.SetTranslation("KEY", De, "Deutsch");
+ Assert.Single(db);
+ }
+
+ // --- Languages list ---
+
+ [Fact]
+ public void Languages_ReflectsConstructorInput()
+ {
+ var db = Create(En, De);
+ Assert.Equal(2, db.Languages.Count);
+ Assert.Contains(En, db.Languages);
+ Assert.Contains(De, db.Languages);
+ }
+
+ // --- enumeration ---
+
+ [Fact]
+ public void Enumeration_ReturnsAllEntries()
+ {
+ var db = Create(En);
+ db.SetTranslation("A", En, "a");
+ db.SetTranslation("B", En, "b");
+ Assert.Equal(2, db.ToList().Count);
+ }
+
+ // --- null guards ---
+
+ [Fact]
+ public void SetTranslation_Throws_WhenKeyIsNull()
+ {
+ var db = Create(En);
+ Assert.Throws(() => db.SetTranslation(null!, En, "v"));
+ }
+
+ [Fact]
+ public void SetTranslation_Throws_WhenLanguageIsNull()
+ {
+ var db = Create(En);
+ Assert.Throws(() => db.SetTranslation("K", null!, "v"));
+ }
+
+ // --- ActiveLanguage ---
+
+ [Fact]
+ public void ActiveLanguage_DefaultsToNull()
+ {
+ var db = Create(En);
+ Assert.Null(db.ActiveLanguage);
+ }
+
+ [Fact]
+ public void ActiveLanguage_CanBeSet_WhenRegistered()
+ {
+ var db = Create(En);
+ db.ActiveLanguage = En;
+ Assert.Equal(En, db.ActiveLanguage);
+ }
+
+ [Fact]
+ public void ActiveLanguage_Throws_WhenNotInLanguages()
+ {
+ var db = Create(En);
+ Assert.Throws(() => db.ActiveLanguage = De);
+ }
+
+ [Fact]
+ public void ActiveLanguage_CanBeSetToNull()
+ {
+ var db = Create(En);
+ db.ActiveLanguage = En;
+ db.ActiveLanguage = null;
+ Assert.Null(db.ActiveLanguage);
+ }
+
+ // --- TryGetTranslation (active language) ---
+
+ [Fact]
+ public void TryGetTranslation_ByActiveLanguage_ReturnsValue()
+ {
+ var db = Create(En);
+ db.SetTranslation("HELLO", En, "Hello");
+ db.ActiveLanguage = En;
+ Assert.True(db.TryGetTranslation("HELLO", out var val));
+ Assert.Equal("Hello", val);
+ }
+
+ [Fact]
+ public void TryGetTranslation_ByActiveLanguage_ReturnsFalse_WhenKeyMissing()
+ {
+ var db = Create(En);
+ db.ActiveLanguage = En;
+ Assert.False(db.TryGetTranslation("MISSING", out _));
+ }
+
+ [Fact]
+ public void TryGetTranslation_Throws_WhenActiveLanguageIsNull()
+ {
+ var db = Create(En);
+ db.SetTranslation("HELLO", En, "Hello");
+ Assert.Throws(() => db.TryGetTranslation("HELLO", out _));
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Data/OrderedTranslationDatabaseTest.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Data/OrderedTranslationDatabaseTest.cs
new file mode 100644
index 000000000..c37fab51a
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Data/OrderedTranslationDatabaseTest.cs
@@ -0,0 +1,129 @@
+// 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.Localisation.Data;
+using PG.StarWarsGame.Localisation.Languages;
+using Xunit;
+
+namespace PG.StarWarsGame.Localisation.Test.Data;
+
+public class OrderedTranslationDatabaseTest
+{
+ private static readonly IAlamoLanguageDefinition En = new EnglishAlamoLanguageDefinition();
+ private static readonly IAlamoLanguageDefinition De = new GermanAlamoLanguageDefinition();
+
+ private static IOrderedTranslationDatabase Create(params IAlamoLanguageDefinition[] langs) =>
+ new TranslationDatabaseFactory().CreateOrdered(langs);
+
+ [Fact]
+ public void Empty_HasZeroEntries()
+ {
+ var db = Create(En);
+ Assert.Empty(db);
+ }
+
+ [Fact]
+ public void SetTranslation_PreservesInsertionOrder()
+ {
+ var db = Create(En);
+ db.SetTranslation("A", En, "First");
+ db.SetTranslation("B", En, "Second");
+ db.SetTranslation("C", En, "Third");
+ var keys = db.Select(e => e.Key).ToList();
+ Assert.Equal(new[] { "A", "B", "C" }, keys);
+ }
+
+ [Fact]
+ public void SetTranslation_AllowsDuplicateKeys()
+ {
+ var db = Create(En);
+ db.SetTranslation("CREDITS_LINE", En, "Line 1");
+ db.SetTranslation("CREDITS_LINE", En, "Line 2");
+ Assert.Equal(2, db.Count);
+ }
+
+ [Fact]
+ public void GetAllEntriesForKey_ReturnsAllDuplicates()
+ {
+ var db = Create(En);
+ db.SetTranslation("K", En, "First");
+ db.SetTranslation("K", En, "Second");
+ var entries = db.GetAllEntriesForKey("K");
+ Assert.Equal(2, entries.Count);
+ }
+
+ [Fact]
+ public void GetAllEntriesForKey_ReturnsEmpty_WhenKeyMissing()
+ {
+ var db = Create(En);
+ Assert.Empty(db.GetAllEntriesForKey("MISSING"));
+ }
+
+ [Fact]
+ public void InsertAt_InsertsAtCorrectPosition()
+ {
+ var db = Create(En);
+ db.SetTranslation("A", En, "First");
+ db.SetTranslation("C", En, "Third");
+ db.InsertAt(1, "B", En, "Second");
+ var keys = db.Select(e => e.Key).ToList();
+ Assert.Equal(new[] { "A", "B", "C" }, keys);
+ }
+
+ [Fact]
+ public void RemoveEntry_RemovesFirstOccurrence()
+ {
+ var db = Create(En);
+ db.SetTranslation("K", En, "First");
+ db.SetTranslation("K", En, "Second");
+ db.RemoveEntry("K");
+ Assert.Single(db);
+ }
+
+ [Fact]
+ public void Clear_RemovesAll()
+ {
+ var db = Create(En);
+ db.SetTranslation("A", En, "a");
+ db.SetTranslation("A", En, "a2");
+ db.Clear();
+ Assert.Empty(db);
+ }
+
+ [Fact]
+ public void MultiLanguage_StoredPerLanguagePerEntry()
+ {
+ var db = Create(En, De);
+ db.SetTranslation("K", En, "English");
+ db.SetTranslation("K", De, "Deutsch");
+ Assert.Equal(2, db.Count);
+ var allK = db.GetAllEntriesForKey("K");
+ Assert.Equal(2, allK.Count);
+ }
+
+ // --- ActiveLanguage ---
+
+ [Fact]
+ public void ActiveLanguage_DefaultsToNull()
+ {
+ var db = Create(En);
+ Assert.Null(db.ActiveLanguage);
+ }
+
+ [Fact]
+ public void ActiveLanguage_CanBeSet()
+ {
+ var db = Create(En);
+ db.ActiveLanguage = En;
+ Assert.Equal(En, db.ActiveLanguage);
+ }
+
+ [Fact]
+ public void ActiveLanguage_Throws_WhenNotInLanguages()
+ {
+ var db = Create(En);
+ Assert.Throws(() => db.ActiveLanguage = De);
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Data/TranslationDatabaseBuilderTest.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Data/TranslationDatabaseBuilderTest.cs
new file mode 100644
index 000000000..32bb9a00b
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Data/TranslationDatabaseBuilderTest.cs
@@ -0,0 +1,93 @@
+// 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.Localisation.Data;
+using PG.StarWarsGame.Localisation.Languages;
+using Xunit;
+
+namespace PG.StarWarsGame.Localisation.Test.Data;
+
+public class TranslationDatabaseBuilderTest
+{
+ private static readonly IAlamoLanguageDefinition En = new EnglishAlamoLanguageDefinition();
+ private static readonly IAlamoLanguageDefinition De = new GermanAlamoLanguageDefinition();
+
+ private static ITranslationDatabaseBuilder Builder() =>
+ new TranslationDatabaseFactory().CreateDatabase();
+
+ [Fact]
+ public void CreateDatabase_BuildsKeyedDatabase()
+ {
+ var db = Builder().WithLanguage(En).BuildKeyed();
+ Assert.NotNull(db);
+ }
+
+ [Fact]
+ public void CreateDatabase_BuildsOrderedDatabase()
+ {
+ var db = Builder().WithLanguage(En).BuildOrdered();
+ Assert.NotNull(db);
+ }
+
+ [Fact]
+ public void WithLanguage_AddsSingleLanguage()
+ {
+ var db = Builder().WithLanguage(En).BuildKeyed();
+ Assert.Single(db.Languages);
+ Assert.Contains(En, db.Languages);
+ }
+
+ [Fact]
+ public void WithLanguages_AddsMultipleLanguages()
+ {
+ var db = Builder().WithLanguages(new[] { En, De }).BuildKeyed();
+ Assert.Equal(2, db.Languages.Count);
+ }
+
+ [Fact]
+ public void WithActiveLanguage_SetsActiveLanguageOnBuiltDatabase()
+ {
+ var db = Builder().WithLanguage(En).SetActiveLanguage(En).BuildKeyed();
+ Assert.Equal(En, db.ActiveLanguage);
+ }
+
+ [Fact]
+ public void WithActiveLanguage_NotInLanguages_ThrowsAtBuildTime()
+ {
+ Assert.Throws(() =>
+ Builder().WithLanguage(En).SetActiveLanguage(De).BuildKeyed());
+ }
+
+ [Fact]
+ public void WithLanguages_Empty_BuildsEmptyLanguagesDatabase()
+ {
+ var db = Builder().BuildKeyed();
+ Assert.Empty(db.Languages);
+ }
+
+ [Fact]
+ public void Builder_IsChainable()
+ {
+ var db = Builder()
+ .WithLanguage(En)
+ .WithLanguage(De)
+ .SetActiveLanguage(En)
+ .BuildOrdered();
+
+ Assert.Equal(2, db.Languages.Count);
+ Assert.Equal(En, db.ActiveLanguage);
+ }
+
+ [Fact]
+ public void WithLanguage_Throws_OnNull()
+ {
+ Assert.Throws(() => Builder().WithLanguage(null!));
+ }
+
+ [Fact]
+ public void WithLanguages_Throws_OnNull()
+ {
+ Assert.Throws(() => Builder().WithLanguages(null!));
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Data/TranslationProjectDescriptorTest.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Data/TranslationProjectDescriptorTest.cs
new file mode 100644
index 000000000..641fe6917
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Data/TranslationProjectDescriptorTest.cs
@@ -0,0 +1,51 @@
+// 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.Localisation.Data.Config;
+using PG.StarWarsGame.Localisation.Data.Config.v2;
+using PG.StarWarsGame.Localisation.Languages;
+using Xunit;
+
+namespace PG.StarWarsGame.Localisation.Test.Data;
+
+public class TranslationProjectDescriptorTest
+{
+ private static readonly IReadOnlyList TwoLangs =
+ new IAlamoLanguageDefinition[] { new EnglishAlamoLanguageDefinition(), new GermanAlamoLanguageDefinition() };
+
+ [Fact]
+ public void Constructor_SetsAllProperties()
+ {
+ var d = new TranslationProjectDescriptor(GameContext.EaW, OverrideType.Core, TranslationResourceType.Dat, TwoLangs);
+ Assert.Equal(GameContext.EaW, d.Game);
+ Assert.Equal(OverrideType.Core, d.OverrideType);
+ Assert.Equal(TranslationResourceType.Dat, d.ResourceType);
+ Assert.Equal(2, d.Languages.Count);
+ }
+
+ [Fact]
+ public void Constructor_Throws_WhenLanguagesIsNull()
+ {
+ Assert.Throws(() =>
+ new TranslationProjectDescriptor(GameContext.EaW, OverrideType.Core, TranslationResourceType.Dat, null!));
+ }
+
+ [Fact]
+ public void Languages_IsImmutable()
+ {
+ var mutable = new List { new EnglishAlamoLanguageDefinition() };
+ var d = new TranslationProjectDescriptor(GameContext.EaW, OverrideType.Core, TranslationResourceType.Xml, mutable);
+ mutable.Add(new GermanAlamoLanguageDefinition());
+ Assert.Single(d.Languages);
+ }
+
+ [Fact]
+ public void Equality_TwoDescriptorsWithSameValues_AreEqual()
+ {
+ var a = new TranslationProjectDescriptor(GameContext.FoC, OverrideType.Mod, TranslationResourceType.Csv, TwoLangs);
+ var b = new TranslationProjectDescriptor(GameContext.FoC, OverrideType.Mod, TranslationResourceType.Csv, TwoLangs);
+ Assert.Equal(a, b);
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/IO/CsvTranslationAdapterTest.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/IO/CsvTranslationAdapterTest.cs
new file mode 100644
index 000000000..458772d2b
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/IO/CsvTranslationAdapterTest.cs
@@ -0,0 +1,116 @@
+// 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 Microsoft.Extensions.DependencyInjection;
+using PG.StarWarsGame.Localisation.Data;
+using PG.StarWarsGame.Localisation.IO.Csv;
+using PG.StarWarsGame.Localisation.Languages;
+using Xunit;
+
+namespace PG.StarWarsGame.Localisation.Test.IO;
+
+public class CsvTranslationAdapterTest : CommonLocalisationTestBase
+{
+ private static readonly IAlamoLanguageDefinition En = new EnglishAlamoLanguageDefinition();
+ private static readonly IAlamoLanguageDefinition De = new GermanAlamoLanguageDefinition();
+
+ private ICsvTranslationImporter CreateImporter() =>
+ ServiceProvider.GetRequiredService();
+
+ private ICsvTranslationExporter CreateExporter() =>
+ ServiceProvider.GetRequiredService();
+
+ [Fact]
+ public void RoundTrip_MultiLanguage_PreservesAllTranslations()
+ {
+ var db = new TranslationDatabaseFactory().CreateKeyed(new[] { En, De });
+ db.SetTranslation("HELLO", En, "Hello");
+ db.SetTranslation("HELLO", De, "Hallo");
+
+ var csv = CreateExporter().Export(db);
+ var db2 = new TranslationDatabaseFactory().CreateKeyed(new[] { En, De });
+ CreateImporter().Import(new StringReader(csv), db2);
+
+ Assert.True(db2.TryGetEntry("HELLO", out var entry));
+ Assert.True(entry!.TryGetTranslation(En, out var en));
+ Assert.True(entry.TryGetTranslation(De, out var de));
+ Assert.Equal("Hello", en);
+ Assert.Equal("Hallo", de);
+ }
+
+ [Fact]
+ public void Export_ContainsHeaderRow()
+ {
+ var db = new TranslationDatabaseFactory().CreateKeyed(new[] { En, De });
+ db.SetTranslation("K", En, "v");
+ var csv = CreateExporter().Export(db);
+ var firstLine = csv.Split('\n')[0];
+ Assert.Contains("ENGLISH", firstLine);
+ Assert.Contains("GERMAN", firstLine);
+ }
+
+ [Fact]
+ public void RoundTrip_ValueWithComma_IsPreserved()
+ {
+ var db = new TranslationDatabaseFactory().CreateKeyed(new[] { En });
+ db.SetTranslation("K", En, "Hello, World");
+ var csv = CreateExporter().Export(db);
+ var db2 = new TranslationDatabaseFactory().CreateKeyed(new[] { En });
+ CreateImporter().Import(new StringReader(csv), db2);
+ Assert.True(db2.TryGetEntry("K", out var entry));
+ Assert.True(entry!.TryGetTranslation(En, out var val));
+ Assert.Equal("Hello, World", val);
+ }
+
+ [Fact]
+ public void Import_EmptyCsv_LeavesDbEmpty()
+ {
+ var db = new TranslationDatabaseFactory().CreateKeyed(new[] { En });
+ CreateImporter().Import(new StringReader("key,ENGLISH\n"), db);
+ Assert.Empty(db);
+ }
+
+ [Fact]
+ public void RoundTrip_ValueWithEmbeddedQuotes_IsPreserved()
+ {
+ var db = new TranslationDatabaseFactory().CreateKeyed(new[] { En });
+ db.SetTranslation("K", En, "The \"Resolute\" is a Venator");
+ var csv = CreateExporter().Export(db);
+ var db2 = new TranslationDatabaseFactory().CreateKeyed(new[] { En });
+ CreateImporter().Import(new StringReader(csv), db2);
+ Assert.True(db2.TryGetEntry("K", out var entry));
+ Assert.True(entry!.TryGetTranslation(En, out var val));
+ Assert.Equal("The \"Resolute\" is a Venator", val);
+ }
+
+ [Fact]
+ public void Import_CrlfLineEndings_AreHandledCorrectly()
+ {
+ var csv = "key,ENGLISH\r\nHELLO,Hello\r\nWORLD,World\r\n";
+ var db = new TranslationDatabaseFactory().CreateKeyed(new[] { En });
+ CreateImporter().Import(new StringReader(csv), db);
+ Assert.True(db.TryGetEntry("HELLO", out var e1));
+ Assert.True(e1!.TryGetTranslation(En, out var v1));
+ Assert.Equal("Hello", v1);
+ Assert.True(db.TryGetEntry("WORLD", out var e2));
+ Assert.True(e2!.TryGetTranslation(En, out var v2));
+ Assert.Equal("World", v2);
+ }
+
+ [Fact]
+ public void Export_KeyedDatabase_WritesKeysAlphabetically()
+ {
+ var db = new TranslationDatabaseFactory().CreateKeyed(new[] { En });
+ db.SetTranslation("ZEBRA", En, "z");
+ db.SetTranslation("APPLE", En, "a");
+ db.SetTranslation("MONKEY", En, "m");
+
+ var csv = CreateExporter().Export(db);
+ var lines = csv.Split('\n');
+ // lines[0] is header; data lines follow
+ Assert.Equal("APPLE", lines[1].Split(',')[0]);
+ Assert.Equal("MONKEY", lines[2].Split(',')[0]);
+ Assert.Equal("ZEBRA", lines[3].Split(',')[0]);
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/IO/DatTranslationAdapterTest.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/IO/DatTranslationAdapterTest.cs
new file mode 100644
index 000000000..e3eb1846c
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/IO/DatTranslationAdapterTest.cs
@@ -0,0 +1,96 @@
+// 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 Microsoft.Extensions.DependencyInjection;
+using PG.StarWarsGame.Files.DAT.Services.Builder;
+using PG.StarWarsGame.Localisation.Data;
+using PG.StarWarsGame.Localisation.IO.Dat;
+using PG.StarWarsGame.Localisation.Languages;
+using Xunit;
+
+namespace PG.StarWarsGame.Localisation.Test.IO;
+
+public class DatTranslationAdapterTest : CommonLocalisationTestBase
+{
+ private static readonly IAlamoLanguageDefinition En = new EnglishAlamoLanguageDefinition();
+
+ private IDatTranslationImporter CreateImporter() =>
+ ServiceProvider.GetRequiredService();
+
+ private IDatTranslationExporter CreateExporter() =>
+ ServiceProvider.GetRequiredService();
+
+ [Fact]
+ public void RoundTrip_KeyedDatabase_PreservesEntriesAndValues()
+ {
+ var builder = new EmpireAtWarMasterTextBuilder(false, ServiceProvider);
+ builder.AddEntry("HELLO", "Hello World");
+ builder.AddEntry("GOODBYE", "Goodbye World");
+ var model = builder.BuildModel();
+
+ var db = new TranslationDatabaseFactory().CreateKeyed(new[] { En });
+ CreateImporter().Import(model, En, db);
+
+ Assert.True(db.ContainsKey("HELLO"));
+ Assert.True(db.ContainsKey("GOODBYE"));
+ Assert.True(db.TryGetEntry("HELLO", out var entry));
+ Assert.True(entry!.TryGetTranslation(En, out var val));
+ Assert.Equal("Hello World", val);
+ }
+
+ [Fact]
+ public void RoundTrip_OrderedDatabase_PreservesOrder()
+ {
+ var builder = new EmpireAtWarCreditsTextBuilder(ServiceProvider);
+ builder.AddEntry("LINE", "First");
+ builder.AddEntry("LINE", "Second");
+ var model = builder.BuildModel();
+
+ var db = new TranslationDatabaseFactory().CreateOrdered(new[] { En });
+ CreateImporter().Import(model, En, db);
+
+ Assert.Equal(2, db.Count);
+ Assert.True(db[0].TryGetTranslation(En, out var first));
+ Assert.True(db[1].TryGetTranslation(En, out var second));
+ Assert.Equal("First", first);
+ Assert.Equal("Second", second);
+ }
+
+ [Fact]
+ public void Export_KeyedDatabase_ProducesEquivalentDatModel()
+ {
+ var db = new TranslationDatabaseFactory().CreateKeyed(new[] { En });
+ db.SetTranslation("HELLO", En, "Hello");
+ db.SetTranslation("BYE", En, "Bye");
+
+ var exported = CreateExporter().Export(db, En);
+ Assert.Equal(2, exported.Count);
+ Assert.Equal("Hello", exported.GetValue("HELLO"));
+ Assert.Equal("Bye", exported.GetValue("BYE"));
+ }
+
+ [Fact]
+ public void Export_OrderedDatabase_PreservesInsertionOrder()
+ {
+ var db = new TranslationDatabaseFactory().CreateOrdered(new[] { En });
+ db.SetTranslation("LINE", En, "First");
+ db.SetTranslation("LINE", En, "Second");
+
+ var exported = CreateExporter().Export(db, En);
+ Assert.Equal(2, exported.Count);
+ var entries = new List(exported);
+ Assert.Equal("First", entries[0].Value);
+ Assert.Equal("Second", entries[1].Value);
+ }
+
+ [Fact]
+ public void Import_EmptyModel_LeavesDbEmpty()
+ {
+ var builder = new EmpireAtWarMasterTextBuilder(false, ServiceProvider);
+ var model = builder.BuildModel();
+ var db = new TranslationDatabaseFactory().CreateKeyed(new[] { En });
+ CreateImporter().Import(model, En, db);
+ Assert.Empty(db);
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/IO/PropertiesTranslationAdapterTest.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/IO/PropertiesTranslationAdapterTest.cs
new file mode 100644
index 000000000..d19c604f0
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/IO/PropertiesTranslationAdapterTest.cs
@@ -0,0 +1,93 @@
+// 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 Microsoft.Extensions.DependencyInjection;
+using PG.StarWarsGame.Localisation.Data;
+using PG.StarWarsGame.Localisation.IO.Properties;
+using PG.StarWarsGame.Localisation.Languages;
+using Xunit;
+
+namespace PG.StarWarsGame.Localisation.Test.IO;
+
+public class PropertiesTranslationAdapterTest : CommonLocalisationTestBase
+{
+ private static readonly IAlamoLanguageDefinition En = new EnglishAlamoLanguageDefinition();
+
+ private IPropertiesTranslationImporter CreateImporter() =>
+ ServiceProvider.GetRequiredService();
+
+ private IPropertiesTranslationExporter CreateExporter() =>
+ ServiceProvider.GetRequiredService();
+
+ [Fact]
+ public void RoundTrip_PreservesKeyValuePairs()
+ {
+ var db = new TranslationDatabaseFactory().CreateKeyed(new[] { En });
+ db.SetTranslation("HELLO", En, "Hello World");
+ db.SetTranslation("BYE", En, "Goodbye");
+
+ var props = CreateExporter().Export(db, En);
+ var db2 = new TranslationDatabaseFactory().CreateKeyed(new[] { En });
+ CreateImporter().Import(new StringReader(props), En, db2);
+
+ Assert.True(db2.TryGetEntry("HELLO", out var hello));
+ Assert.True(hello!.TryGetTranslation(En, out var val));
+ Assert.Equal("Hello World", val);
+ }
+
+ [Fact]
+ public void Export_ProducesKeyEqualsValueFormat()
+ {
+ var db = new TranslationDatabaseFactory().CreateKeyed(new[] { En });
+ db.SetTranslation("KEY", En, "value");
+ var props = CreateExporter().Export(db, En);
+ Assert.Contains("KEY=value", props);
+ }
+
+ [Fact]
+ public void RoundTrip_ValueWithEqualsSign_IsPreserved()
+ {
+ var db = new TranslationDatabaseFactory().CreateKeyed(new[] { En });
+ db.SetTranslation("EQ", En, "a=b");
+ var props = CreateExporter().Export(db, En);
+ var db2 = new TranslationDatabaseFactory().CreateKeyed(new[] { En });
+ CreateImporter().Import(new StringReader(props), En, db2);
+ Assert.True(db2.TryGetEntry("EQ", out var entry));
+ Assert.True(entry!.TryGetTranslation(En, out var val));
+ Assert.Equal("a=b", val);
+ }
+
+ [Fact]
+ public void Import_Empty_LeavesDbEmpty()
+ {
+ var db = new TranslationDatabaseFactory().CreateKeyed(new[] { En });
+ CreateImporter().Import(new StringReader(""), En, db);
+ Assert.Empty(db);
+ }
+
+ [Fact]
+ public void Import_SkipsCommentLines()
+ {
+ const string props = "# This is a comment\nKEY=Value\n";
+ var db = new TranslationDatabaseFactory().CreateKeyed(new[] { En });
+ CreateImporter().Import(new StringReader(props), En, db);
+ Assert.Single(db);
+ }
+
+ [Fact]
+ public void Export_KeyedDatabase_WritesKeysAlphabetically()
+ {
+ var db = new TranslationDatabaseFactory().CreateKeyed(new[] { En });
+ db.SetTranslation("ZEBRA", En, "z");
+ db.SetTranslation("APPLE", En, "a");
+ db.SetTranslation("MONKEY", En, "m");
+
+ var props = CreateExporter().Export(db, En);
+ var lines = props.Split('\n');
+ var dataLines = System.Array.FindAll(lines, l => l.Contains("="));
+ Assert.StartsWith("APPLE=", dataLines[0]);
+ Assert.StartsWith("MONKEY=", dataLines[1]);
+ Assert.StartsWith("ZEBRA=", dataLines[2]);
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/IO/XmlTranslationAdapterTest.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/IO/XmlTranslationAdapterTest.cs
new file mode 100644
index 000000000..50b9fb61c
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/IO/XmlTranslationAdapterTest.cs
@@ -0,0 +1,95 @@
+// 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.Xml.Linq;
+using Microsoft.Extensions.DependencyInjection;
+using PG.StarWarsGame.Localisation.Data;
+using PG.StarWarsGame.Localisation.IO.Xml;
+using System.Linq;
+using PG.StarWarsGame.Localisation.Languages;
+using Xunit;
+
+namespace PG.StarWarsGame.Localisation.Test.IO;
+
+public class XmlTranslationAdapterTest : CommonLocalisationTestBase
+{
+ private static readonly IAlamoLanguageDefinition En = new EnglishAlamoLanguageDefinition();
+ private static readonly IAlamoLanguageDefinition De = new GermanAlamoLanguageDefinition();
+
+ private IXmlTranslationImporter CreateImporter() =>
+ ServiceProvider.GetRequiredService();
+
+ private IXmlTranslationExporter CreateExporter() =>
+ ServiceProvider.GetRequiredService();
+
+ [Fact]
+ public void RoundTrip_MultiLanguage_PreservesAllTranslations()
+ {
+ var db = new TranslationDatabaseFactory().CreateKeyed(new[] { En, De });
+ db.SetTranslation("HELLO", En, "Hello");
+ db.SetTranslation("HELLO", De, "Hallo");
+ db.SetTranslation("BYE", En, "Bye");
+ db.SetTranslation("BYE", De, "Tschüss");
+
+ var xml = CreateExporter().Export(db);
+
+ var db2 = new TranslationDatabaseFactory().CreateKeyed(new[] { En, De });
+ CreateImporter().Import(xml, db2);
+
+ Assert.True(db2.TryGetEntry("HELLO", out var hello));
+ Assert.True(hello!.TryGetTranslation(En, out var en));
+ Assert.True(hello.TryGetTranslation(De, out var de));
+ Assert.Equal("Hello", en);
+ Assert.Equal("Hallo", de);
+ }
+
+ [Fact]
+ public void Export_ProducesValidXml()
+ {
+ var db = new TranslationDatabaseFactory().CreateKeyed(new[] { En });
+ db.SetTranslation("KEY", En, "Value");
+
+ var xml = CreateExporter().Export(db);
+ Assert.NotNull(xml.Root);
+ }
+
+ [Fact]
+ public void Import_EmptyXml_LeavesDbEmpty()
+ {
+ var xml = new XDocument(new XElement(
+ XName.Get("LocalisationData", "urn:alamoenginetools:localisation:v1")));
+ var db = new TranslationDatabaseFactory().CreateKeyed(new[] { En });
+ CreateImporter().Import(xml, db);
+ Assert.Empty(db);
+ }
+
+ [Fact]
+ public void Import_UnknownLanguage_IsSkipped()
+ {
+ var db = new TranslationDatabaseFactory().CreateKeyed(new[] { En });
+ db.SetTranslation("K", En, "v");
+ var xml = CreateExporter().Export(db);
+
+ var db2 = new TranslationDatabaseFactory().CreateKeyed(new[] { De });
+ CreateImporter().Import(xml, db2);
+ Assert.Empty(db2);
+ }
+
+ [Fact]
+ public void Export_KeyedDatabase_WritesKeysAlphabetically()
+ {
+ var db = new TranslationDatabaseFactory().CreateKeyed(new[] { En });
+ db.SetTranslation("ZEBRA", En, "z");
+ db.SetTranslation("APPLE", En, "a");
+ db.SetTranslation("MONKEY", En, "m");
+
+ var xml = CreateExporter().Export(db);
+ XNamespace ns = "urn:alamoenginetools:localisation:v1";
+ var keys = xml.Root!
+ .Elements(ns + "Localisation")
+ .Select(e => e.Attribute("key")!.Value)
+ .ToList();
+
+ Assert.Equal(new[] { "APPLE", "MONKEY", "ZEBRA" }, keys);
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Languages/AbstractAlamoLanguageDefinitionTest.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Languages/AbstractAlamoLanguageDefinitionTest.cs
new file mode 100644
index 000000000..4e2a18a70
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Languages/AbstractAlamoLanguageDefinitionTest.cs
@@ -0,0 +1,106 @@
+// 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;
+using PG.StarWarsGame.Localisation.Languages;
+using PG.StarWarsGame.Localisation.Languages.Attributes;
+using Xunit;
+
+namespace PG.StarWarsGame.Localisation.Test.Languages;
+
+public class AbstractAlamoLanguageDefinitionTest
+{
+ [Fact]
+ public void LanguageIdentifier_ReturnsConfiguredValue()
+ {
+ var lang = new EnglishAlamoLanguageDefinition();
+ Assert.Equal("ENGLISH", lang.LanguageIdentifier);
+ }
+
+ [Fact]
+ public void Culture_ReturnsConfiguredCulture()
+ {
+ var lang = new EnglishAlamoLanguageDefinition();
+ Assert.Equal(CultureInfo.GetCultureInfo("en-US"), lang.Culture);
+ }
+
+ [Fact]
+ public void IsOfficiallySupported_IsTrue_WhenMarkedWithAttribute()
+ {
+ var lang = new EnglishAlamoLanguageDefinition();
+ Assert.True(lang.IsOfficiallySupported());
+ }
+
+ [Fact]
+ public void IsDefault_IsTrue_OnlyForEnglish()
+ {
+ var english = new EnglishAlamoLanguageDefinition();
+ var german = new GermanAlamoLanguageDefinition();
+ Assert.True(english.IsDefault);
+ Assert.False(german.IsDefault);
+ }
+
+ [Fact]
+ public void Equals_ReturnsTrueForSameLanguageIdentifier()
+ {
+ var a = new EnglishAlamoLanguageDefinition();
+ var b = new EnglishAlamoLanguageDefinition();
+ Assert.Equal(a, b);
+ }
+
+ [Fact]
+ public void Equals_ReturnsFalseForDifferentLanguages()
+ {
+ var english = new EnglishAlamoLanguageDefinition();
+ var german = new GermanAlamoLanguageDefinition();
+ Assert.NotEqual(english, german);
+ }
+
+ [Fact]
+ public void IsOfficiallySupported_WithContext_ReturnsTrueForBothContexts_WhenNoContextsSpecified()
+ {
+ var lang = new EnglishAlamoLanguageDefinition();
+ Assert.True(lang.IsOfficiallySupported(GameContext.EaW));
+ Assert.True(lang.IsOfficiallySupported(GameContext.FoC));
+ }
+
+ [Fact]
+ public void IsOfficiallySupported_WithContext_ReturnsFalse_WhenLanguageHasNoAttribute()
+ {
+ var lang = new UnsupportedTestLanguage();
+ Assert.False(lang.IsOfficiallySupported(GameContext.EaW));
+ Assert.False(lang.IsOfficiallySupported(GameContext.FoC));
+ }
+
+ [Fact]
+ public void IsOfficiallySupported_WithContext_ReturnsTrue_OnlyForMatchingContext()
+ {
+ var baseOnly = new BaseGameOnlyTestLanguage();
+ Assert.True(baseOnly.IsOfficiallySupported(GameContext.EaW));
+ Assert.False(baseOnly.IsOfficiallySupported(GameContext.FoC));
+
+ var expOnly = new ExpansionOnlyTestLanguage();
+ Assert.False(expOnly.IsOfficiallySupported(GameContext.EaW));
+ Assert.True(expOnly.IsOfficiallySupported(GameContext.FoC));
+ }
+
+ private sealed class UnsupportedTestLanguage : AlamoLanguageDefinitionBase
+ {
+ protected override string ConfiguredLanguageIdentifier => "KLINGON";
+ protected override CultureInfo ConfiguredCulture => CultureInfo.InvariantCulture;
+ }
+
+ [OfficiallySupportedLanguage(GameContext.EaW)]
+ private sealed class BaseGameOnlyTestLanguage : AlamoLanguageDefinitionBase
+ {
+ protected override string ConfiguredLanguageIdentifier => "BASE_ONLY";
+ protected override CultureInfo ConfiguredCulture => CultureInfo.InvariantCulture;
+ }
+
+ [OfficiallySupportedLanguage(GameContext.FoC)]
+ private sealed class ExpansionOnlyTestLanguage : AlamoLanguageDefinitionBase
+ {
+ protected override string ConfiguredLanguageIdentifier => "EXP_ONLY";
+ protected override CultureInfo ConfiguredCulture => CultureInfo.InvariantCulture;
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Languages/AlamoLanguageDefinitionIntegrityTest.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Languages/AlamoLanguageDefinitionIntegrityTest.cs
index 013e4ed0d..f69bc2ba4 100644
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Languages/AlamoLanguageDefinitionIntegrityTest.cs
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Languages/AlamoLanguageDefinitionIntegrityTest.cs
@@ -1,60 +1,71 @@
// 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 Microsoft.VisualStudio.TestTools.UnitTesting;
-using PG.Core.Attributes;
-using PG.Core.Localisation;
-using PG.Core.Localisation.Attributes;
-using PG.Core.Test;
-using PG.StarWarsGame.Localisation.Util;
-using System.Collections.Generic;
-
-namespace PG.StarWarsGame.Localisation.Test.Languages
+using System.Linq;
+using System.Reflection;
+using PG.StarWarsGame.Localisation.Languages;
+using PG.StarWarsGame.Localisation.Languages.Attributes;
+using Xunit;
+
+namespace PG.StarWarsGame.Localisation.Test.Languages;
+
+public class AlamoLanguageDefinitionIntegrityTest
{
- [TestClass]
- [TestCategory(TestConstants.TEST_TYPE_HOLY)]
- public class AlamoLanguageDefinitionIntegrityTest
+ private static readonly Assembly LocalisationAssembly =
+ typeof(IAlamoLanguageDefinition).Assembly;
+
+ [Fact]
+ public void ExactlyElevenConcreteLanguagesAreDefined()
+ {
+ var count = LocalisationAssembly.GetTypes()
+ .Count(t => t is { IsClass: true, IsAbstract: false }
+ && typeof(IAlamoLanguageDefinition).IsAssignableFrom(t));
+ Assert.Equal(11, count);
+ }
+
+ [Fact]
+ public void ExactlyOneDefaultLanguageIsDefined()
+ {
+ var defaults = LocalisationAssembly.GetTypes()
+ .Where(t => t is { IsClass: true, IsAbstract: false }
+ && typeof(IAlamoLanguageDefinition).IsAssignableFrom(t)
+ && t.IsDefined(typeof(DefaultLanguageAttribute), false))
+ .ToList();
+ Assert.Single(defaults);
+ }
+
+ [Fact]
+ public void DefaultLanguageIsEnglish()
{
- [TestMethod]
- public void Test_CorrectNumberOfLanguages()
- {
- IList l = LocalisationUtility.GetAllAlamoLanguageDefinitions();
- Assert.AreEqual(LocalisationTestConstants.REGISTERED_LANGUAGE_DEFINITIONS.Count, l.Count, "An official language definition has been added or removed. This should never happen - if there is a good reason for this, please update LocalisationTestConstants.REGISTERED_LANGUAGE_DEFINITIONS accordingly.");
- }
-
- [TestMethod]
- public void Test_CorrectLanguagesRegistered()
- {
- IList l = LocalisationUtility.GetAllAlamoLanguageDefinitions();
- foreach (IAlamoLanguageDefinition alamoLanguageDefinition in l)
- {
- Assert.IsTrue(
- LocalisationTestConstants.REGISTERED_LANGUAGE_DEFINITIONS.Contains(
- alamoLanguageDefinition.GetType()));
- Assert.IsTrue(alamoLanguageDefinition.GetType().GetAttributeValueOrDefault((OfficiallySupportedLanguageAttribute o) => o.IsOfficiallySupported));
- }
- }
-
- [TestMethod]
- public void Test_DefaultLanguageIsDefined()
- {
- IList l = LocalisationUtility.GetAllAlamoLanguageDefinitions();
- bool isDefaultDefined = false;
- foreach (IAlamoLanguageDefinition alamoLanguageDefinition in l)
- {
- if (alamoLanguageDefinition.GetType().GetAttributeValueOrDefault((DefaultAttribute d) => d.IsDefault))
- {
- isDefaultDefined = true;
- }
- }
- Assert.IsTrue(isDefaultDefined, "No default language is defined. This should not happen. EnglishAlamoLanguageDefinition should have the Default attribute set.");
- }
-
- [TestMethod]
- public void Test_DefaultLanguageIsCorrect()
- {
- IAlamoLanguageDefinition l = LocalisationUtility.GetDefaultAlamoLanguageDefinition();
- Assert.AreEqual(LocalisationTestConstants.DEFAULT_LANGUAGE, l.GetType(), "The default language is not EnglishAlamoLanguageDefinition. This should never happen. Please ensure EnglishAlamoLanguageDefinition has the Default attribute set.");
- }
+ var defaultType = LocalisationAssembly.GetTypes()
+ .Single(t => t is { IsClass: true, IsAbstract: false }
+ && typeof(IAlamoLanguageDefinition).IsAssignableFrom(t)
+ && t.IsDefined(typeof(DefaultLanguageAttribute), false));
+ var instance = (IAlamoLanguageDefinition)System.Activator.CreateInstance(defaultType)!;
+ Assert.Equal("ENGLISH", instance.LanguageIdentifier);
+ }
+
+ [Fact]
+ public void AllConcreteLanguagesAreOfficiallySupported()
+ {
+ var langs = LocalisationAssembly.GetTypes()
+ .Where(t => t is { IsClass: true, IsAbstract: false }
+ && typeof(IAlamoLanguageDefinition).IsAssignableFrom(t))
+ .Select(t => (IAlamoLanguageDefinition)System.Activator.CreateInstance(t)!)
+ .ToList();
+
+ Assert.All(langs, l => Assert.True(l.IsOfficiallySupported()));
+ }
+
+ [Fact]
+ public void AllConcreteLanguagesHaveUniqueIdentifiers()
+ {
+ var identifiers = LocalisationAssembly.GetTypes()
+ .Where(t => t is { IsClass: true, IsAbstract: false }
+ && typeof(IAlamoLanguageDefinition).IsAssignableFrom(t))
+ .Select(t => ((IAlamoLanguageDefinition)System.Activator.CreateInstance(t)!).LanguageIdentifier)
+ .ToList();
+
+ Assert.Equal(identifiers.Count, identifiers.Distinct().Count());
}
}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/LocalisationServiceContributionTest.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/LocalisationServiceContributionTest.cs
new file mode 100644
index 000000000..1a38e3936
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/LocalisationServiceContributionTest.cs
@@ -0,0 +1,101 @@
+// 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 Microsoft.Extensions.DependencyInjection;
+using PG.StarWarsGame.Localisation.Data;
+using PG.StarWarsGame.Localisation.IO.Csv;
+using PG.StarWarsGame.Localisation.IO.Dat;
+using PG.StarWarsGame.Localisation.IO.Properties;
+using PG.StarWarsGame.Localisation.IO.Xml;
+using PG.StarWarsGame.Localisation.Services;
+using Xunit;
+
+namespace PG.StarWarsGame.Localisation.Test;
+
+public class LocalisationServiceContributionTest
+{
+ private static ServiceProvider BuildProvider()
+ {
+ var sc = new ServiceCollection();
+ sc.SupportLocalisation();
+ return sc.BuildServiceProvider();
+ }
+
+ [Fact]
+ public void SupportLocalisation_Resolves_ILanguageService()
+ {
+ using var sp = BuildProvider();
+ Assert.NotNull(sp.GetRequiredService());
+ }
+
+ [Fact]
+ public void SupportLocalisation_Resolves_ITranslationDatabaseFactory()
+ {
+ using var sp = BuildProvider();
+ Assert.NotNull(sp.GetRequiredService());
+ }
+
+ [Fact]
+ public void SupportLocalisation_Resolves_IDatTranslationImporter()
+ {
+ using var sp = BuildProvider();
+ Assert.NotNull(sp.GetRequiredService());
+ }
+
+ [Fact]
+ public void SupportLocalisation_Resolves_IDatTranslationExporter()
+ {
+ using var sp = BuildProvider();
+ Assert.NotNull(sp.GetRequiredService());
+ }
+
+ [Fact]
+ public void SupportLocalisation_Resolves_IXmlTranslationImporter()
+ {
+ using var sp = BuildProvider();
+ Assert.NotNull(sp.GetRequiredService());
+ }
+
+ [Fact]
+ public void SupportLocalisation_Resolves_IXmlTranslationExporter()
+ {
+ using var sp = BuildProvider();
+ Assert.NotNull(sp.GetRequiredService());
+ }
+
+ [Fact]
+ public void SupportLocalisation_Resolves_ICsvTranslationImporter()
+ {
+ using var sp = BuildProvider();
+ Assert.NotNull(sp.GetRequiredService());
+ }
+
+ [Fact]
+ public void SupportLocalisation_Resolves_ICsvTranslationExporter()
+ {
+ using var sp = BuildProvider();
+ Assert.NotNull(sp.GetRequiredService());
+ }
+
+ [Fact]
+ public void SupportLocalisation_Resolves_IPropertiesTranslationImporter()
+ {
+ using var sp = BuildProvider();
+ Assert.NotNull(sp.GetRequiredService());
+ }
+
+ [Fact]
+ public void SupportLocalisation_Resolves_IPropertiesTranslationExporter()
+ {
+ using var sp = BuildProvider();
+ Assert.NotNull(sp.GetRequiredService());
+ }
+
+ [Fact]
+ public void SupportLocalisation_LanguageService_HasElevenLanguages()
+ {
+ using var sp = BuildProvider();
+ var svc = sp.GetRequiredService();
+ Assert.Equal(11, svc.AllLanguages.Count);
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/LocalisationTestConstants.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/LocalisationTestConstants.cs
deleted file mode 100644
index a4eb8d04c..000000000
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/LocalisationTestConstants.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-// 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.Core.Test;
-using PG.StarWarsGame.Localisation.Languages;
-using System;
-using System.Collections.Generic;
-
-namespace PG.StarWarsGame.Localisation.Test
-{
- public sealed class LocalisationTestConstants : TestConstants
- {
- public static readonly IList REGISTERED_LANGUAGE_DEFINITIONS = 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 DEFAULT_LANGUAGE = typeof(EnglishAlamoLanguageDefinition);
- }
-}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/PG.StarWarsGame.Localisation.Test.csproj b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/PG.StarWarsGame.Localisation.Test.csproj
index b210c6bfb..7455d587b 100644
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/PG.StarWarsGame.Localisation.Test.csproj
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/PG.StarWarsGame.Localisation.Test.csproj
@@ -1,30 +1,34 @@
-
+
+
+ net8.0;net10.0
+ $(TargetFrameworks);net481
+
false
- net8.0
- $(TargetFrameworks);net48
+ true
+ Exe
-
+
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
- all
+
runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
-
-
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
-
\ No newline at end of file
+
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Services/LanguageServiceTest.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Services/LanguageServiceTest.cs
new file mode 100644
index 000000000..cb20bdd6a
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Services/LanguageServiceTest.cs
@@ -0,0 +1,166 @@
+// 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 PG.StarWarsGame.Localisation.Languages;
+using PG.StarWarsGame.Localisation.Languages.Attributes;
+using PG.StarWarsGame.Localisation.Services;
+using Xunit;
+
+namespace PG.StarWarsGame.Localisation.Test.Services;
+
+public class LanguageServiceTest
+{
+ private static ILanguageService CreateService(IEnumerable? langs = null)
+ {
+ var all = langs ?? new IAlamoLanguageDefinition[]
+ {
+ new EnglishAlamoLanguageDefinition(),
+ new GermanAlamoLanguageDefinition(),
+ new FrenchAlamoLanguageDefinition(),
+ new SpanishAlamoLanguageDefinition(),
+ new ItalianAlamoLanguageDefinition(),
+ new ChineseAlamoLanguageDefinition(),
+ new JapaneseAlamoLanguageDefinition(),
+ new KoreanAlamoLanguageDefinition(),
+ new PolishAlamoLanguageDefinition(),
+ new RussianAlamoLanguageDefinition(),
+ new ThaiAlamoLanguageDefinition(),
+ };
+ return new LanguageService(all);
+ }
+
+ [Fact]
+ public void AllLanguages_ReturnsAllRegistered()
+ {
+ var svc = CreateService();
+ Assert.Equal(11, svc.AllLanguages.Count);
+ }
+
+ [Fact]
+ public void OfficiallySupported_ReturnsOnlySupported()
+ {
+ var svc = CreateService();
+ Assert.All(svc.OfficiallySupported(), l => Assert.True(l.IsOfficiallySupported()));
+ }
+
+ [Fact]
+ public void Default_ReturnsEnglish()
+ {
+ var svc = CreateService();
+ Assert.Equal("ENGLISH", svc.Default.LanguageIdentifier);
+ }
+
+ [Fact]
+ public void Default_Throws_WhenNoDefaultRegistered()
+ {
+ var svc = new LanguageService(new[] { new GermanAlamoLanguageDefinition() });
+ Assert.Throws(() => _ = svc.Default);
+ }
+
+ [Fact]
+ public void TryGetByIdentifier_ReturnsTrue_OnExactMatch()
+ {
+ var svc = CreateService();
+ Assert.True(svc.TryGetByIdentifier("ENGLISH", out var lang));
+ Assert.NotNull(lang);
+ Assert.Equal("ENGLISH", lang!.LanguageIdentifier);
+ }
+
+ [Fact]
+ public void TryGetByIdentifier_IsCaseInsensitive()
+ {
+ var svc = CreateService();
+ Assert.True(svc.TryGetByIdentifier("english", out var lang));
+ Assert.NotNull(lang);
+ }
+
+ [Fact]
+ public void TryGetByIdentifier_ReturnsFalse_WhenNotFound()
+ {
+ var svc = CreateService();
+ Assert.False(svc.TryGetByIdentifier("KLINGON", out var lang));
+ Assert.Null(lang);
+ }
+
+ [Fact]
+ public void IsOfficiallySupported_ReturnsTrue_ForSupportedLanguage()
+ {
+ var svc = CreateService();
+ Assert.True(svc.IsOfficiallySupported(new EnglishAlamoLanguageDefinition()));
+ }
+
+ [Fact]
+ public void IsOfficiallySupported_ReturnsFalse_ForUnsupportedLanguage()
+ {
+ var svc = CreateService();
+ var unsupported = new UnsupportedTestLanguage();
+ Assert.False(svc.IsOfficiallySupported(unsupported));
+ }
+
+ [Fact]
+ public void IsOfficiallySupported_Throws_OnNull()
+ {
+ var svc = CreateService();
+ Assert.Throws(() => svc.IsOfficiallySupported(null!));
+ }
+
+ [Fact]
+ public void OfficiallySupported_WithContext_ReturnsOnlyContextMatchingLanguages()
+ {
+ var baseOnly = new BaseGameOnlyTestLanguage();
+ var expOnly = new ExpansionOnlyTestLanguage();
+ var both = new EnglishAlamoLanguageDefinition();
+ var svc = new LanguageService(new IAlamoLanguageDefinition[] { baseOnly, expOnly, both });
+
+ var baseResult = svc.OfficiallySupported(GameContext.EaW);
+ var expResult = svc.OfficiallySupported(GameContext.FoC);
+
+ Assert.Contains(baseOnly, baseResult);
+ Assert.Contains(both, baseResult);
+ Assert.DoesNotContain(expOnly, baseResult);
+
+ Assert.Contains(expOnly, expResult);
+ Assert.Contains(both, expResult);
+ Assert.DoesNotContain(baseOnly, expResult);
+ }
+
+ [Fact]
+ public void IsOfficiallySupported_WithContext_ReturnsCorrectResultPerContext()
+ {
+ var baseOnly = new BaseGameOnlyTestLanguage();
+ var svc = new LanguageService(new IAlamoLanguageDefinition[] { baseOnly, new EnglishAlamoLanguageDefinition() });
+
+ Assert.True(svc.IsOfficiallySupported(baseOnly, GameContext.EaW));
+ Assert.False(svc.IsOfficiallySupported(baseOnly, GameContext.FoC));
+ }
+
+ [Fact]
+ public void IsOfficiallySupported_WithContext_Throws_OnNull()
+ {
+ var svc = CreateService();
+ Assert.Throws(() => svc.IsOfficiallySupported(null!, GameContext.EaW));
+ }
+
+ private sealed class UnsupportedTestLanguage : AlamoLanguageDefinitionBase
+ {
+ protected override string ConfiguredLanguageIdentifier => "KLINGON";
+ protected override CultureInfo ConfiguredCulture => CultureInfo.InvariantCulture;
+ }
+
+ [OfficiallySupportedLanguage(GameContext.EaW)]
+ private sealed class BaseGameOnlyTestLanguage : AlamoLanguageDefinitionBase
+ {
+ protected override string ConfiguredLanguageIdentifier => "BASE_ONLY";
+ protected override CultureInfo ConfiguredCulture => CultureInfo.InvariantCulture;
+ }
+
+ [OfficiallySupportedLanguage(GameContext.FoC)]
+ private sealed class ExpansionOnlyTestLanguage : AlamoLanguageDefinitionBase
+ {
+ protected override string ConfiguredLanguageIdentifier => "EXP_ONLY";
+ protected override CultureInfo ConfiguredCulture => CultureInfo.InvariantCulture;
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Util/LocalisationUtilityTest.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Util/LocalisationUtilityTest.cs
deleted file mode 100644
index 412352319..000000000
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.Test/Util/LocalisationUtilityTest.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-// 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 Microsoft.VisualStudio.TestTools.UnitTesting;
-using PG.Core.Attributes;
-using PG.Core.Localisation;
-using PG.Core.Localisation.Attributes;
-using PG.Core.Test;
-using PG.StarWarsGame.Localisation.Util;
-using System;
-using System.Collections.Generic;
-
-namespace PG.StarWarsGame.Localisation.Test.Util
-{
- [TestClass]
- [TestCategory(TestConstants.TEST_TYPE_UTILITY)]
- public class LocalisationUtilityTest
- {
- [TestMethod]
- public void Test_GetAllAlamoLanguageDefinitions__ReturnsAtLeastDefaults()
- {
- IList l = LocalisationUtility.GetAllAlamoLanguageDefinitions();
- Assert.IsTrue(l.Count >= LocalisationTestConstants.REGISTERED_LANGUAGE_DEFINITIONS.Count,
- "This function should always at least return the default language definitions.");
- int actualNumberOfLanguageDefinitionsMarkedAsOfficial = 0;
- int actualDefaultLanguageCount = 0;
- foreach (IAlamoLanguageDefinition alamoLanguageDefinition in l)
- {
- if (alamoLanguageDefinition.GetType()
- .GetAttributeValueOrDefault((OfficiallySupportedLanguageAttribute o) => o.IsOfficiallySupported))
- {
- actualNumberOfLanguageDefinitionsMarkedAsOfficial++;
- }
-
- if (alamoLanguageDefinition.GetType()
- .GetAttributeValueOrDefault((DefaultAttribute d) => d.IsDefault))
- {
- actualDefaultLanguageCount++;
- }
- }
- Assert.AreEqual(LocalisationTestConstants.REGISTERED_LANGUAGE_DEFINITIONS.Count, actualNumberOfLanguageDefinitionsMarkedAsOfficial,
- "Someone added a language definition marked as official. This should not happen. Please remove the OfficiallySupportedLanguageAttribute from the offending language.");
- Assert.AreEqual(1, actualDefaultLanguageCount,
- "Someone added a language definition marked as default. This should not happen. Please remove the DefaultAttribute from the offending language.");
-
- }
-
- [TestMethod]
- public void Test_GetDefaultAlamoLanguageDefinition__ReturnsCorrectLanguage()
- {
- IAlamoLanguageDefinition l = LocalisationUtility.GetDefaultAlamoLanguageDefinition();
- Assert.AreEqual(LocalisationTestConstants.DEFAULT_LANGUAGE, l.GetType());
- IAlamoLanguageDefinition actual = Activator.CreateInstance(LocalisationTestConstants.DEFAULT_LANGUAGE) as IAlamoLanguageDefinition;
- Assert.AreEqual(actual, l);
- }
- }
-}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Commons/DefaultAlamoLanguageDefinitionException.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Commons/DefaultAlamoLanguageDefinitionException.cs
deleted file mode 100644
index 79140dd13..000000000
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Commons/DefaultAlamoLanguageDefinitionException.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-// 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.Runtime.Serialization;
-
-namespace PG.StarWarsGame.Localisation.Commons
-{
- ///
- [Serializable]
- public sealed class DefaultAlamoLanguageDefinitionException : Exception
- {
- public DefaultAlamoLanguageDefinitionException()
- {
- }
-
- public DefaultAlamoLanguageDefinitionException(string message) : base(message)
- {
- }
-
- public DefaultAlamoLanguageDefinitionException(string message, Exception innerException) : base(message,
- innerException)
- {
- }
-
- public DefaultAlamoLanguageDefinitionException(SerializationInfo info, StreamingContext context) : base(info,
- context)
- {
- }
- }
-}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/TranslationProjectDescriptor.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/TranslationProjectDescriptor.cs
new file mode 100644
index 000000000..1de8dd0d2
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/TranslationProjectDescriptor.cs
@@ -0,0 +1,73 @@
+// 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.Collections.ObjectModel;
+using System.Linq;
+using PG.StarWarsGame.Localisation.Data.Config.v2;
+using PG.StarWarsGame.Localisation.Languages;
+
+namespace PG.StarWarsGame.Localisation.Data.Config
+{
+ ///
+ /// Immutable descriptor for a translation project.
+ ///
+ public sealed class TranslationProjectDescriptor : IEquatable
+ {
+ private readonly HashSet _languagesSet;
+
+ /// Gets the game this project provides translations for.
+ public GameContext Game { get; }
+
+ /// Gets whether this project provides core, expansion, or mod text.
+ public OverrideType OverrideType { get; }
+
+ /// Gets the file format used to store translation resources.
+ public TranslationResourceType ResourceType { get; }
+
+ /// Gets the languages covered by this translation project.
+ public IReadOnlyCollection Languages { get; }
+
+ /// Initialises a new .
+ public TranslationProjectDescriptor(
+ GameContext game,
+ OverrideType overrideType,
+ TranslationResourceType resourceType,
+ IEnumerable languages)
+ {
+ if (languages is null)
+ throw new ArgumentNullException(nameof(languages));
+
+ _languagesSet = new HashSet(languages);
+ Languages = new ReadOnlyCollection(_languagesSet.ToList());
+ Game = game;
+ OverrideType = overrideType;
+ ResourceType = resourceType;
+ }
+
+ ///
+ public bool Equals(TranslationProjectDescriptor? other)
+ {
+ if (other is null) return false;
+ if (ReferenceEquals(this, other)) return true;
+ return Game == other.Game
+ && OverrideType == other.OverrideType
+ && ResourceType == other.ResourceType
+ && _languagesSet.SetEquals(other._languagesSet);
+ }
+
+ ///
+ public override bool Equals(object? obj) =>
+ obj is TranslationProjectDescriptor other && Equals(other);
+
+ ///
+ public override int GetHashCode()
+ {
+ var hash = HashCode.Combine((int)Game, (int)OverrideType, (int)ResourceType);
+ foreach (var lang in _languagesSet.OrderBy(l => l.LanguageIdentifier, StringComparer.Ordinal))
+ hash = HashCode.Combine(hash, lang.GetHashCode());
+ return hash;
+ }
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v1/LocalisationDataType.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v1/LocalisationDataType.cs
index 79f86b4f1..9fe37d107 100644
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v1/LocalisationDataType.cs
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v1/LocalisationDataType.cs
@@ -1,22 +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;
+using System.CodeDom.Compiler;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Xml.Serialization;
+
namespace PG.StarWarsGame.Localisation.Data.Config.v1
{
///
- [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
- [System.SerializableAttribute()]
- [System.Diagnostics.DebuggerStepThroughAttribute()]
- [System.ComponentModel.DesignerCategoryAttribute("code")]
- [System.Xml.Serialization.XmlTypeAttribute(Namespace = "http://www.example.org/eaw-translation/")]
- [System.Xml.Serialization.XmlRootAttribute("LocalisationData", Namespace = "http://www.example.org/eaw-translation/", IsNullable = false)]
- public class LocalisationDataType : object, System.ComponentModel.INotifyPropertyChanged
+ [ExcludeFromCodeCoverage]
+ [GeneratedCode("xsd", "4.8.3928.0")]
+ [Serializable]
+ [DebuggerStepThrough]
+ [DesignerCategory("code")]
+ [XmlType(Namespace = "urn:alamoenginetools:localisation:v1")]
+ [XmlRoot("LocalisationData", Namespace = "urn:alamoenginetools:localisation:v1", IsNullable = false)]
+ public class LocalisationDataType : object, INotifyPropertyChanged
{
- private LocalisationType[] m_localisationField;
+ private LocalisationType[]? m_localisationField;
///
- [System.Xml.Serialization.XmlElementAttribute("Localisation", Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
- public LocalisationType[] Localisation
+ [XmlElement("Localisation", Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
+ public LocalisationType[]? Localisation
{
get => m_localisationField;
set
@@ -26,12 +34,12 @@ public LocalisationType[] Localisation
}
}
- public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
+ ///
+ public event PropertyChangedEventHandler? PropertyChanged;
- protected void RaisePropertyChanged(string propertyName)
+ private void RaisePropertyChanged(string propertyName)
{
- System.ComponentModel.PropertyChangedEventHandler propertyChanged = PropertyChanged;
- propertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v1/LocalisationType.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v1/LocalisationType.cs
index 3d23a9572..f8d203bb7 100644
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v1/LocalisationType.cs
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v1/LocalisationType.cs
@@ -1,24 +1,31 @@
// 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.CodeDom.Compiler;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Xml.Serialization;
+
namespace PG.StarWarsGame.Localisation.Data.Config.v1
{
///
- [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
- [System.SerializableAttribute()]
- [System.Diagnostics.DebuggerStepThroughAttribute()]
- [System.ComponentModel.DesignerCategoryAttribute("code")]
- [System.Xml.Serialization.XmlTypeAttribute(Namespace = "http://www.example.org/eaw-translation/")]
- public class LocalisationType : object, System.ComponentModel.INotifyPropertyChanged
+ [ExcludeFromCodeCoverage]
+ [GeneratedCode("xsd", "4.8.3928.0")]
+ [Serializable]
+ [DebuggerStepThrough]
+ [DesignerCategory("code")]
+ [XmlType(Namespace = "urn:alamoenginetools:localisation:v1")]
+ public class LocalisationType : object, INotifyPropertyChanged
{
- private TranslationType[] m_translationDataField;
-
- private string m_keyField;
+ private TranslationType[]? m_translationDataField;
+ private string? m_keyField;
///
- [System.Xml.Serialization.XmlArrayAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
- [System.Xml.Serialization.XmlArrayItemAttribute("Translation", Form = System.Xml.Schema.XmlSchemaForm.Unqualified, IsNullable = false)]
- public TranslationType[] TranslationData
+ [XmlArray(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
+ [XmlArrayItem("Translation", Form = System.Xml.Schema.XmlSchemaForm.Unqualified, IsNullable = false)]
+ public TranslationType[]? TranslationData
{
get => m_translationDataField;
set
@@ -29,8 +36,8 @@ public TranslationType[] TranslationData
}
///
- [System.Xml.Serialization.XmlAttributeAttribute()]
- public string Key
+ [XmlAttribute]
+ public string? Key
{
get => m_keyField;
set
@@ -40,12 +47,12 @@ public string Key
}
}
- public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
+ ///
+ public event PropertyChangedEventHandler? PropertyChanged;
- protected void RaisePropertyChanged(string propertyName)
+ private void RaisePropertyChanged(string propertyName)
{
- System.ComponentModel.PropertyChangedEventHandler propertyChanged = PropertyChanged;
- propertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v1/TranslationType.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v1/TranslationType.cs
index 2284d564d..576192ce5 100644
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v1/TranslationType.cs
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v1/TranslationType.cs
@@ -16,15 +16,15 @@ namespace PG.StarWarsGame.Localisation.Data.Config.v1
[Serializable]
[DebuggerStepThrough]
[DesignerCategory("code")]
- [XmlType(Namespace = "http://www.example.org/eaw-translation/")]
+ [XmlType(Namespace = "urn:alamoenginetools:localisation:v1")]
public class TranslationType : object, INotifyPropertyChanged
{
- private string m_languageField;
- private string m_valueField;
+ private string? m_languageField;
+ private string? m_valueField;
///
[XmlAttribute]
- public string Language
+ public string? Language
{
get => m_languageField;
set
@@ -36,7 +36,7 @@ public string Language
///
[XmlText]
- public string Value
+ public string? Value
{
get => m_valueField;
set
@@ -46,12 +46,12 @@ public string Value
}
}
- public event PropertyChangedEventHandler PropertyChanged;
+ ///
+ public event PropertyChangedEventHandler? PropertyChanged;
- protected void RaisePropertyChanged(string propertyName)
+ private void RaisePropertyChanged(string propertyName)
{
- PropertyChangedEventHandler propertyChanged = PropertyChanged;
- propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v2/GameType.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v2/GameType.cs
deleted file mode 100644
index 9faa4f68b..000000000
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v2/GameType.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.StarWarsGame.Localisation.Data.Config.v2
-{
- public enum GameType
- {
- EaW,
- FoC,
- Mod
- }
-}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v2/OverrideType.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v2/OverrideType.cs
index 7894872fc..af76ae788 100644
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v2/OverrideType.cs
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v2/OverrideType.cs
@@ -3,10 +3,16 @@
namespace PG.StarWarsGame.Localisation.Data.Config.v2
{
+ ///
+ /// Specifies whether a translation project provides core game text, expansion text, or mod text.
+ ///
public enum OverrideType
{
+ /// Core base-game translation resources.
Core,
+ /// Expansion translation resources.
Expansion,
+ /// Mod-specific translation resources.
Mod
}
}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v2/TranslationResourceType.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v2/TranslationResourceType.cs
index d107cc13a..6b0aa14a9 100644
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v2/TranslationResourceType.cs
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Config/v2/TranslationResourceType.cs
@@ -3,9 +3,18 @@
namespace PG.StarWarsGame.Localisation.Data.Config.v2
{
+ ///
+ /// Specifies the file format used to store translation resources.
+ ///
public enum TranslationResourceType
{
+ /// Binary Petroglyph DAT format.
+ Dat,
+ /// XML format conforming to the eaw-translation schema.
+ Xml,
+ /// CSV format with a header row of language identifiers.
Csv,
+ /// Java-style .properties format (one file per language).
Nls
}
}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/IKeyedTranslationDatabase.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/IKeyedTranslationDatabase.cs
new file mode 100644
index 000000000..9c5889b11
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/IKeyedTranslationDatabase.cs
@@ -0,0 +1,28 @@
+// 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;
+
+namespace PG.StarWarsGame.Localisation.Data
+{
+ ///
+ /// A translation database where keys are unique (MasterText / mastertextfile semantics).
+ /// upserts by key.
+ ///
+ public interface IKeyedTranslationDatabase : ITranslationDatabase
+ {
+ /// Returns if an entry with exists.
+ bool ContainsKey(string key);
+
+ /// Attempts to retrieve the entry for .
+ bool TryGetEntry(string key, [NotNullWhen(true)] out TranslationEntry? entry);
+
+ ///
+ /// Attempts to retrieve the translation for using .
+ ///
+ ///
+ /// is .
+ ///
+ bool TryGetTranslation(string key, [MaybeNullWhen(false)] out string? value);
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/IOrderedTranslationDatabase.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/IOrderedTranslationDatabase.cs
new file mode 100644
index 000000000..2cf806024
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/IOrderedTranslationDatabase.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.Collections.Generic;
+using PG.StarWarsGame.Localisation.Languages;
+
+namespace PG.StarWarsGame.Localisation.Data
+{
+ ///
+ /// A translation database where insertion order is preserved and duplicate keys are allowed
+ /// (Credits / creditstext semantics).
+ /// always appends a new entry.
+ ///
+ public interface IOrderedTranslationDatabase : ITranslationDatabase
+ {
+ ///
+ /// Inserts a new entry at with an initial translation.
+ ///
+ void InsertAt(int index, string key, IAlamoLanguageDefinition language, string value);
+
+ /// Returns all entries whose key equals .
+ IReadOnlyList GetAllEntriesForKey(string key);
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/ITranslationDatabase.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/ITranslationDatabase.cs
new file mode 100644
index 000000000..8c5e78b90
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/ITranslationDatabase.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.Generic;
+using PG.StarWarsGame.Localisation.Languages;
+
+namespace PG.StarWarsGame.Localisation.Data
+{
+ ///
+ /// An in-memory store of objects for one or more languages.
+ /// Changes are fully decoupled from any file format.
+ ///
+ public interface ITranslationDatabase : IReadOnlyList
+ {
+ /// Gets the languages tracked by this database.
+ IReadOnlyList Languages { get; }
+
+ ///
+ /// Adds or updates the translation value for in .
+ ///
+ /// if a new entry was created; if an existing entry was updated.
+ bool SetTranslation(string key, IAlamoLanguageDefinition language, string value);
+
+ /// Removes the entry identified by .
+ /// if an entry was removed; otherwise .
+ bool RemoveEntry(string key);
+
+ /// Removes all entries from the database.
+ void Clear();
+
+ ///
+ /// Gets or sets the language used by single-language convenience operations.
+ /// Must be one of , or to clear.
+ ///
+ ///
+ /// Set to a language not present in .
+ ///
+ IAlamoLanguageDefinition? ActiveLanguage { get; set; }
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/ITranslationDatabaseBuilder.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/ITranslationDatabaseBuilder.cs
new file mode 100644
index 000000000..777bb2ee8
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/ITranslationDatabaseBuilder.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 PG.StarWarsGame.Localisation.Languages;
+
+namespace PG.StarWarsGame.Localisation.Data
+{
+ ///
+ /// A fluent builder for constructing instances.
+ ///
+ public interface ITranslationDatabaseBuilder
+ {
+ /// Adds a single language to the database.
+ /// is .
+ ITranslationDatabaseBuilder WithLanguage(IAlamoLanguageDefinition language);
+
+ /// Adds multiple languages to the database.
+ /// is .
+ ITranslationDatabaseBuilder WithLanguages(IEnumerable languages);
+
+ ///
+ /// Sets the active language for the database without adding it to the language list.
+ /// Validation that the language is present in the accumulated language list occurs at build time.
+ ///
+ ITranslationDatabaseBuilder SetActiveLanguage(IAlamoLanguageDefinition language);
+
+ ///
+ /// Builds a keyed translation database (unique keys, MasterText semantics).
+ ///
+ ///
+ /// The active language (if set) is not in the accumulated language list.
+ ///
+ IKeyedTranslationDatabase BuildKeyed();
+
+ ///
+ /// Builds an ordered translation database (duplicate keys allowed, Credits semantics).
+ ///
+ ///
+ /// The active language (if set) is not in the accumulated language list.
+ ///
+ IOrderedTranslationDatabase BuildOrdered();
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/ITranslationDatabaseFactory.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/ITranslationDatabaseFactory.cs
new file mode 100644
index 000000000..29e17acc5
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/ITranslationDatabaseFactory.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 PG.StarWarsGame.Localisation.Data.Config;
+using PG.StarWarsGame.Localisation.Languages;
+
+namespace PG.StarWarsGame.Localisation.Data
+{
+ ///
+ /// Creates empty translation databases for a given set of languages.
+ ///
+ public interface ITranslationDatabaseFactory
+ {
+ /// Creates a keyed database (unique keys, MasterText semantics).
+ IKeyedTranslationDatabase CreateKeyed(IReadOnlyList languages);
+
+ /// Creates a keyed database using the languages from .
+ IKeyedTranslationDatabase CreateKeyed(TranslationProjectDescriptor descriptor);
+
+ /// Creates an ordered database (duplicate keys allowed, Credits semantics).
+ IOrderedTranslationDatabase CreateOrdered(IReadOnlyList languages);
+
+ /// Creates an ordered database using the languages from .
+ IOrderedTranslationDatabase CreateOrdered(TranslationProjectDescriptor descriptor);
+
+ /// Returns a fluent builder for constructing a translation database.
+ ITranslationDatabaseBuilder CreateDatabase();
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Internal/KeyedTranslationDatabase.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Internal/KeyedTranslationDatabase.cs
new file mode 100644
index 000000000..a6b78b5b2
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Internal/KeyedTranslationDatabase.cs
@@ -0,0 +1,67 @@
+// 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.Linq;
+using PG.StarWarsGame.Localisation.Languages;
+
+namespace PG.StarWarsGame.Localisation.Data.Internal
+{
+ internal sealed class KeyedTranslationDatabase : TranslationDatabaseBase, IKeyedTranslationDatabase
+ {
+ private readonly Dictionary _entries = new(StringComparer.Ordinal);
+
+ public override int Count => _entries.Count;
+
+ public override TranslationEntry this[int index] => _entries.Values.ElementAt(index);
+
+ internal KeyedTranslationDatabase(IReadOnlyList languages)
+ : base(languages) { }
+
+ public override bool SetTranslation(string key, IAlamoLanguageDefinition language, string value)
+ {
+ if (key is null) throw new ArgumentNullException(nameof(key));
+ if (language is null) throw new ArgumentNullException(nameof(language));
+
+ if (_entries.TryGetValue(key, out var existing))
+ {
+ existing.SetTranslation(language, value);
+ return false;
+ }
+
+ var entry = new TranslationEntry(key);
+ entry.SetTranslation(language, value);
+ _entries[key] = entry;
+ return true;
+ }
+
+ public override bool RemoveEntry(string key) => _entries.Remove(key);
+
+ public override void Clear() => _entries.Clear();
+
+ public bool ContainsKey(string key) => _entries.ContainsKey(key);
+
+ public bool TryGetEntry(string key, [NotNullWhen(true)] out TranslationEntry? entry)
+ {
+ return _entries.TryGetValue(key, out entry);
+ }
+
+ public bool TryGetTranslation(string key, [MaybeNullWhen(false)] out string? value)
+ {
+ if (ActiveLanguage is null)
+ throw new InvalidOperationException(
+ "ActiveLanguage must be set before calling TryGetTranslation(string, out string?).");
+ if (!TryGetEntry(key, out var entry))
+ {
+ value = null;
+ return false;
+ }
+ return entry.TryGetTranslation(ActiveLanguage, out value);
+ }
+
+ public override IEnumerator GetEnumerator() =>
+ _entries.Values.GetEnumerator();
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Internal/OrderedTranslationDatabase.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Internal/OrderedTranslationDatabase.cs
new file mode 100644
index 000000000..a07f6655e
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Internal/OrderedTranslationDatabase.cs
@@ -0,0 +1,59 @@
+// 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 PG.StarWarsGame.Localisation.Languages;
+
+namespace PG.StarWarsGame.Localisation.Data.Internal
+{
+ internal sealed class OrderedTranslationDatabase : TranslationDatabaseBase, IOrderedTranslationDatabase
+ {
+ private readonly List _entries = new();
+
+ public override int Count => _entries.Count;
+
+ public override TranslationEntry this[int index] => _entries[index];
+
+ internal OrderedTranslationDatabase(IReadOnlyList languages)
+ : base(languages) { }
+
+ public override bool SetTranslation(string key, IAlamoLanguageDefinition language, string value)
+ {
+ if (key is null) throw new ArgumentNullException(nameof(key));
+ if (language is null) throw new ArgumentNullException(nameof(language));
+
+ var entry = new TranslationEntry(key);
+ entry.SetTranslation(language, value);
+ _entries.Add(entry);
+ return true;
+ }
+
+ public override bool RemoveEntry(string key)
+ {
+ var idx = _entries.FindIndex(e => e.Key == key);
+ if (idx < 0) return false;
+ _entries.RemoveAt(idx);
+ return true;
+ }
+
+ public override void Clear() => _entries.Clear();
+
+ public void InsertAt(int index, string key, IAlamoLanguageDefinition language, string value)
+ {
+ if (key is null) throw new ArgumentNullException(nameof(key));
+ if (language is null) throw new ArgumentNullException(nameof(language));
+
+ var entry = new TranslationEntry(key);
+ entry.SetTranslation(language, value);
+ _entries.Insert(index, entry);
+ }
+
+ public IReadOnlyList GetAllEntriesForKey(string key) =>
+ _entries.Where(e => e.Key == key).ToList().AsReadOnly();
+
+ public override IEnumerator GetEnumerator() =>
+ _entries.GetEnumerator();
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Internal/TranslationDatabaseBase.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Internal/TranslationDatabaseBase.cs
new file mode 100644
index 000000000..af66b3fb4
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Internal/TranslationDatabaseBase.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;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using PG.StarWarsGame.Localisation.Languages;
+
+namespace PG.StarWarsGame.Localisation.Data.Internal
+{
+ internal abstract class TranslationDatabaseBase : ITranslationDatabase
+ {
+ private IAlamoLanguageDefinition? _activeLanguage;
+
+ public IReadOnlyList Languages { get; }
+
+ public IAlamoLanguageDefinition? ActiveLanguage
+ {
+ get => _activeLanguage;
+ set
+ {
+ if (value is not null && !Languages.Any(l => l.Equals(value)))
+ throw new ArgumentException(
+ $"Language '{value.LanguageIdentifier}' is not registered in this database.", nameof(value));
+ _activeLanguage = value;
+ }
+ }
+
+ protected TranslationDatabaseBase(IReadOnlyList languages)
+ {
+ Languages = languages;
+ }
+
+ public abstract int Count { get; }
+ public abstract TranslationEntry this[int index] { get; }
+ public abstract bool SetTranslation(string key, IAlamoLanguageDefinition language, string value);
+ public abstract bool RemoveEntry(string key);
+ public abstract void Clear();
+ public abstract IEnumerator GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Internal/TranslationDatabaseBuilder.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Internal/TranslationDatabaseBuilder.cs
new file mode 100644
index 000000000..f2dab8da2
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Internal/TranslationDatabaseBuilder.cs
@@ -0,0 +1,62 @@
+// 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 PG.StarWarsGame.Localisation.Languages;
+
+namespace PG.StarWarsGame.Localisation.Data.Internal
+{
+ internal sealed class TranslationDatabaseBuilder : ITranslationDatabaseBuilder
+ {
+ private readonly List _languages = new List();
+ private IAlamoLanguageDefinition? _activeLanguage;
+
+ public ITranslationDatabaseBuilder WithLanguage(IAlamoLanguageDefinition language)
+ {
+ if (language is null) throw new ArgumentNullException(nameof(language));
+ _languages.Add(language);
+ return this;
+ }
+
+ public ITranslationDatabaseBuilder WithLanguages(IEnumerable languages)
+ {
+ if (languages is null) throw new ArgumentNullException(nameof(languages));
+ _languages.AddRange(languages);
+ return this;
+ }
+
+ public ITranslationDatabaseBuilder SetActiveLanguage(IAlamoLanguageDefinition language)
+ {
+ _activeLanguage = language;
+ return this;
+ }
+
+ public IKeyedTranslationDatabase BuildKeyed()
+ {
+ var langs = _languages.ToList().AsReadOnly();
+ var db = new KeyedTranslationDatabase(langs);
+ ApplyActiveLanguage(db);
+ return db;
+ }
+
+ public IOrderedTranslationDatabase BuildOrdered()
+ {
+ var langs = _languages.ToList().AsReadOnly();
+ var db = new OrderedTranslationDatabase(langs);
+ ApplyActiveLanguage(db);
+ return db;
+ }
+
+ private void ApplyActiveLanguage(ITranslationDatabase db)
+ {
+ if (_activeLanguage is null) return;
+ if (!db.Languages.Contains(_activeLanguage))
+ throw new ArgumentException(
+ $"Active language '{_activeLanguage.LanguageIdentifier}' was not added to this builder.",
+ nameof(_activeLanguage));
+ db.ActiveLanguage = _activeLanguage;
+ }
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Localisation/LocalisationBean.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Localisation/LocalisationBean.cs
deleted file mode 100644
index 040b84d1e..000000000
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Localisation/LocalisationBean.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-// 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.Core.Data.Bean;
-using System.Diagnostics.CodeAnalysis;
-
-namespace PG.StarWarsGame.Localisation.Data.Localisation
-{
- public class LocalisationBean : AbstractBean
- {
- public string Localisation { get; set; }
-
- public LocalisationBean([NotNull] LocalisationKey key, string localisation) : base(key)
- {
- Localisation = localisation;
- }
- }
-}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Localisation/LocalisationKey.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Localisation/LocalisationKey.cs
deleted file mode 100644
index ac08080d6..000000000
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Localisation/LocalisationKey.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-// 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.Core.Data.Key;
-using PG.Core.Localisation;
-
-namespace PG.StarWarsGame.Localisation.Data.Localisation
-{
- public class LocalisationKey : AbstractKey
- {
- public LocalisationKey(IAlamoLanguageDefinition key) : base(key)
- {
- }
- }
-}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Translation/TranslationBean.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Translation/TranslationBean.cs
deleted file mode 100644
index 53c462614..000000000
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Translation/TranslationBean.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-// 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.Core.Data.Bean;
-using PG.Core.Data.Repository;
-using PG.StarWarsGame.Localisation.Data.Localisation;
-
-namespace PG.StarWarsGame.Localisation.Data.Translation
-{
- public class TranslationBean : AbstractInMemoryKeyValuePairRepository,
- IBean
- {
- public TranslationKey Key { get; }
- }
-}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Translation/TranslationKey.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Translation/TranslationKey.cs
deleted file mode 100644
index d37f0d783..000000000
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/Translation/TranslationKey.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// 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.Core.Data.Key;
-
-namespace PG.StarWarsGame.Localisation.Data.Translation
-{
- public class TranslationKey : AbstractStringKey
- {
- public TranslationKey(string key) : base(key)
- {
- }
- }
-}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/TranslationDatabaseFactory.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/TranslationDatabaseFactory.cs
new file mode 100644
index 000000000..eed41778b
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/TranslationDatabaseFactory.cs
@@ -0,0 +1,49 @@
+// 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 PG.StarWarsGame.Localisation.Data.Config;
+using PG.StarWarsGame.Localisation.Data.Internal;
+using PG.StarWarsGame.Localisation.Languages;
+
+namespace PG.StarWarsGame.Localisation.Data
+{
+ ///
+ /// Default implementation of .
+ ///
+ public sealed class TranslationDatabaseFactory : ITranslationDatabaseFactory
+ {
+ ///
+ public IKeyedTranslationDatabase CreateKeyed(IReadOnlyList languages)
+ {
+ if (languages is null) throw new ArgumentNullException(nameof(languages));
+ return new KeyedTranslationDatabase(languages);
+ }
+
+ ///
+ public IKeyedTranslationDatabase CreateKeyed(TranslationProjectDescriptor descriptor)
+ {
+ if (descriptor is null) throw new ArgumentNullException(nameof(descriptor));
+ return CreateKeyed(descriptor.Languages.ToList());
+ }
+
+ ///
+ public IOrderedTranslationDatabase CreateOrdered(IReadOnlyList languages)
+ {
+ if (languages is null) throw new ArgumentNullException(nameof(languages));
+ return new OrderedTranslationDatabase(languages);
+ }
+
+ ///
+ public IOrderedTranslationDatabase CreateOrdered(TranslationProjectDescriptor descriptor)
+ {
+ if (descriptor is null) throw new ArgumentNullException(nameof(descriptor));
+ return CreateOrdered(descriptor.Languages.ToList());
+ }
+
+ ///
+ public ITranslationDatabaseBuilder CreateDatabase() => new TranslationDatabaseBuilder();
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/TranslationEntry.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/TranslationEntry.cs
new file mode 100644
index 000000000..4b01965c0
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Data/TranslationEntry.cs
@@ -0,0 +1,43 @@
+// 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 PG.StarWarsGame.Localisation.Languages;
+
+namespace PG.StarWarsGame.Localisation.Data
+{
+ ///
+ /// A single translation entry: a string key with zero or more language-specific values.
+ ///
+ public sealed class TranslationEntry
+ {
+ private readonly Dictionary _translations;
+
+ /// Gets the translation key.
+ public string Key { get; }
+
+ /// Gets all available translations, keyed by language definition.
+ public IReadOnlyDictionary Translations => _translations;
+
+ internal TranslationEntry(string key)
+ {
+ Key = key ?? throw new ArgumentNullException(nameof(key));
+ _translations = new Dictionary();
+ }
+
+ internal void SetTranslation(IAlamoLanguageDefinition language, string value)
+ {
+ _translations[language] = value;
+ }
+
+ ///
+ /// Attempts to retrieve the translation value for .
+ ///
+ public bool TryGetTranslation(IAlamoLanguageDefinition language, [MaybeNullWhen(false)] out string? value)
+ {
+ return _translations.TryGetValue(language, out value);
+ }
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Csv/CsvTranslationExporter.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Csv/CsvTranslationExporter.cs
new file mode 100644
index 000000000..c2a0f96f3
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Csv/CsvTranslationExporter.cs
@@ -0,0 +1,62 @@
+// 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;
+using System.IO;
+using System.Linq;
+using System.Text;
+using CsvHelper;
+using CsvHelper.Configuration;
+using PG.StarWarsGame.Localisation.Data;
+
+namespace PG.StarWarsGame.Localisation.IO.Csv
+{
+ ///
+ /// Default implementation of .
+ /// Produces a CSV with header row: key,LANG1,LANG2,...
+ ///
+ public sealed class CsvTranslationExporter : ICsvTranslationExporter
+ {
+ ///
+ public string Export(ITranslationDatabase source)
+ {
+ if (source is null) throw new ArgumentNullException(nameof(source));
+
+ var config = new CsvConfiguration(CultureInfo.InvariantCulture)
+ {
+ HasHeaderRecord = false,
+ NewLine = "\n",
+ };
+
+ var sb = new StringBuilder();
+ using var writer = new StringWriter(sb);
+ using var csv = new CsvWriter(writer, config);
+
+ var langs = source.Languages;
+
+ csv.WriteField("key");
+ foreach (var lang in langs)
+ csv.WriteField(lang.LanguageIdentifier);
+ csv.NextRecord();
+
+ var rows = source is IKeyedTranslationDatabase
+ ? source.OrderBy(e => e.Key, StringComparer.Ordinal)
+ : source.AsEnumerable();
+
+ foreach (var entry in rows)
+ {
+ csv.WriteField(entry.Key);
+ foreach (var lang in langs)
+ {
+ entry.TryGetTranslation(lang, out var val);
+ csv.WriteField(val ?? string.Empty);
+ }
+ csv.NextRecord();
+ }
+
+ writer.Flush();
+ return sb.ToString();
+ }
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Csv/CsvTranslationImporter.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Csv/CsvTranslationImporter.cs
new file mode 100644
index 000000000..b027a6885
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Csv/CsvTranslationImporter.cs
@@ -0,0 +1,74 @@
+// 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;
+using System.IO;
+using System.Linq;
+using CsvHelper;
+using CsvHelper.Configuration;
+using PG.StarWarsGame.Localisation.Data;
+using PG.StarWarsGame.Localisation.Languages;
+using PG.StarWarsGame.Localisation.Services;
+
+namespace PG.StarWarsGame.Localisation.IO.Csv
+{
+ ///
+ /// Default implementation of .
+ /// Expects a CSV with header row: key,LANG1,LANG2,...
+ ///
+ public sealed class CsvTranslationImporter : ICsvTranslationImporter
+ {
+ private readonly ILanguageService _languageService;
+
+ /// Initialises a new instance with the given language service.
+ public CsvTranslationImporter(ILanguageService languageService)
+ {
+ _languageService = languageService ?? throw new ArgumentNullException(nameof(languageService));
+ }
+
+ ///
+ public void Import(TextReader source, ITranslationDatabase target)
+ {
+ if (source is null) throw new ArgumentNullException(nameof(source));
+ if (target is null) throw new ArgumentNullException(nameof(target));
+
+ var config = new CsvConfiguration(CultureInfo.InvariantCulture)
+ {
+ HasHeaderRecord = true,
+ MissingFieldFound = null,
+ };
+
+ using var csv = new CsvReader(source, config);
+
+ csv.Read();
+ csv.ReadHeader();
+ var headers = csv.HeaderRecord;
+ if (headers is null || headers.Length < 2) return;
+
+ var langColumns = new IAlamoLanguageDefinition?[headers.Length - 1];
+ for (var i = 1; i < headers.Length; i++)
+ {
+ _languageService.TryGetByIdentifier(headers[i], out langColumns[i - 1]);
+ }
+
+ var dbLanguages = target.Languages;
+
+ while (csv.Read())
+ {
+ var key = csv.GetField(0);
+ if (key is null) continue;
+
+ for (var i = 1; i < headers.Length; i++)
+ {
+ var lang = langColumns[i - 1];
+ if (lang is null) continue;
+ if (!dbLanguages.Any(l => l.Equals(lang))) continue;
+ var val = csv.GetField(i);
+ if (val is null) continue;
+ target.SetTranslation(key, lang, val);
+ }
+ }
+ }
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Csv/ICsvTranslationExporter.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Csv/ICsvTranslationExporter.cs
new file mode 100644
index 000000000..11a6089f4
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Csv/ICsvTranslationExporter.cs
@@ -0,0 +1,13 @@
+// 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.StarWarsGame.Localisation.IO.Csv
+{
+ ///
+ /// Exports all languages from a translation database to a CSV string.
+ /// Schema: first column is key, remaining columns are language identifiers.
+ ///
+ public interface ICsvTranslationExporter : IMultiLanguageTranslationExporter
+ {
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Csv/ICsvTranslationImporter.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Csv/ICsvTranslationImporter.cs
new file mode 100644
index 000000000..2e0f4203a
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Csv/ICsvTranslationImporter.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 System.IO;
+
+namespace PG.StarWarsGame.Localisation.IO.Csv
+{
+ ///
+ /// Imports multi-language translation data from a CSV text reader.
+ /// Schema: first column is key, remaining columns are language identifiers.
+ ///
+ public interface ICsvTranslationImporter : IMultiLanguageTranslationImporter
+ {
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Dat/DatTranslationExporter.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Dat/DatTranslationExporter.cs
new file mode 100644
index 000000000..df73dc2d3
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Dat/DatTranslationExporter.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.
+
+using System;
+using PG.StarWarsGame.Files.DAT.Data;
+using PG.StarWarsGame.Files.DAT.Services.Builder;
+using PG.StarWarsGame.Localisation.Data;
+using PG.StarWarsGame.Localisation.Languages;
+
+namespace PG.StarWarsGame.Localisation.IO.Dat
+{
+ ///
+ /// Default implementation of .
+ /// Produces a sorted (CRC32-ordered) model for
+ /// and an unsorted model for .
+ ///
+ public sealed class DatTranslationExporter : IDatTranslationExporter
+ {
+ private readonly IServiceProvider _services;
+
+ /// Initialises a new instance with the given service provider.
+ public DatTranslationExporter(IServiceProvider services)
+ {
+ _services = services ?? throw new ArgumentNullException(nameof(services));
+ }
+
+ ///
+ public IDatModel Export(ITranslationDatabase source, IAlamoLanguageDefinition language)
+ {
+ if (source is null) throw new ArgumentNullException(nameof(source));
+ if (language is null) throw new ArgumentNullException(nameof(language));
+
+ IDatBuilder builder = source is IKeyedTranslationDatabase
+ ? new EmpireAtWarMasterTextBuilder(false, _services)
+ : new EmpireAtWarCreditsTextBuilder(_services);
+
+ foreach (var entry in source)
+ {
+ if (entry.TryGetTranslation(language, out var value) && !string.IsNullOrEmpty(value))
+ builder.AddEntry(entry.Key, value!);
+ }
+
+ return builder.BuildModel();
+ }
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Dat/DatTranslationImporter.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Dat/DatTranslationImporter.cs
new file mode 100644
index 000000000..269fbf264
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Dat/DatTranslationImporter.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 PG.StarWarsGame.Files.DAT.Data;
+using PG.StarWarsGame.Localisation.Data;
+using PG.StarWarsGame.Localisation.Languages;
+
+namespace PG.StarWarsGame.Localisation.IO.Dat
+{
+ ///
+ /// Default implementation of .
+ ///
+ public sealed class DatTranslationImporter : IDatTranslationImporter
+ {
+ ///
+ public void Import(IDatModel source, IAlamoLanguageDefinition language, ITranslationDatabase target)
+ {
+ if (source is null) throw new ArgumentNullException(nameof(source));
+ if (language is null) throw new ArgumentNullException(nameof(language));
+ if (target is null) throw new ArgumentNullException(nameof(target));
+
+ foreach (var entry in source)
+ target.SetTranslation(entry.Key, language, entry.Value);
+ }
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Dat/IDatTranslationExporter.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Dat/IDatTranslationExporter.cs
new file mode 100644
index 000000000..bc777d39b
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Dat/IDatTranslationExporter.cs
@@ -0,0 +1,14 @@
+// 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.Files.DAT.Data;
+
+namespace PG.StarWarsGame.Localisation.IO.Dat
+{
+ ///
+ /// Exports a single language from a translation database to a DAT model.
+ ///
+ public interface IDatTranslationExporter : ITranslationExporter
+ {
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Dat/IDatTranslationImporter.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Dat/IDatTranslationImporter.cs
new file mode 100644
index 000000000..9529eb01a
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Dat/IDatTranslationImporter.cs
@@ -0,0 +1,14 @@
+// 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.Files.DAT.Data;
+
+namespace PG.StarWarsGame.Localisation.IO.Dat
+{
+ ///
+ /// Imports translation data from a binary DAT model into a translation database.
+ ///
+ public interface IDatTranslationImporter : ITranslationImporter
+ {
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/ITranslationExporter.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/ITranslationExporter.cs
new file mode 100644
index 000000000..9c1a33a88
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/ITranslationExporter.cs
@@ -0,0 +1,31 @@
+// 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.Localisation.Data;
+using PG.StarWarsGame.Localisation.Languages;
+
+namespace PG.StarWarsGame.Localisation.IO
+{
+ ///
+ /// Exports single-language translation data from a database to .
+ ///
+ public interface ITranslationExporter
+ {
+ ///
+ /// Serialises the translations for from
+ /// into .
+ ///
+ TResult Export(ITranslationDatabase source, IAlamoLanguageDefinition language);
+ }
+
+ ///
+ /// Exports multi-language translation data from a database to .
+ ///
+ public interface IMultiLanguageTranslationExporter
+ {
+ ///
+ /// Serialises all languages and entries from into .
+ ///
+ TResult Export(ITranslationDatabase source);
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/ITranslationImporter.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/ITranslationImporter.cs
new file mode 100644
index 000000000..fd9c7c7be
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/ITranslationImporter.cs
@@ -0,0 +1,32 @@
+// 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.Localisation.Data;
+using PG.StarWarsGame.Localisation.Languages;
+
+namespace PG.StarWarsGame.Localisation.IO
+{
+ ///
+ /// Imports single-language translation data from into a database.
+ ///
+ public interface ITranslationImporter
+ {
+ ///
+ /// Reads and adds all entries for
+ /// into .
+ ///
+ void Import(TSource source, IAlamoLanguageDefinition language, ITranslationDatabase target);
+ }
+
+ ///
+ /// Imports multi-language translation data from into a database.
+ ///
+ public interface IMultiLanguageTranslationImporter
+ {
+ ///
+ /// Reads and adds all languages and entries into .
+ /// Languages not tracked by are skipped.
+ ///
+ void Import(TSource source, ITranslationDatabase target);
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Properties/IPropertiesTranslationExporter.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Properties/IPropertiesTranslationExporter.cs
new file mode 100644
index 000000000..d76ddab5f
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Properties/IPropertiesTranslationExporter.cs
@@ -0,0 +1,12 @@
+// 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.StarWarsGame.Localisation.IO.Properties
+{
+ ///
+ /// Exports a single language from a translation database to a Java-style .properties string.
+ ///
+ public interface IPropertiesTranslationExporter : ITranslationExporter
+ {
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Properties/IPropertiesTranslationImporter.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Properties/IPropertiesTranslationImporter.cs
new file mode 100644
index 000000000..811b8d8a0
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Properties/IPropertiesTranslationImporter.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 System.IO;
+
+namespace PG.StarWarsGame.Localisation.IO.Properties
+{
+ ///
+ /// Imports single-language translation data from a Java-style .properties text reader.
+ /// Format: KEY=Value, lines beginning with # are comments.
+ ///
+ public interface IPropertiesTranslationImporter : ITranslationImporter
+ {
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Properties/PropertiesTranslationExporter.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Properties/PropertiesTranslationExporter.cs
new file mode 100644
index 000000000..6c3d908f4
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Properties/PropertiesTranslationExporter.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;
+using System.Linq;
+using System.Text;
+using PG.StarWarsGame.Localisation.Data;
+using PG.StarWarsGame.Localisation.Languages;
+
+namespace PG.StarWarsGame.Localisation.IO.Properties
+{
+ ///
+ /// Default implementation of .
+ /// Produces KEY=Value lines, one per entry.
+ ///
+ public sealed class PropertiesTranslationExporter : IPropertiesTranslationExporter
+ {
+ ///
+ public string Export(ITranslationDatabase source, IAlamoLanguageDefinition language)
+ {
+ if (source is null) throw new ArgumentNullException(nameof(source));
+ if (language is null) throw new ArgumentNullException(nameof(language));
+
+ var rows = source is IKeyedTranslationDatabase
+ ? source.OrderBy(e => e.Key, StringComparer.Ordinal).AsEnumerable()
+ : source.AsEnumerable();
+
+ var sb = new StringBuilder();
+ foreach (var entry in rows)
+ {
+ if (entry.TryGetTranslation(language, out var value) && value is not null)
+ {
+ sb.Append(EscapeKey(entry.Key));
+ sb.Append('=');
+ sb.AppendLine(value);
+ }
+ }
+ return sb.ToString();
+ }
+
+ private static string EscapeKey(string key) =>
+ key.Replace("=", "\\=").Replace(":", "\\:").Replace("#", "\\#");
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Properties/PropertiesTranslationImporter.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Properties/PropertiesTranslationImporter.cs
new file mode 100644
index 000000000..40fbfe4ec
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Properties/PropertiesTranslationImporter.cs
@@ -0,0 +1,43 @@
+// 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 PG.StarWarsGame.Localisation.Data;
+using PG.StarWarsGame.Localisation.Languages;
+
+namespace PG.StarWarsGame.Localisation.IO.Properties
+{
+ ///
+ /// Default implementation of .
+ /// Reads KEY=Value lines; lines starting with # are comments and are skipped.
+ /// Only the first = is treated as a separator; the value may contain further = characters.
+ ///
+ public sealed class PropertiesTranslationImporter : IPropertiesTranslationImporter
+ {
+ ///
+ public void Import(TextReader source, IAlamoLanguageDefinition language, ITranslationDatabase target)
+ {
+ if (source is null) throw new ArgumentNullException(nameof(source));
+ if (language is null) throw new ArgumentNullException(nameof(language));
+ if (target is null) throw new ArgumentNullException(nameof(target));
+
+ string? line;
+ while ((line = source.ReadLine()) is not null)
+ {
+ var trimmed = line.TrimStart();
+ if (trimmed.Length == 0 || trimmed[0] == '#') continue;
+
+ var separator = trimmed.IndexOf('=');
+ if (separator < 0) continue;
+
+ var key = UnescapeKey(trimmed.Substring(0, separator));
+ var value = trimmed.Substring(separator + 1);
+ target.SetTranslation(key, language, value);
+ }
+ }
+
+ private static string UnescapeKey(string key) =>
+ key.Replace("\\=", "=").Replace("\\:", ":").Replace("\\#", "#");
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Xml/IXmlTranslationExporter.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Xml/IXmlTranslationExporter.cs
new file mode 100644
index 000000000..42e450f2a
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Xml/IXmlTranslationExporter.cs
@@ -0,0 +1,14 @@
+// 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.Xml.Linq;
+
+namespace PG.StarWarsGame.Localisation.IO.Xml
+{
+ ///
+ /// Exports all languages from a translation database to an eaw-translation XML document.
+ ///
+ public interface IXmlTranslationExporter : IMultiLanguageTranslationExporter
+ {
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Xml/IXmlTranslationImporter.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Xml/IXmlTranslationImporter.cs
new file mode 100644
index 000000000..701840cee
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Xml/IXmlTranslationImporter.cs
@@ -0,0 +1,14 @@
+// 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.Xml.Linq;
+
+namespace PG.StarWarsGame.Localisation.IO.Xml
+{
+ ///
+ /// Imports multi-language translation data from an eaw-translation XML document.
+ ///
+ public interface IXmlTranslationImporter : IMultiLanguageTranslationImporter
+ {
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Xml/XmlTranslationExporter.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Xml/XmlTranslationExporter.cs
new file mode 100644
index 000000000..8fafeff57
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Xml/XmlTranslationExporter.cs
@@ -0,0 +1,50 @@
+// 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 System.Xml.Linq;
+using PG.StarWarsGame.Localisation.Data;
+using PG.StarWarsGame.Localisation.Services;
+
+namespace PG.StarWarsGame.Localisation.IO.Xml
+{
+ ///
+ /// Default implementation of .
+ /// Produces an eaw-translation v1 XML document.
+ ///
+ public sealed class XmlTranslationExporter : IXmlTranslationExporter
+ {
+ internal static readonly XNamespace Ns = "urn:alamoenginetools:localisation:v1";
+
+ ///
+ public XDocument Export(ITranslationDatabase source)
+ {
+ if (source is null) throw new ArgumentNullException(nameof(source));
+
+ var root = new XElement(Ns + "LocalisationData");
+
+ var rows = source is IKeyedTranslationDatabase
+ ? source.OrderBy(e => e.Key, StringComparer.Ordinal).AsEnumerable()
+ : source.AsEnumerable();
+
+ foreach (var entry in rows)
+ {
+ var locEl = new XElement(Ns + "Localisation",
+ new XAttribute("key", entry.Key));
+
+ foreach (var kv in entry.Translations.OrderBy(kv => kv.Key.LanguageIdentifier))
+ {
+ locEl.Add(new XElement(Ns + "TranslationData",
+ new XElement(Ns + "Translation",
+ new XAttribute("Language", kv.Key.LanguageIdentifier),
+ kv.Value)));
+ }
+
+ root.Add(locEl);
+ }
+
+ return new XDocument(root);
+ }
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Xml/XmlTranslationImporter.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Xml/XmlTranslationImporter.cs
new file mode 100644
index 000000000..8c621fa39
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/IO/Xml/XmlTranslationImporter.cs
@@ -0,0 +1,59 @@
+// 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 System.Xml.Linq;
+using PG.StarWarsGame.Localisation.Data;
+using PG.StarWarsGame.Localisation.Services;
+
+namespace PG.StarWarsGame.Localisation.IO.Xml
+{
+ ///
+ /// Default implementation of .
+ /// Reads eaw-translation v1 XML documents.
+ ///
+ public sealed class XmlTranslationImporter : IXmlTranslationImporter
+ {
+ private readonly ILanguageService _languageService;
+
+ /// Initialises a new instance with the given language service.
+ public XmlTranslationImporter(ILanguageService languageService)
+ {
+ _languageService = languageService ?? throw new ArgumentNullException(nameof(languageService));
+ }
+
+ ///
+ public void Import(XDocument source, ITranslationDatabase target)
+ {
+ if (source is null) throw new ArgumentNullException(nameof(source));
+ if (target is null) throw new ArgumentNullException(nameof(target));
+
+ var ns = XmlTranslationExporter.Ns;
+ var root = source.Root;
+ if (root is null) return;
+
+ foreach (var locEl in root.Elements(ns + "Localisation"))
+ {
+ var key = locEl.Attribute("key")?.Value;
+ if (key is null) continue;
+
+ foreach (var dataEl in locEl.Elements(ns + "TranslationData"))
+ {
+ foreach (var transEl in dataEl.Elements(ns + "Translation"))
+ {
+ var langId = transEl.Attribute("Language")?.Value;
+ var value = transEl.Value;
+
+ if (langId is null) continue;
+ if (!_languageService.TryGetByIdentifier(langId, out var lang) || lang is null)
+ continue;
+ if (!target.Languages.Contains(lang)) continue;
+
+ target.SetTranslation(key, lang, value);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/AlamoLanguageDefinitionBase.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/AlamoLanguageDefinitionBase.cs
new file mode 100644
index 000000000..b5dcb50d7
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/AlamoLanguageDefinitionBase.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.Globalization;
+using PG.StarWarsGame.Localisation.Languages.Attributes;
+
+namespace PG.StarWarsGame.Localisation.Languages
+{
+ ///
+ /// Base class for all Alamo language definitions.
+ ///
+ public abstract class AlamoLanguageDefinitionBase : IAlamoLanguageDefinition
+ {
+ private readonly bool _isOfficiallySupported;
+ private readonly GameContext[] _supportedContexts;
+
+ ///
+ public string LanguageIdentifier { get; }
+
+ ///
+ public CultureInfo Culture { get; }
+
+ ///
+ public bool IsDefault { get; }
+
+ ///
+ /// Gets the engine's language identifier string. Subclasses return a constant value.
+ ///
+ protected abstract string ConfiguredLanguageIdentifier { get; }
+
+ ///
+ /// Gets the .NET culture for this language. Subclasses return a constant value.
+ ///
+ protected abstract CultureInfo ConfiguredCulture { get; }
+
+ /// Initialises the language definition by reading its attributes.
+ protected AlamoLanguageDefinitionBase()
+ {
+ LanguageIdentifier = ConfiguredLanguageIdentifier;
+ Culture = ConfiguredCulture;
+
+ var attr = (OfficiallySupportedLanguageAttribute?)Attribute.GetCustomAttribute(
+ GetType(), typeof(OfficiallySupportedLanguageAttribute));
+
+ _isOfficiallySupported = attr != null;
+ _supportedContexts = attr?.SupportedContexts ?? Array.Empty();
+ IsDefault = GetType().IsDefined(typeof(DefaultLanguageAttribute), false);
+ }
+
+ ///
+ public bool IsOfficiallySupported() => _isOfficiallySupported;
+
+ ///
+ public bool IsOfficiallySupported(GameContext context)
+ {
+ if (!_isOfficiallySupported) return false;
+ return _supportedContexts.Length == 0 || Array.IndexOf(_supportedContexts, context) >= 0;
+ }
+
+ ///
+ public bool Equals(IAlamoLanguageDefinition? other)
+ {
+ if (other is null) return false;
+ if (ReferenceEquals(this, other)) return true;
+ return string.Equals(LanguageIdentifier, other.LanguageIdentifier, StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ public override bool Equals(object? obj) => obj is IAlamoLanguageDefinition other && Equals(other);
+
+ ///
+ public override int GetHashCode() =>
+ StringComparer.OrdinalIgnoreCase.GetHashCode(LanguageIdentifier);
+
+ ///
+ public override string ToString() => LanguageIdentifier;
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/Attributes/DefaultLanguageAttribute.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/Attributes/DefaultLanguageAttribute.cs
new file mode 100644
index 000000000..c49c232e0
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/Attributes/DefaultLanguageAttribute.cs
@@ -0,0 +1,16 @@
+// 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;
+
+namespace PG.StarWarsGame.Localisation.Languages.Attributes
+{
+ ///
+ /// Marks a language definition as the default game language.
+ /// Only one concrete implementation may carry this attribute.
+ ///
+ [AttributeUsage(AttributeTargets.Class, Inherited = false)]
+ public sealed class DefaultLanguageAttribute : Attribute
+ {
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/Attributes/OfficiallySupportedLanguageAttribute.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/Attributes/OfficiallySupportedLanguageAttribute.cs
new file mode 100644
index 000000000..c56496e10
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/Attributes/OfficiallySupportedLanguageAttribute.cs
@@ -0,0 +1,28 @@
+// 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;
+
+namespace PG.StarWarsGame.Localisation.Languages.Attributes
+{
+ ///
+ /// Marks a language definition as officially supported by the Alamo engine,
+ /// optionally restricting support to specific game contexts.
+ /// When no contexts are specified the language is considered supported in all game contexts.
+ ///
+ [AttributeUsage(AttributeTargets.Class, Inherited = false)]
+ public sealed class OfficiallySupportedLanguageAttribute : Attribute
+ {
+ ///
+ /// The game contexts in which this language is officially supported.
+ /// An empty array means the language is supported in all game contexts.
+ ///
+ public GameContext[] SupportedContexts { get; }
+
+ ///
+ public OfficiallySupportedLanguageAttribute(params GameContext[] contexts)
+ {
+ SupportedContexts = contexts ?? Array.Empty();
+ }
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/ChineseAlamoLanguageDefinition.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/ChineseAlamoLanguageDefinition.cs
index 24aaf739e..5d7847cef 100644
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/ChineseAlamoLanguageDefinition.cs
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/ChineseAlamoLanguageDefinition.cs
@@ -1,26 +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 PG.Core.Attributes;
-using PG.Core.Localisation;
-using PG.Core.Localisation.Attributes;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
+using PG.StarWarsGame.Localisation.Languages.Attributes;
namespace PG.StarWarsGame.Localisation.Languages
{
- ///
- /// The language definition for the Chinese language.
- ///
- ///
- /// Officially supported by the Alamo Engine.
- ///
- [Order(5000)]
- [ExcludeFromCodeCoverage]
+ /// The language definition for Chinese (Simplified, CN).
+ /// Officially supported by the Alamo Engine.
[OfficiallySupportedLanguage]
- public sealed class ChineseAlamoLanguageDefinition : AbstractAlamoLanguageDefinition
+ [ExcludeFromCodeCoverage]
+ public sealed class ChineseAlamoLanguageDefinition : AlamoLanguageDefinitionBase
{
+ ///
protected override string ConfiguredLanguageIdentifier => "CHINESE";
+
+ ///
protected override CultureInfo ConfiguredCulture => CultureInfo.GetCultureInfo("zh-CN");
}
}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/EnglishAlamoLanguageDefinition.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/EnglishAlamoLanguageDefinition.cs
index b40de6b56..fbbe9b900 100644
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/EnglishAlamoLanguageDefinition.cs
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/EnglishAlamoLanguageDefinition.cs
@@ -1,28 +1,28 @@
// 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.Core.Attributes;
-using PG.Core.Localisation;
-using PG.Core.Localisation.Attributes;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
+using PG.StarWarsGame.Localisation.Languages.Attributes;
namespace PG.StarWarsGame.Localisation.Languages
{
///
- /// The language definition for the English (US) language.
+ /// The language definition for English (US).
///
///
/// Officially supported by the Alamo Engine.
- /// This language is the default game language and development language of all Alamo Engine games.
+ /// This is the default game language and development language of all Alamo Engine games.
///
- [Default]
- [Order(5000)]
- [ExcludeFromCodeCoverage]
+ [DefaultLanguage]
[OfficiallySupportedLanguage]
- public sealed class EnglishAlamoLanguageDefinition : AbstractAlamoLanguageDefinition
+ [ExcludeFromCodeCoverage]
+ public sealed class EnglishAlamoLanguageDefinition : AlamoLanguageDefinitionBase
{
+ ///
protected override string ConfiguredLanguageIdentifier => "ENGLISH";
+
+ ///
protected override CultureInfo ConfiguredCulture => CultureInfo.GetCultureInfo("en-US");
}
}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/FrenchAlamoLanguageDefinition.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/FrenchAlamoLanguageDefinition.cs
index f35f750eb..65d07a2f8 100644
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/FrenchAlamoLanguageDefinition.cs
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/FrenchAlamoLanguageDefinition.cs
@@ -1,26 +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 PG.Core.Attributes;
-using PG.Core.Localisation;
-using PG.Core.Localisation.Attributes;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
+using PG.StarWarsGame.Localisation.Languages.Attributes;
namespace PG.StarWarsGame.Localisation.Languages
{
- ///
- /// The language definition for the French language.
- ///
- ///
- /// Officially supported by the Alamo Engine.
- ///
- [Order(5000)]
- [ExcludeFromCodeCoverage]
+ /// The language definition for French (FR).
+ /// Officially supported by the Alamo Engine.
[OfficiallySupportedLanguage]
- public sealed class FrenchAlamoLanguageDefinition : AbstractAlamoLanguageDefinition
+ [ExcludeFromCodeCoverage]
+ public sealed class FrenchAlamoLanguageDefinition : AlamoLanguageDefinitionBase
{
+ ///
protected override string ConfiguredLanguageIdentifier => "FRENCH";
+
+ ///
protected override CultureInfo ConfiguredCulture => CultureInfo.GetCultureInfo("fr-FR");
}
}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/GameContext.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/GameContext.cs
new file mode 100644
index 000000000..1b37e2046
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/GameContext.cs
@@ -0,0 +1,17 @@
+// 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.StarWarsGame.Localisation.Languages
+{
+ ///
+ /// Identifies the Petroglyph Star Wars game product.
+ ///
+ public enum GameContext
+ {
+ /// Star Wars: Empire at War (base game).
+ EaW,
+
+ /// Star Wars: Empire at War — Forces of Corruption (expansion).
+ FoC
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/GermanAlamoLanguageDefinition.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/GermanAlamoLanguageDefinition.cs
index a751c334f..56f016351 100644
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/GermanAlamoLanguageDefinition.cs
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/GermanAlamoLanguageDefinition.cs
@@ -1,26 +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 PG.Core.Attributes;
-using PG.Core.Localisation;
-using PG.Core.Localisation.Attributes;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
+using PG.StarWarsGame.Localisation.Languages.Attributes;
namespace PG.StarWarsGame.Localisation.Languages
{
- ///
- /// The language definition for the German language.
- ///
- ///
- /// Officially supported by the Alamo Engine.
- ///
- [Order(5000)]
- [ExcludeFromCodeCoverage]
+ /// The language definition for German (DE).
+ /// Officially supported by the Alamo Engine.
[OfficiallySupportedLanguage]
- public sealed class GermanAlamoLanguageDefinition : AbstractAlamoLanguageDefinition
+ [ExcludeFromCodeCoverage]
+ public sealed class GermanAlamoLanguageDefinition : AlamoLanguageDefinitionBase
{
+ ///
protected override string ConfiguredLanguageIdentifier => "GERMAN";
+
+ ///
protected override CultureInfo ConfiguredCulture => CultureInfo.GetCultureInfo("de-DE");
}
}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/IAlamoLanguageDefinition.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/IAlamoLanguageDefinition.cs
new file mode 100644
index 000000000..173a50ec9
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/IAlamoLanguageDefinition.cs
@@ -0,0 +1,41 @@
+// 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.Localisation.Languages
+{
+ ///
+ /// Describes a language supported by the Alamo game engine.
+ ///
+ public interface IAlamoLanguageDefinition : IEquatable
+ {
+ ///
+ /// Gets the engine's internal language identifier (e.g. "ENGLISH").
+ ///
+ string LanguageIdentifier { get; }
+
+ ///
+ /// Gets the .NET culture associated with this language.
+ ///
+ CultureInfo Culture { get; }
+
+ ///
+ /// Returns if this language is officially supported by the Alamo engine
+ /// in at least one game context.
+ ///
+ bool IsOfficiallySupported();
+
+ ///
+ /// Returns if this language is officially supported within the given
+ /// .
+ ///
+ bool IsOfficiallySupported(GameContext context);
+
+ ///
+ /// Gets a value indicating whether this is the default game language (English).
+ ///
+ bool IsDefault { get; }
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/ItalianAlamoLanguageDefinition.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/ItalianAlamoLanguageDefinition.cs
index 05f86ce88..d8936f7e5 100644
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/ItalianAlamoLanguageDefinition.cs
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/ItalianAlamoLanguageDefinition.cs
@@ -1,26 +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 PG.Core.Attributes;
-using PG.Core.Localisation;
-using PG.Core.Localisation.Attributes;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
+using PG.StarWarsGame.Localisation.Languages.Attributes;
namespace PG.StarWarsGame.Localisation.Languages
{
- ///
- /// The language definition for the Italian language.
- ///
- ///
- /// Officially supported by the Alamo Engine.
- ///
- [Order(5000)]
- [ExcludeFromCodeCoverage]
+ /// The language definition for Italian (IT).
+ /// Officially supported by the Alamo Engine.
[OfficiallySupportedLanguage]
- public sealed class ItalianAlamoLanguageDefinition : AbstractAlamoLanguageDefinition
+ [ExcludeFromCodeCoverage]
+ public sealed class ItalianAlamoLanguageDefinition : AlamoLanguageDefinitionBase
{
+ ///
protected override string ConfiguredLanguageIdentifier => "ITALIAN";
+
+ ///
protected override CultureInfo ConfiguredCulture => CultureInfo.GetCultureInfo("it-IT");
}
}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/JapaneseAlamoLanguageDefinition.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/JapaneseAlamoLanguageDefinition.cs
index d7f572a7c..9d7bac474 100644
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/JapaneseAlamoLanguageDefinition.cs
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/JapaneseAlamoLanguageDefinition.cs
@@ -1,26 +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 PG.Core.Attributes;
-using PG.Core.Localisation;
-using PG.Core.Localisation.Attributes;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
+using PG.StarWarsGame.Localisation.Languages.Attributes;
namespace PG.StarWarsGame.Localisation.Languages
{
- ///
- /// The language definition for the Japanese language.
- ///
- ///
- /// Officially supported by the Alamo Engine.
- ///
- [Order(5000)]
+ /// The language definition for Japanese (JA).
+ /// Officially supported by the Alamo Engine.
+ [OfficiallySupportedLanguage(GameContext.EaW)]
[ExcludeFromCodeCoverage]
- [OfficiallySupportedLanguage]
- public sealed class JapaneseAlamoLanguageDefinition : AbstractAlamoLanguageDefinition
+ public sealed class JapaneseAlamoLanguageDefinition : AlamoLanguageDefinitionBase
{
+ ///
protected override string ConfiguredLanguageIdentifier => "JAPANESE";
+
+ ///
protected override CultureInfo ConfiguredCulture => CultureInfo.GetCultureInfo("ja");
}
}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/KoreanAlamoLanguageDefinition.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/KoreanAlamoLanguageDefinition.cs
index 944ea8be4..79509396a 100644
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/KoreanAlamoLanguageDefinition.cs
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/KoreanAlamoLanguageDefinition.cs
@@ -1,26 +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 PG.Core.Attributes;
-using PG.Core.Localisation;
-using PG.Core.Localisation.Attributes;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
+using PG.StarWarsGame.Localisation.Languages.Attributes;
namespace PG.StarWarsGame.Localisation.Languages
{
- ///
- /// The language definition for the Korean language.
- ///
- ///
- /// Officially supported by the Alamo Engine.
- ///
- [Order(5000)]
+ /// The language definition for Korean (KO).
+ /// Officially supported by the Alamo Engine.
+ [OfficiallySupportedLanguage(GameContext.EaW)]
[ExcludeFromCodeCoverage]
- [OfficiallySupportedLanguage]
- public sealed class KoreanAlamoLanguageDefinition : AbstractAlamoLanguageDefinition
+ public sealed class KoreanAlamoLanguageDefinition : AlamoLanguageDefinitionBase
{
+ ///
protected override string ConfiguredLanguageIdentifier => "KOREAN";
+
+ ///
protected override CultureInfo ConfiguredCulture => CultureInfo.GetCultureInfo("ko");
}
}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/PolishAlamoLanguageDefinition.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/PolishAlamoLanguageDefinition.cs
index bf175b13d..3f977d2cd 100644
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/PolishAlamoLanguageDefinition.cs
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/PolishAlamoLanguageDefinition.cs
@@ -1,26 +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 PG.Core.Attributes;
-using PG.Core.Localisation;
-using PG.Core.Localisation.Attributes;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
+using PG.StarWarsGame.Localisation.Languages.Attributes;
namespace PG.StarWarsGame.Localisation.Languages
{
- ///
- /// The language definition for the Polish language.
- ///
- ///
- /// Officially supported by the Alamo Engine.
- ///
- [Order(5000)]
- [ExcludeFromCodeCoverage]
+ /// The language definition for Polish (PL).
+ /// Officially supported by the Alamo Engine.
[OfficiallySupportedLanguage]
- public sealed class PolishAlamoLanguageDefinition : AbstractAlamoLanguageDefinition
+ [ExcludeFromCodeCoverage]
+ public sealed class PolishAlamoLanguageDefinition : AlamoLanguageDefinitionBase
{
+ ///
protected override string ConfiguredLanguageIdentifier => "POLISH";
+
+ ///
protected override CultureInfo ConfiguredCulture => CultureInfo.GetCultureInfo("pl");
}
}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/RussianAlamoLanguageDefinition.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/RussianAlamoLanguageDefinition.cs
index 35e3f4107..16861cdb7 100644
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/RussianAlamoLanguageDefinition.cs
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/RussianAlamoLanguageDefinition.cs
@@ -1,26 +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 PG.Core.Attributes;
-using PG.Core.Localisation;
-using PG.Core.Localisation.Attributes;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
+using PG.StarWarsGame.Localisation.Languages.Attributes;
namespace PG.StarWarsGame.Localisation.Languages
{
- ///
- /// The language definition for the Russian language.
- ///
- ///
- /// Officially supported by the Alamo Engine.
- ///
- [Order(5000)]
- [ExcludeFromCodeCoverage]
+ /// The language definition for Russian (RU).
+ /// Officially supported by the Alamo Engine.
[OfficiallySupportedLanguage]
- public sealed class RussianAlamoLanguageDefinition : AbstractAlamoLanguageDefinition
+ [ExcludeFromCodeCoverage]
+ public sealed class RussianAlamoLanguageDefinition : AlamoLanguageDefinitionBase
{
+ ///
protected override string ConfiguredLanguageIdentifier => "RUSSIAN";
+
+ ///
protected override CultureInfo ConfiguredCulture => CultureInfo.GetCultureInfo("ru");
}
}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/SpanishAlamoLanguageDefinition.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/SpanishAlamoLanguageDefinition.cs
index 841b80196..e43a8282f 100644
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/SpanishAlamoLanguageDefinition.cs
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/SpanishAlamoLanguageDefinition.cs
@@ -1,26 +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 PG.Core.Attributes;
-using PG.Core.Localisation;
-using PG.Core.Localisation.Attributes;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
+using PG.StarWarsGame.Localisation.Languages.Attributes;
namespace PG.StarWarsGame.Localisation.Languages
{
- ///
- /// The language definition for the Spanish language.
- ///
- ///
- /// Officially supported by the Alamo Engine.
- ///
- [Order(5000)]
- [ExcludeFromCodeCoverage]
+ /// The language definition for Spanish (ES).
+ /// Officially supported by the Alamo Engine.
[OfficiallySupportedLanguage]
- public sealed class SpanishAlamoLanguageDefinition : AbstractAlamoLanguageDefinition
+ [ExcludeFromCodeCoverage]
+ public sealed class SpanishAlamoLanguageDefinition : AlamoLanguageDefinitionBase
{
+ ///
protected override string ConfiguredLanguageIdentifier => "SPANISH";
+
+ ///
protected override CultureInfo ConfiguredCulture => CultureInfo.GetCultureInfo("es-ES");
}
}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/ThaiAlamoLanguageDefinition.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/ThaiAlamoLanguageDefinition.cs
index f74773cde..c9f76a303 100644
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/ThaiAlamoLanguageDefinition.cs
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Languages/ThaiAlamoLanguageDefinition.cs
@@ -1,26 +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 PG.Core.Attributes;
-using PG.Core.Localisation;
-using PG.Core.Localisation.Attributes;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
+using PG.StarWarsGame.Localisation.Languages.Attributes;
namespace PG.StarWarsGame.Localisation.Languages
{
- ///
- /// The language definition for the Thai language.
- ///
- ///
- /// Officially supported by the Alamo Engine.
- ///
- [Order(5000)]
+ /// The language definition for Thai (TH).
+ /// Officially supported by the Alamo Engine.
+ [OfficiallySupportedLanguage(GameContext.EaW)]
[ExcludeFromCodeCoverage]
- [OfficiallySupportedLanguage]
- public sealed class ThaiAlamoLanguageDefinition : AbstractAlamoLanguageDefinition
+ public sealed class ThaiAlamoLanguageDefinition : AlamoLanguageDefinitionBase
{
+ ///
protected override string ConfiguredLanguageIdentifier => "THAI";
+
+ ///
protected override CultureInfo ConfiguredCulture => CultureInfo.GetCultureInfo("th");
}
}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/LocalisationServiceContribution.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/LocalisationServiceContribution.cs
new file mode 100644
index 000000000..8ed0b491a
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/LocalisationServiceContribution.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 Microsoft.Extensions.DependencyInjection;
+using PG.StarWarsGame.Localisation.Data;
+using PG.StarWarsGame.Localisation.IO.Csv;
+using PG.StarWarsGame.Localisation.IO.Dat;
+using PG.StarWarsGame.Localisation.IO.Properties;
+using PG.StarWarsGame.Localisation.IO.Xml;
+using PG.StarWarsGame.Localisation.Languages;
+using PG.StarWarsGame.Localisation.Services;
+
+namespace PG.StarWarsGame.Localisation
+{
+ ///
+ /// Provides initialization routines for the PG.StarWarsGame.Localisation library.
+ ///
+ public static class LocalisationServiceContribution
+ {
+ ///
+ /// Adds all services provided by this library to the specified .
+ ///
+ public static IServiceCollection SupportLocalisation(this IServiceCollection serviceCollection)
+ {
+ // Register all 11 officially supported language definitions
+ serviceCollection.AddSingleton(new EnglishAlamoLanguageDefinition());
+ serviceCollection.AddSingleton(new GermanAlamoLanguageDefinition());
+ serviceCollection.AddSingleton(new FrenchAlamoLanguageDefinition());
+ serviceCollection.AddSingleton(new SpanishAlamoLanguageDefinition());
+ serviceCollection.AddSingleton(new ItalianAlamoLanguageDefinition());
+ serviceCollection.AddSingleton(new ChineseAlamoLanguageDefinition());
+ serviceCollection.AddSingleton(new JapaneseAlamoLanguageDefinition());
+ serviceCollection.AddSingleton(new KoreanAlamoLanguageDefinition());
+ serviceCollection.AddSingleton(new PolishAlamoLanguageDefinition());
+ serviceCollection.AddSingleton(new RussianAlamoLanguageDefinition());
+ serviceCollection.AddSingleton(new ThaiAlamoLanguageDefinition());
+
+ serviceCollection.AddSingleton(sp =>
+ new LanguageService(sp.GetServices()));
+
+ serviceCollection.AddSingleton();
+
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+
+ return serviceCollection;
+ }
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.csproj b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.csproj
index eabd49fb8..08d058bc3 100644
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.csproj
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation.csproj
@@ -1,52 +1,40 @@
-
+
- false
- netstandard2.0
+ netstandard2.0;netstandard2.1;net10.0
+ PG.StarWarsGame.Localisation
+ Provides language definitions, an in-memory translation database, and format adapters (DAT, XML, CSV, Properties) for Petroglyph Star Wars game localisations.
+ PG.StarWarsGame.Localisation
+ AlamoEngineTools.PG.StarWarsGame.Localisation
+ alamo;petroglyph;glyphx;localisation;translation
+
+
+ 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.Localisation/PG.StarWarsGame.Localisation/Services/ILanguageService.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Services/ILanguageService.cs
new file mode 100644
index 000000000..7ecfcf0e4
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Services/ILanguageService.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.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using PG.StarWarsGame.Localisation.Languages;
+
+namespace PG.StarWarsGame.Localisation.Services
+{
+ ///
+ /// Provides lookup and validation operations over the registered Alamo language definitions.
+ ///
+ public interface ILanguageService
+ {
+ /// Gets all registered language definitions.
+ IReadOnlyList AllLanguages { get; }
+
+ /// Returns all officially supported language definitions across all game contexts.
+ IReadOnlyList OfficiallySupported();
+
+ /// Returns the officially supported language definitions for the given .
+ IReadOnlyList OfficiallySupported(GameContext context);
+
+ ///
+ /// Gets the default game language (English).
+ ///
+ ///
+ /// Thrown when no language definition is marked as default.
+ ///
+ IAlamoLanguageDefinition Default { get; }
+
+ ///
+ /// Returns if is officially supported in at least one game context.
+ ///
+ ///
+ /// Thrown when is .
+ ///
+ bool IsOfficiallySupported(IAlamoLanguageDefinition language);
+
+ ///
+ /// Returns if is officially supported in the given .
+ ///
+ ///
+ /// Thrown when is .
+ ///
+ bool IsOfficiallySupported(IAlamoLanguageDefinition language, GameContext context);
+
+ ///
+ /// Attempts to find a language definition whose
+ /// matches (case-insensitive).
+ ///
+ bool TryGetByIdentifier(string identifier, [NotNullWhen(true)] out IAlamoLanguageDefinition? language);
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Services/ILocalisationEditService.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Services/ILocalisationEditService.cs
deleted file mode 100644
index dd5875da0..000000000
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Services/ILocalisationEditService.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-// 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.Core.Attributes;
-using PG.Core.Data.Etl.Source;
-using PG.Core.Data.Etl.Target;
-using PG.Core.Reporting;
-using PG.Core.Services.Attributes;
-using PG.StarWarsGame.Localisation.Data.Translation;
-
-namespace PG.StarWarsGame.Localisation.Services
-{
- [Order(OrderAttribute.DEFAULT_ORDER)]
- [DefaultServiceImplementation(typeof(LocalisationEditService))]
- public interface ILocalisationEditService
- {
- bool IsInitialised { get; }
-
- ISourceDescriptor SourceDescriptor { get; }
- ITargetDescriptor TargetDescriptor { get; }
-
- IReport LoadAndInitialise();
-
- IReport SaveAndDiscard();
-
- bool TryGet(TranslationKey key, out TranslationBean bean);
-
- bool TryAdd(TranslationBean bean);
-
- bool TryUpdate(TranslationBean bean);
-
- bool TryRemove(TranslationKey key);
- }
-}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Services/ILocalisationProviderService.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Services/ILocalisationProviderService.cs
deleted file mode 100644
index 6dbb89bb6..000000000
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Services/ILocalisationProviderService.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// 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.Core.Attributes;
-using PG.Core.Localisation;
-
-namespace PG.StarWarsGame.Localisation.Services
-{
- [Order(OrderAttribute.DEFAULT_ORDER)]
- public interface ILocalisationProviderService where T : IAlamoLanguageDefinition
- {
- bool TryGet(string key, out string value);
- }
-}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Services/LanguageService.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Services/LanguageService.cs
new file mode 100644
index 000000000..876a98382
--- /dev/null
+++ b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Services/LanguageService.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.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using PG.StarWarsGame.Localisation.Languages;
+
+namespace PG.StarWarsGame.Localisation.Services
+{
+ ///
+ /// Default implementation of .
+ ///
+ public sealed class LanguageService : ILanguageService
+ {
+ private readonly IReadOnlyList _all;
+ private readonly IReadOnlyList _officiallySupported;
+ private readonly Dictionary _byIdentifier;
+
+ ///
+ public IReadOnlyList AllLanguages => _all;
+
+ ///
+ public IAlamoLanguageDefinition Default =>
+ _all.FirstOrDefault(l => l.IsDefault)
+ ?? throw new InvalidOperationException("No default language definition is registered.");
+
+ ///
+ /// Initialises the service with the given language definitions.
+ ///
+ public LanguageService(IEnumerable languages)
+ {
+ if (languages is null)
+ throw new ArgumentNullException(nameof(languages));
+
+ var list = languages.ToList();
+ _all = list.AsReadOnly();
+ _officiallySupported = list.Where(l => l.IsOfficiallySupported()).ToList().AsReadOnly();
+ _byIdentifier = list.ToDictionary(
+ l => l.LanguageIdentifier,
+ l => l,
+ StringComparer.OrdinalIgnoreCase);
+ }
+
+ ///
+ public IReadOnlyList OfficiallySupported() => _officiallySupported;
+
+ ///
+ public IReadOnlyList OfficiallySupported(GameContext context)
+ {
+ return _all.Where(l => l.IsOfficiallySupported(context)).ToList().AsReadOnly();
+ }
+
+ ///
+ public bool IsOfficiallySupported(IAlamoLanguageDefinition language)
+ {
+ if (language is null)
+ throw new ArgumentNullException(nameof(language));
+ return _byIdentifier.TryGetValue(language.LanguageIdentifier, out var found)
+ && found.IsOfficiallySupported();
+ }
+
+ ///
+ public bool IsOfficiallySupported(IAlamoLanguageDefinition language, GameContext context)
+ {
+ if (language is null)
+ throw new ArgumentNullException(nameof(language));
+ return _byIdentifier.TryGetValue(language.LanguageIdentifier, out var found)
+ && found.IsOfficiallySupported(context);
+ }
+
+ ///
+ public bool TryGetByIdentifier(string identifier, [NotNullWhen(true)] out IAlamoLanguageDefinition? language)
+ {
+ return _byIdentifier.TryGetValue(identifier, out language);
+ }
+ }
+}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Services/LocalisationEditService.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Services/LocalisationEditService.cs
deleted file mode 100644
index 2f293a329..000000000
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Services/LocalisationEditService.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-// 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.Core.Data.Etl.Source;
-using PG.Core.Data.Etl.Target;
-using PG.Core.Reporting;
-using PG.Core.Services;
-using PG.StarWarsGame.Localisation.Data.Translation;
-using System;
-
-namespace PG.StarWarsGame.Localisation.Services
-{
- public class LocalisationEditService : AbstractService, ILocalisationEditService
- {
- private bool m_isInitialised = false;
-
- public LocalisationEditService(ISourceDescriptor sourceDescriptor, ITargetDescriptor targetDescriptor, IServiceProvider services) : base(services)
- {
- SourceDescriptor = sourceDescriptor;
- TargetDescriptor = targetDescriptor;
- }
-
- public bool IsInitialised => m_isInitialised;
-
- public ISourceDescriptor SourceDescriptor { get; }
- public ITargetDescriptor TargetDescriptor { get; }
-
- public IReport LoadAndInitialise()
- {
- throw new System.NotImplementedException();
- }
-
- public IReport SaveAndDiscard()
- {
- throw new System.NotImplementedException();
- }
-
- public bool TryGet(TranslationKey key, out TranslationBean bean)
- {
- throw new System.NotImplementedException();
- }
-
- public bool TryAdd(TranslationBean bean)
- {
- throw new System.NotImplementedException();
- }
-
- public bool TryUpdate(TranslationBean bean)
- {
- throw new System.NotImplementedException();
- }
-
- public bool TryRemove(TranslationKey key)
- {
- throw new System.NotImplementedException();
- }
- }
-}
diff --git a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Util/LocalisationUtility.cs b/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Util/LocalisationUtility.cs
deleted file mode 100644
index b6b6faca7..000000000
--- a/PG.StarWarsGame.Localisation/PG.StarWarsGame.Localisation/Util/LocalisationUtility.cs
+++ /dev/null
@@ -1,72 +0,0 @@
-// 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.Core.Attributes;
-using PG.Core.Localisation;
-using PG.StarWarsGame.Localisation.Commons;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace PG.StarWarsGame.Localisation.Util
-{
- public static class LocalisationUtility
- {
- private static readonly IEnumerable ALAMO_LANGUAGE_DEFINITION_TYPE_CACHE;
-
- static LocalisationUtility()
- {
- ALAMO_LANGUAGE_DEFINITION_TYPE_CACHE = AppDomain.CurrentDomain.GetAssemblies().SelectMany(s => s.GetTypes())
- .Where(p =>
- typeof(IAlamoLanguageDefinition).IsAssignableFrom(p) &&
- typeof(AbstractAlamoLanguageDefinition).IsAssignableFrom(p) &&
- p.IsClass &&
- !p.IsAbstract &&
- !p.IsInterface
- ).ToList();
- }
-
- public static IList GetAllAlamoLanguageDefinitions()
- {
- List alamoLanguageDefinitions = new();
- foreach (Type type in ALAMO_LANGUAGE_DEFINITION_TYPE_CACHE)
- {
- try
- {
- IAlamoLanguageDefinition languageDefinition =
- Activator.CreateInstance(type) as IAlamoLanguageDefinition;
- alamoLanguageDefinitions.Add(languageDefinition);
- }
- catch
- {
- // NOP - this definition is faulty.
- }
- }
- return alamoLanguageDefinitions;
- }
-
- public static IAlamoLanguageDefinition GetDefaultAlamoLanguageDefinition()
- {
- foreach (Type type in ALAMO_LANGUAGE_DEFINITION_TYPE_CACHE)
- {
- if (!type.GetAttributeValueOrDefault((DefaultAttribute d) => d.IsDefault))
- {
- continue;
- }
-
- try
- {
- return Activator.CreateInstance(type) as IAlamoLanguageDefinition;
- }
- catch (Exception e)
- {
- throw new DefaultAlamoLanguageDefinitionException(
- $"Could not create an instance of the default {nameof(IAlamoLanguageDefinition)} specification \"{type}\".", e);
- }
- }
-
- throw new DefaultAlamoLanguageDefinitionException(
- $"No default {nameof(IAlamoLanguageDefinition)} specified.");
- }
- }
-}
diff --git a/PetroglyphTools.slnx b/PetroglyphTools.slnx
index cfd0881dd..b7388c1fe 100644
--- a/PetroglyphTools.slnx
+++ b/PetroglyphTools.slnx
@@ -14,6 +14,10 @@
+
+
+
+