localisation service and others

This commit is contained in:
Alex
2025-01-06 23:24:11 +00:00
parent 35c62e6ada
commit 210ee10362
11 changed files with 305 additions and 84 deletions
-8
View File
@@ -1,8 +0,0 @@
namespace Core.Models.Config;
public class HttpConfig
{
public int Port { get; set; }
public string Ip { get; set; }
public bool LogRequests { get; set; }
}
+103 -2
View File
@@ -1,6 +1,107 @@
namespace Core.Models.Enums;
using Core.Models.Spt.Config;
namespace Core.Models.Enums;
public static class ConfigTypesExtension
{
public static string GetValue(this ConfigTypes type)
{
return type switch
{
ConfigTypes.AIRDROP => "spt-airdrop",
ConfigTypes.BACKUP => "spt-backup",
ConfigTypes.BOT => "spt-bot",
ConfigTypes.PMC => "spt-pmc",
ConfigTypes.CORE => "spt-core",
ConfigTypes.HEALTH => "spt-health",
ConfigTypes.HIDEOUT => "spt-hideout",
ConfigTypes.HTTP => "spt-http",
ConfigTypes.IN_RAID => "spt-inraid",
ConfigTypes.INSURANCE => "spt-insurance",
ConfigTypes.INVENTORY => "spt-inventory",
ConfigTypes.ITEM => "spt-item",
ConfigTypes.LOCALE => "spt-locale",
ConfigTypes.LOCATION => "spt-location",
ConfigTypes.LOOT => "spt-loot",
ConfigTypes.MATCH => "spt-match",
ConfigTypes.PLAYERSCAV => "spt-playerscav",
ConfigTypes.PMC_CHAT_RESPONSE => "spt-pmcchatresponse",
ConfigTypes.QUEST => "spt-quest",
ConfigTypes.RAGFAIR => "spt-ragfair",
ConfigTypes.REPAIR => "spt-repair",
ConfigTypes.SCAVCASE => "spt-scavcase",
ConfigTypes.TRADER => "spt-trader",
ConfigTypes.WEATHER => "spt-weather",
ConfigTypes.SEASONAL_EVENT => "spt-seasonalevents",
ConfigTypes.LOST_ON_DEATH => "spt-lostondeath",
ConfigTypes.GIFTS => "spt-gifts",
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
}
public static Type GetConfigType(this ConfigTypes type)
{
return type switch
{
ConfigTypes.AIRDROP => typeof(AirdropConfig),
ConfigTypes.BACKUP => typeof(BackupConfig),
ConfigTypes.BOT => typeof(BotConfig),
ConfigTypes.PMC => typeof(PmcConfig),
ConfigTypes.CORE => typeof(CoreConfig),
ConfigTypes.HEALTH => typeof(HealthConfig),
ConfigTypes.HIDEOUT => typeof(HideoutConfig),
ConfigTypes.HTTP => typeof(HttpConfig),
ConfigTypes.IN_RAID => typeof(InRaidConfig),
ConfigTypes.INSURANCE => typeof(InsuranceConfig),
ConfigTypes.INVENTORY => typeof(InventoryConfig),
ConfigTypes.ITEM => typeof(ItemConfig),
ConfigTypes.LOCALE => typeof(LocaleConfig),
ConfigTypes.LOCATION => typeof(LocationConfig),
ConfigTypes.LOOT => typeof(LootConfig),
ConfigTypes.MATCH => typeof(MatchConfig),
ConfigTypes.PLAYERSCAV => typeof(PlayerScavConfig),
ConfigTypes.PMC_CHAT_RESPONSE => typeof(PmcChatResponse),
ConfigTypes.QUEST => typeof(QuestConfig),
ConfigTypes.RAGFAIR => typeof(RagfairConfig),
ConfigTypes.REPAIR => typeof(RepairConfig),
ConfigTypes.SCAVCASE => typeof(ScavCaseConfig),
ConfigTypes.TRADER => typeof(TraderConfig),
ConfigTypes.WEATHER => typeof(WeatherConfig),
ConfigTypes.SEASONAL_EVENT => typeof(SeasonalEventConfig),
ConfigTypes.LOST_ON_DEATH => typeof(LostOnDeathConfig),
ConfigTypes.GIFTS => typeof(GiftsConfig),
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
}
}
public enum ConfigTypes
{
AIRDROP,
BACKUP,
BOT,
PMC,
CORE,
HEALTH,
HIDEOUT,
HTTP,
IN_RAID,
INSURANCE,
INVENTORY,
ITEM,
LOCALE,
LOCATION,
LOOT,
MATCH,
PLAYERSCAV,
PMC_CHAT_RESPONSE,
QUEST,
RAGFAIR,
REPAIR,
SCAVCASE,
TRADER,
WEATHER,
SEASONAL_EVENT,
LOST_ON_DEATH,
GIFTS
}
+9
View File
@@ -4,6 +4,15 @@ using Core.Models.Spt.Dialog;
namespace Core.Models.Spt.Config;
public class GiftsConfig : BaseConfig
{
[JsonPropertyName("kind")]
public string Kind { get; set; } = "spt-gifts";
[JsonPropertyName("gifts")]
public Dictionary<string, Gift> Gifts { get; set; }
}
public class Gift
{
/// <summary>
+1 -1
View File
@@ -2,7 +2,7 @@
namespace Core.Models.Spt.Config;
public class MatchConfig
public class MatchConfig : BaseConfig
{
[JsonPropertyName("kind")]
public string Kind { get; set; } = "spt-match";
+77 -1
View File
@@ -1,8 +1,84 @@
using Core.Annotations;
using System.Runtime.InteropServices.JavaScript;
using System.Text.Json;
using Core.Annotations;
using Core.Models.Enums;
using Core.Models.Spt.Config;
using ILogger = Core.Models.Utils.ILogger;
namespace Core.Servers;
[Injectable(InjectionType.Singleton)]
public class ConfigServer
{
private ILogger _logger;
protected Dictionary<string, object> configs;
protected readonly string[] acceptableFileExtensions = ["json", "jsonc"];
public ConfigServer(
ILogger logger
// TODO: We need JsonUtil here => JsonUtil jsonUtil,
)
{
_logger = logger;
Initialize();
}
public T GetConfig<T>(ConfigTypes configType) where T : BaseConfig
{
if (!configs.ContainsKey(configType.GetValue()))
{
throw new Exception($"Config: {configType} is undefined. Ensure you have not broken it via editing");
}
return configs[configType.GetValue()] as T;
}
public T GetConfigByString<T>(string configType) where T : BaseConfig
{
return configs[configType] as T;
}
public void Initialize()
{
_logger.Debug("Importing configs...");
// Get all filepaths
var filepath = "./assets/configs/";
var files = Directory.GetFiles(filepath);
// Add file content to result
foreach (var file in files)
{
if (acceptableFileExtensions.Contains(Path.GetExtension(file)))
{
var fileContent = File.ReadAllText(file);
var type = GetConfigTypeByFilename(file);
var deserializedContent = JsonSerializer.Deserialize(fileContent, type);
if (deserializedContent == null)
{
_logger.Error($"Config file: {file} is corrupt. Use a site like: https://jsonlint.com to find the issue.");
throw new Exception($"Server will not run until the: {file} config error mentioned above is fixed");
}
this.configs[$"spt-{Path.GetFileNameWithoutExtension(file)}"] = deserializedContent;
}
}
/** TODO: deal with this:
this.logger.info(`Commit hash: ${
globalThis.G_COMMIT || "DEBUG"
}`);
this.logger.info(`Build date: ${
globalThis.G_BUILDTIME || "DEBUG"
}`);
**/
}
private Type GetConfigTypeByFilename(string filename)
{
var type = Enum.GetValues<ConfigTypes>()
.First(en => en.GetValue().Contains(Path.GetFileNameWithoutExtension(filename)));
return type.GetConfigType();
}
}
+8 -2
View File
@@ -1,6 +1,12 @@
namespace Core.Servers;
using Core.Models.Spt.Server;
namespace Core.Servers;
public class DatabaseServer
{
protected DatabaseTables tableData = new();
public DatabaseTables GetTables() => tableData;
public void SetTables(DatabaseTables tables) => tableData = tables;
}
+9 -9
View File
@@ -1,10 +1,11 @@
using System.Net.WebSockets;
using Core.Context;
using Core.Models.Config;
using Core.Servers.Http;
using Core.Services;
using Microsoft.Extensions.Primitives;
using Core.Annotations;
using Core.Models.Enums;
using Core.Models.Spt.Config;
using ILogger = Core.Models.Utils.ILogger;
namespace Core.Servers;
@@ -38,8 +39,7 @@ public class HttpServer
_webSocketServer = webSocketServer;
_httpListeners = httpListeners;
// TODO: hook up the config server to put the HttpConfig here
httpConfig = new HttpConfig() { Ip = "127.0.0.1", Port = 80, LogRequests = true};
httpConfig = _configServer.GetConfig<HttpConfig>(ConfigTypes.HTTP);
}
public void Load(WebApplicationBuilder builder)
@@ -89,13 +89,13 @@ public class HttpServer
var isLocalRequest = IsLocalRequest(clientIp);
if (isLocalRequest.HasValue) {
if (isLocalRequest.Value) {
_logger.Info(_localisationService.GetText("client_request", req.url));
} else {
_logger.Info(_localisationService.GetText("client_request", context.Request.Path.Value));
} else
{
_logger.Info(
_localisationService.GetText("client_request_ip", {
ip: clientIp,
url: req.url.replaceAll("/", "\\"), // Localisation service escapes `/` into hex code `&#x2f;`
}),
_localisationService.GetText("client_request_ip",
new Dictionary<string, string>
{ { "ip", clientIp }, { "url", context.Request.Path.Value } })
);
}
}
+20 -5
View File
@@ -4,7 +4,7 @@ namespace Core.Services;
public class I18nService
{
private string[] _locales;
private List<string> _locales;
private Dictionary<string, string> _fallbacks;
private string _defaultLocale;
private string _directory;
@@ -13,7 +13,7 @@ public class I18nService
private Dictionary<string, Dictionary<string, string>> _loadedLocales = new();
public I18nService(string[] locales, Dictionary<string, string> fallbacks, string defaultLocale, string directory)
public I18nService(List<string> locales, Dictionary<string, string> fallbacks, string defaultLocale, string directory)
{
_locales = locales;
_fallbacks = fallbacks;
@@ -30,9 +30,12 @@ public class I18nService
throw new Exception($"Localisation files in directory {_directory} not found.");
foreach (var file in files)
{
_loadedLocales.Add(Path.GetFileName(file),
_loadedLocales.Add(Path.GetFileNameWithoutExtension(file),
JsonSerializer.Deserialize<Dictionary<string, string>>(File.ReadAllText(file)) ?? new Dictionary<string, string>());
}
if (!_loadedLocales.ContainsKey(_defaultLocale))
throw new Exception($"The default locale '{_defaultLocale}' does not exist on the loaded locales.");
}
public void SetLocale(string locale)
@@ -49,16 +52,28 @@ public class I18nService
throw new Exception($"Locale '{locale}' was not defined, and the found fallback locale did not match any of the loaded locales.");
_setLocale = foundFallbackLocale;
}
_setLocale = _defaultLocale;
}
}
public string GetLocalised(string key)
{
return null;
if (!_loadedLocales.TryGetValue(_setLocale, out var locales))
return key;
if (!locales.TryGetValue(key, out var value))
{
_loadedLocales.TryGetValue(_defaultLocale, out var defaults);
defaults.TryGetValue(key, out value);
return value ?? key;
}
return value;
}
public string GetLocalised(string key, params object[] args)
{
return null;
// TODO: Deal with arguments
return GetLocalised(key);
}
}
+61 -53
View File
@@ -1,4 +1,7 @@
using Core.Annotations;
using System.Globalization;
using Core.Annotations;
using Core.Models.Enums;
using Core.Models.Spt.Config;
using Core.Servers;
using ILogger = Core.Models.Utils.ILogger;
@@ -7,6 +10,7 @@ namespace Core.Services;
[Injectable(InjectionType.Singleton)]
public class LocaleService
{
private readonly LocaleConfig _localeConfig;
private readonly ILogger _logger;
private readonly DatabaseServer _databaseServer;
private readonly ConfigServer _configServer;
@@ -16,6 +20,7 @@ public class LocaleService
_logger = logger;
_databaseServer = databaseServer;
_configServer = configServer;
_localeConfig = configServer.GetConfig<LocaleConfig>(ConfigTypes.LOCALE);
}
/**
@@ -24,18 +29,16 @@ public class LocaleService
*/
public Dictionary<string, string> GetLocaleDb()
{
/*
const desiredLocale = this.databaseServer.getTables().locales.global[this.getDesiredGameLocale()];
if (desiredLocale) {
return desiredLocale;
}
var desiredLocale = _databaseServer.GetTables().locales.Global[GetDesiredGameLocale()];
if (desiredLocale != null)
{
return desiredLocale;
}
this.logger.warning(
`Unable to find desired locale file using locale: ${this.getDesiredGameLocale()} from config/locale.json, falling back to 'en'`,
);
_logger.Warning(
$"Unable to find desired locale file using locale: {GetDesiredGameLocale()} from config/locale.json, falling back to 'en'");
return this.databaseServer.getTables().locales.global.en;
*/
return _databaseServer.GetTables().locales.Global["en"];
}
/**
@@ -45,13 +48,12 @@ public class LocaleService
*/
public string GetDesiredGameLocale()
{
/*
if (this.localeConfig.gameLocale.toLowerCase() === "system") {
return this.getPlatformForClientLocale();
if (_localeConfig.GameLocale.ToLower() == "system")
{
return GetPlatformForClientLocale();
}
return this.localeConfig.gameLocale.toLowerCase();
*/
return _localeConfig.GameLocale.ToLower();
}
/**
@@ -61,22 +63,21 @@ public class LocaleService
*/
public string GetDesiredServerLocale()
{
/*
if (this.localeConfig.serverLocale.toLowerCase() === "system") {
return this.getPlatformForServerLocale();
if (_localeConfig.ServerLocale.ToLower() == "system")
{
return GetPlatformForServerLocale();
}
return this.localeConfig.serverLocale.toLowerCase();
*/
return _localeConfig.ServerLocale.ToLower();
}
/**
* Get array of languages supported for localisation
* @returns array of locales e.g. en/fr/cn
*/
public ICollection<string> GetServerSupportedLocales()
public List<string> GetServerSupportedLocales()
{
// return this.localeConfig.serverSupportedLocales;
return _localeConfig.ServerSupportedLocales;
}
/**
@@ -85,7 +86,7 @@ public class LocaleService
*/
public Dictionary<string, string> GetLocaleFallbacks()
{
// return this.localeConfig.fallbacks;
return _localeConfig.Fallbacks;
}
/**
@@ -94,20 +95,22 @@ public class LocaleService
*/
public string GetPlatformForServerLocale()
{
/*
const platformLocale = this.getPlatformLocale();
if (!platformLocale) {
this.logger.warning("System language could not be found, falling back to english");
var platformLocale = GetPlatformLocale();
if (platformLocale == null)
{
_logger.Warning("System language could not be found, falling back to english");
return "en";
}
const baseNameCode = platformLocale.baseName.toLowerCase();
if (!this.localeConfig.serverSupportedLocales.includes(baseNameCode)) {
// Chek if base language (e.g. CN / EN / DE) exists
const languageCode = platformLocale.language.toLocaleLowerCase();
if (this.localeConfig.serverSupportedLocales.includes(languageCode)) {
if (baseNameCode === "zh") {
var baseNameCode = platformLocale.TwoLetterISOLanguageName.ToLower();
if (!_localeConfig.ServerSupportedLocales.Contains(baseNameCode))
{
// Check if base language (e.g. CN / EN / DE) exists
var languageCode = platformLocale.Name.ToLower();
if (_localeConfig.ServerSupportedLocales.Contains(languageCode))
{
if (baseNameCode == "zh")
{
// Handle edge case of zh
return "zh-cn";
}
@@ -115,18 +118,18 @@ public class LocaleService
return languageCode;
}
if (baseNameCode === "pt") {
if (baseNameCode == "pt")
{
// Handle edge case of pt
return "pt-pt";
}
this.logger.warning(`Unsupported system language found: ${baseNameCode}, falling back to english`);
_logger.Warning($"Unsupported system language found: {baseNameCode}, falling back to english");
return "en";
}
return baseNameCode;
*/
}
/**
@@ -135,44 +138,49 @@ public class LocaleService
*/
protected string GetPlatformForClientLocale()
{
/*
const platformLocale = this.getPlatformLocale();
if (!platformLocale) {
this.logger.warning("System language could not be found, falling back to english");
var platformLocale = GetPlatformLocale();
if (platformLocale == null)
{
_logger.Warning("System language could not be found, falling back to english");
return "en";
}
const locales = this.databaseServer.getTables().locales;
const baseNameCode = platformLocale.baseName?.toLocaleLowerCase();
if (baseNameCode && locales.global[baseNameCode]) {
var locales = _databaseServer.GetTables().locales;
var baseNameCode = platformLocale.TwoLetterISOLanguageName.ToLower();
if (locales.Global.ContainsKey(baseNameCode))
{
return baseNameCode;
}
const languageCode = platformLocale.language?.toLowerCase();
if (languageCode && locales.global[languageCode]) {
var languageCode = platformLocale.Name.ToLower();
if (locales.Global.ContainsKey(languageCode))
{
return languageCode;
}
/*
const regionCode = platformLocale.region?.toLocaleLowerCase();
if (regionCode && locales.global[regionCode]) {
return regionCode;
}
*/
// BSG map DE to GE some reason
if (platformLocale.language === "de") {
if (baseNameCode == "de")
{
return "ge";
}
this.logger.warning(`Unsupported system language found: ${languageCode}, falling back to english`);
_logger.Warning($"Unsupported system language found: {languageCode}, falling back to english");
return "en";
*/
}
/**
* This is in a function so we can overwrite it during testing
* @returns The current platform locale
protected getPlatformLocale(): Intl.Locale {
return new Intl.Locale(Intl.DateTimeFormat().resolvedOptions().locale);
}
*/
protected CultureInfo GetPlatformLocale()
{
return CultureInfo.InstalledUICulture;
}
}
+7 -3
View File
@@ -25,9 +25,13 @@ public class LocalisationService
_randomUtil = randomUtil;
_databaseServer = databaseServer;
_localeService = localeService;
_i18nService = new I18nService();
File.ReadAllText()
_i18nService = new I18nService(
localeService.GetServerSupportedLocales(),
localeService.GetLocaleFallbacks(),
"en",
"./Assets/database/locales/server"
);
_i18nService.SetLocale(localeService.GetDesiredServerLocale());
}
public string GetText(string key, object? args = null)
+10
View File
@@ -0,0 +1,10 @@
using Core.Annotations;
namespace Core.Utils;
[Injectable(InjectionType.Singleton)]
public class JsonUtil
{
}