Merge pull request #155 from sp-tarkov/localeService

Locale service
This commit is contained in:
Chomp
2025-04-18 09:52:25 +01:00
committed by GitHub
11 changed files with 119 additions and 138 deletions
@@ -11,7 +11,8 @@ public class DataCallbacks(
HttpResponseUtil _httpResponseUtil,
DatabaseService _databaseService,
TraderController _traderController,
HideoutController _hideoutController
HideoutController _hideoutController,
LocaleService _localeService
)
{
/// <summary>
@@ -133,10 +134,9 @@ public class DataCallbacks(
public string GetLocalesGlobal(string url, EmptyRequestData _, string sessionID)
{
var localeId = url.Replace("/client/locale/", "");
var locales = _databaseService.GetLocales();
var result = locales.Global?[localeId].Value ?? locales.Global?.FirstOrDefault(m => m.Key == "en").Value.Value;
var locales = _localeService.GetLocaleDb(localeId);
return _httpResponseUtil.GetUnclearedBody(result);
return _httpResponseUtil.GetUnclearedBody(locales);
}
/// <summary>
@@ -296,15 +296,13 @@ public class GiveSptCommand(
}
/// <summary>
/// Return the desired locale, falls back to english if it cannot be found
/// Return the desired locale, falls back to english if it cannot be found
/// </summary>
/// <param name="desiredLocale">Locale code, e.g. "fr" for french</param>
/// <returns></returns>
protected Dictionary<string, string> GetGlobalsLocale(string desiredLocale)
{
return _databaseService.GetLocales().Global.TryGetValue(desiredLocale, out var locale)
? locale.Value
: _databaseService.GetLocales().Global["en"].Value;
return _localeService.GetLocaleDb(desiredLocale);
}
/**
@@ -6,6 +6,8 @@ namespace SPTarkov.Server.Core.Models.Spt.Server;
public record LocaleBase
{
[JsonPropertyName("global")]
/// DO NOT USE THIS PROPERTY DIRECTLY, USE LOCALESERVICE INSTEAD
/// THIS IS LAZY LOADED AND YOUR CHANGES WILL NOT BE SAVED
public Dictionary<string, LazyLoad<Dictionary<string, string>>>? Global
{
get;
@@ -1,75 +0,0 @@
using SPTarkov.Server.Core.Models.Utils;
using SPTarkov.Common.Annotations;
namespace SPTarkov.Server.Core.Services;
[Injectable(InjectionType.Singleton)]
public class CustomLocaleService(
ISptLogger<CustomLocaleService> logger
)
{
protected Dictionary<string, Dictionary<string, string>> customServerLocales = new();
protected Dictionary<string, Dictionary<string, string>> customClientLocales = new();
/// <summary>
/// Path should link to a folder containing every locale that should be added to the server locales
/// e.g. en.json for english, fr.json for french. <br/>
/// Inside each JSON should be a Dictionary of the locale key and localised text
/// </summary>
/// <param name="locale">en/fr/de</param>
/// <param name="localeKey">locale key to store values against</param>
/// <param name="localeValue">Localised string to store</param>
public void AddServerLocales(string locale, string localeKey, string localeValue)
{
AddToDictionary(locale, localeKey, localeValue, customServerLocales);
}
/// <summary>
/// Path should link to a folder containing every locale that should be added to the game locales
/// e.g. en.json for english, fr.json for french. <br/>
/// Inside each JSON should be a Dictionary of the locale key and localised text
/// </summary>
/// <param name="locale">en/fr/de</param>
/// <param name="localeKey">locale key to store values against</param>
/// <param name="localeValue">Localised string to store</param>
public void AddGameLocales(string locale, string localeKey, string localeValue)
{
AddToDictionary(locale, localeKey, localeValue, customClientLocales);
}
protected void AddToDictionary(string locale, string localeKey, string localeValue,
Dictionary<string, Dictionary<string, string>> dictionaryToAddTo)
{
dictionaryToAddTo.TryAdd(locale, new Dictionary<string, string>());
if (!dictionaryToAddTo.TryGetValue(locale, out var localeDictToAddTo))
{
logger.Error($"Unable to get custom locale dictionary keyed by: {locale}");
return;
}
if (!localeDictToAddTo.TryAdd(localeKey, localeValue))
{
logger.Error($"Unable to add: {localeKey} {localeValue} to custom locale dictionary: {locale}");
}
}
public string? GetServerValue(string locale, string localeKey)
{
return GetValueFromDictionary(locale, localeKey, customServerLocales);
}
public string? GetClientValue(string locale, string localeKey)
{
return GetValueFromDictionary(locale, localeKey, customClientLocales);
}
protected string? GetValueFromDictionary(string locale, string localeKey,
Dictionary<string, Dictionary<string, string>> dictionaryToGetFrom)
{
return dictionaryToGetFrom.TryGetValue(locale, out var localeDictToGetFrom)
? localeDictToGetFrom.GetValueOrDefault(localeKey) // Locale exists, look up value or return null
: null; // No locale (e.g. en/fr/de) at all
}
}
@@ -11,7 +11,7 @@ public class I18nService
private readonly Dictionary<string, string> _fallbacks;
private readonly FileUtil _fileUtil;
private readonly JsonUtil _jsonUtil;
private readonly CustomLocaleService _customLocaleService;
private readonly LocaleService _localeService;
private readonly Dictionary<string, LazyLoad<Dictionary<string, string>>> _loadedLocales = new();
private HashSet<string> _locales;
@@ -20,11 +20,11 @@ public class I18nService
public I18nService(
FileUtil fileUtil,
JsonUtil jsonUtil,
CustomLocaleService customLocaleService,
HashSet<string> locales,
Dictionary<string, string> fallbacks,
string defaultLocale,
string directory
string directory,
LocaleService localeService
)
{
_locales = locales;
@@ -32,8 +32,8 @@ public class I18nService
_defaultLocale = defaultLocale;
_directory = directory;
_jsonUtil = jsonUtil;
_customLocaleService = customLocaleService;
_fileUtil = fileUtil;
_localeService = localeService;
Initialize();
}
@@ -91,22 +91,28 @@ public class I18nService
public string GetLocalisedValue(string key)
{
// get loaded locales for set key
if (!_loadedLocales.TryGetValue(_setLocale, out var locales))
{
// if we are unable to get the "loadedLocales" for the set locale, return the key
return key;
}
// searching through loaded locales for given key
if (!locales.Value.TryGetValue(key, out var value))
{
// if the key is not found in loaded locales
// check if the key is found in the default locale
_loadedLocales.TryGetValue(_defaultLocale, out var defaults);
if (!defaults.Value.TryGetValue(key, out value))
{
value = _customLocaleService.GetClientValue(_defaultLocale, key);
value = _localeService.GetLocaleDb(_defaultLocale).FirstOrDefault(x => x.Key == key).Value;
}
return value ?? key;
}
// if the key is found in the server locale, return the value
return value;
}
@@ -3,6 +3,7 @@ using SPTarkov.Server.Core.Models.Spt.Config;
using SPTarkov.Server.Core.Models.Utils;
using SPTarkov.Server.Core.Servers;
using SPTarkov.Common.Annotations;
using SPTarkov.Server.Core.Utils.Json;
namespace SPTarkov.Server.Core.Services;
@@ -13,24 +14,68 @@ public class LocaleService(
ConfigServer _configServer
)
{
// we have to LazyLoad the data from the database and then combine it with the custom data before returning it
protected LocaleConfig _localeConfig = _configServer.GetConfig<LocaleConfig>();
protected Dictionary<string, Dictionary<string, string>> customClientLocales = new Dictionary<string, Dictionary<string, string>>();
/// <summary>
/// Get the eft globals db file based on the configured locale in config/locale.json, if not found, fall back to 'en'
/// Get the eft globals db file based on the configured locale in config/locale.json, if not found, fall back to 'en'
/// This will contain Custom locales added by mods
/// </summary>
/// <returns> Dictionary </returns>
public Dictionary<string, string> GetLocaleDb()
/// <returns> Dictionary of locales for desired language - en/fr/cn </returns>
public Dictionary<string, string> GetLocaleDb(string? language = null)
{
if (_databaseServer.GetTables().Locales.Global.TryGetValue(GetDesiredGameLocale(), out var desiredLocale))
var languageToUse = string.IsNullOrEmpty(language) ? GetDesiredGameLocale() : language;
Dictionary<string, string>? localeToReturn;
// if it can't get locales for language provided, default to en
if (TryGetLocaleDbWithCustomLocales(languageToUse, out localeToReturn) ||
TryGetLocaleDbWithCustomLocales("en", out localeToReturn))
{
return desiredLocale.Value;
// TODO: need to see if this needs to be cloned
return RemovePraporTestMessage(localeToReturn);
}
_logger.Warning(
$"Unable to find desired locale file using locale: {GetDesiredGameLocale()} from config/locale.json, falling back to 'en'"
);
throw new Exception($"unable to get locales from either {languageToUse} or en");
}
return _databaseServer.GetTables().Locales.Global["en"].Value;
/// <summary>
/// Attempts to retrieve the locale database for the specified language key, including custom locales if available.
/// </summary>
/// <param name="languageKey">The language key for which the locale database should be retrieved.</param>
/// <param name="localeToReturn">The resulting locale database as a dictionary, or null if the operation fails.</param>
/// <returns>True if the locale database was successfully retrieved, otherwise false.</returns>
private bool TryGetLocaleDbWithCustomLocales(string languageKey, out Dictionary<string, string>? localeToReturn)
{
localeToReturn = null;
if (!_databaseServer.GetTables().Locales.Global.TryGetValue(languageKey, out var keyedLocales))
{
return false;
}
localeToReturn = keyedLocales.Value;
if (customClientLocales.TryGetValue(languageKey, out var customClientLocale))
{
// there were custom locales for this language
localeToReturn = CombineDbWithCustomLocales(localeToReturn, customClientLocale);
}
return true;
}
/// <summary>
/// Combines the provided database locales with custom locales, ensuring that all entries are merged into a single dictionary.
/// Custom locale entries will overwrite existing keys from the database locales if conflicts occur.
///
/// </summary>
/// <param name="dbLocales">The dictionary containing locale entries from the database.</param>
/// <param name="customLocales">The dictionary containing custom locale entries to be merged.</param>
/// <returns>A dictionary representing the merged result of database and custom locales.</returns>
private Dictionary<string, string> CombineDbWithCustomLocales(Dictionary<string, string> dbLocales, Dictionary<string, string> customLocales)
{
return dbLocales.Union(customLocales).ToDictionary(x => x.Key, x => x.Value);
}
/// <summary>
@@ -179,4 +224,35 @@ public class LocaleService(
{
return GetLocaleDb().Keys.Where(x => x.StartsWith(partialKey)).ToList();
}
public void AddCustomClientLocale(string locale, string localeKey, string localeValue)
{
AddToDictionary(locale, localeKey, localeValue, customClientLocales);
}
private void AddToDictionary(string locale, string localeKey, string localeValue,
Dictionary<string, Dictionary<string, string>> dictionaryToAddTo)
{
dictionaryToAddTo.TryAdd(locale, new Dictionary<string, string>());
if (!dictionaryToAddTo.TryGetValue(locale, out var localeDictToAddTo))
{
_logger.Error($"Unable to get custom locale dictionary keyed by: {locale}");
return;
}
if (!localeDictToAddTo.TryAdd(localeKey, localeValue))
{
_logger.Error($"Unable to add: {localeKey} {localeValue} to custom locale dictionary: {locale}");
}
}
/// <summary>
/// Blank out the "test" mail message from prapor
/// </summary>
protected Dictionary<string, string> RemovePraporTestMessage(Dictionary<string, string> dbLocales)
{
dbLocales["61687e2c3e526901fa76baf9"] = "";
return dbLocales;
}
}
@@ -22,7 +22,6 @@ public class LocalisationService
public LocalisationService(
ISptLogger<LocalisationService> logger,
RandomUtil randomUtil,
CustomLocaleService customLocaleService,
DatabaseServer databaseServer,
LocaleService localeService,
JsonUtil jsonUtil,
@@ -36,11 +35,11 @@ public class LocalisationService
_i18nService = new I18nService(
fileUtil,
jsonUtil,
customLocaleService,
localeService.GetServerSupportedLocales().ToHashSet(),
localeService.GetLocaleFallbacks(),
"en",
"./Assets/database/locales/server"
"./Assets/database/locales/server",
localeService
);
_i18nService.SetLocaleByKey(localeService.GetDesiredServerLocale());
}
@@ -17,7 +17,8 @@ public class CustomItemService(
DatabaseService databaseService,
ItemHelper itemHelper,
ItemBaseClassService itemBaseClassService,
ICloner cloner
ICloner cloner,
LocaleService localeService
)
{
/// <summary>
@@ -206,17 +207,9 @@ public class CustomItemService(
newLocaleDetails ??= localeDetails[localeDetails.Keys.FirstOrDefault()];
// Create new record in locale file
if (!databaseService.GetLocales().Global.TryGetValue(shortNameKey.Key, out var desiredGlobal))
{
logger.Error($"Unable to add locale keys to {shortNameKey.Key}");
return;
}
desiredGlobal.Value[$"{newItemId} Name"] = newLocaleDetails.Name;
desiredGlobal.Value[$"{newItemId} ShortName"] = newLocaleDetails.ShortName;
desiredGlobal.Value[$"{newItemId} Description"] = newLocaleDetails.Description;
localeService.AddCustomClientLocale(shortNameKey.Key, $"{newItemId} Name", newLocaleDetails.Name);
localeService.AddCustomClientLocale(shortNameKey.Key, $"{newItemId} ShortName", newLocaleDetails.ShortName);
localeService.AddCustomClientLocale(shortNameKey.Key, $"{newItemId} Description", newLocaleDetails.Description);
}
}
@@ -101,8 +101,6 @@ public class PostDbLoadService(
RemoveNewBeginningRequirementFromPrestige();
RemovePraporTestMessage();
ValidateQuestAssortUnlocksExist();
if (_seasonalEventService.IsAutomaticEventDetectionEnabled())
@@ -145,7 +143,7 @@ public class PostDbLoadService(
}
/// <summary>
/// Merge custom achievements into achievement db table
/// Merge custom achievements into achievement db table
/// </summary>
protected void MergeCustomAchievements()
{
@@ -519,19 +517,6 @@ public class PostDbLoadService(
}
}
/// <summary>
/// Blank out the "test" mail message from prapor
/// </summary>
protected void RemovePraporTestMessage()
{
// Iterate over all languages (e.g. "en", "fr")
var locales = _databaseService.GetLocales();
foreach (var localeKvP in locales.Global)
{
locales.Global[localeKvP.Key].Value["61687e2c3e526901fa76baf9"] = "";
}
}
/// <summary>
/// Check for any missing assorts inside each traders assort.json data, checking against traders questassort.json
/// </summary>
@@ -21,7 +21,8 @@ public class SeasonalEventService(
BotHelper _botHelper,
ProfileHelper _profileHelper,
//DatabaseImporter _databaseImporter,
ConfigServer _configServer
ConfigServer _configServer,
LocaleService _localeService
)
{
private bool _christmasEventActive;
@@ -1060,9 +1061,8 @@ public class SeasonalEventService(
protected void RenameBitcoin()
{
var enLocale = _databaseService.GetLocales().Global["en"];
enLocale.Value[$"{ItemTpl.BARTER_PHYSICAL_BITCOIN} Name"] = "Physical SPT Coin";
enLocale.Value[$"{ItemTpl.BARTER_PHYSICAL_BITCOIN} ShortName"] = "0.2SPT";
_localeService.AddCustomClientLocale("en", $"{ItemTpl.BARTER_PHYSICAL_BITCOIN} Name", "Physical SPT Coin");
_localeService.AddCustomClientLocale("en", $"{ItemTpl.BARTER_PHYSICAL_BITCOIN} ShortName", "0.2SPT");
}
/// <summary>
+3 -6
View File
@@ -6,9 +6,7 @@ using SPTarkov.Server.Core.Models.Spt.Mod;
using SPTarkov.Server.Core.Models.Utils;
using SPTarkov.Server.Core.Utils;
using Serilog;
using Serilog.Events;
using Serilog.Exceptions;
using Serilog.Extensions.Logging;
using SPTarkov.Common.Semver;
using SPTarkov.Common.Semver.Implementations;
using SPTarkov.Server.Logger;
@@ -32,7 +30,7 @@ public static class Program
// validate and sort mods, this will also discard any mods that are invalid
var sortedLoadedMods = ValidateMods(mods);
// for harmony we use the original list, as some mods may only be bepinex patches only
// for harmony, we use the original list, as some mods may only be bepinex patches only
HarmonyBootstrapper.LoadAllPatches(mods.SelectMany(asm => asm.Assemblies).ToList());
// register SPT components
@@ -49,7 +47,7 @@ public static class Program
try
{
var watermark = serviceProvider.GetService<Watermark>();
// Initialize Watermak
// Initialize Watermark
watermark?.Initialize();
var appContext = serviceProvider.GetService<ApplicationContext>();
@@ -72,7 +70,7 @@ public static class Program
var app = serviceProvider.GetService<App>();
app?.Run().Wait();
// Run garbage collection now server is ready to start
// Run garbage collection now the server is ready to start
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
@@ -89,7 +87,6 @@ public static class Program
{
Console.WriteLine(ex);
logger.LogCritical(ex, "Critical exception, stopping server...");
// throw ex;
}
}