using System.Collections.Frozen; using SPTarkov.Common.Extensions; using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Utils; using SPTarkov.Server.Core.Utils.Json; namespace SPTarkov.Server.Core.Services; /// /// Handles translating server text into different languages /// [Injectable(InjectionType.Singleton)] public class ServerLocalisationService( ISptLogger logger, RandomUtil randomUtil, LocaleService localeService, JsonUtil jsonUtil, FileUtil fileUtil ) { private readonly Dictionary>> _loadedLocales = []; private string _serverLocale = localeService.GetDesiredServerLocale(); private readonly FrozenDictionary _localeFallbacks = localeService.GetLocaleFallbacks().ToFrozenDictionary(); private const string DefaultLocale = "en"; private const string LocaleDirectory = "./SPT_Data/database/locales/server"; private bool _serverLocalesHydrated = false; protected void HydrateServerLocales() { if (_serverLocalesHydrated) { return; } var files = fileUtil.GetFiles(LocaleDirectory, true).Where(f => fileUtil.GetFileExtension(f) == "json"); if (!files.Any()) { throw new Exception($"Localisation files in directory {LocaleDirectory} not found."); } foreach (var file in files) { _loadedLocales.Add( fileUtil.StripExtension(file), new LazyLoad>(() => jsonUtil.DeserializeFromFile>(file) ?? []) ); } if (!_loadedLocales.ContainsKey(DefaultLocale)) { throw new Exception($"The default locale '{DefaultLocale}' does not exist on the loaded locales."); } _serverLocalesHydrated = true; } public void SetServerLocaleByKey(string locale) { if (_loadedLocales.ContainsKey(locale)) { _serverLocale = locale; } else { var fallback = _localeFallbacks.Where(kv => locale.StartsWith(kv.Key.Replace("*", ""))); if (fallback.Any()) { var foundFallbackLocale = fallback.First().Value; if (!_loadedLocales.ContainsKey(foundFallbackLocale)) { throw new Exception( $"Locale '{locale}' was not defined, and the found fallback locale did not match any of the loaded locales." ); } _serverLocale = foundFallbackLocale; } _serverLocale = DefaultLocale; } } /// /// Get a localised value using the passed in key /// /// Key to look up locale for /// optional arguments /// Localised string public string GetText(string key, object? args = null) { return args is null ? GetLocalisedValue(key) : GetLocalised(key, args); } /// /// Get a localised value using the passed in key /// /// Key to look up locale for /// Value to localize /// Localised string public string GetText(string key, T value) where T : IConvertible? { return GetLocalised(key, value); } /// /// Get all locale keys /// /// Generic collection of keys public IEnumerable GetLocaleKeys() { return _loadedLocales["en"].Value?.Keys ?? Enumerable.Empty(); } /// /// From the provided partial key, find all keys that start with text and choose a random match /// /// Key to match locale keys on /// Locale text public string GetRandomTextThatMatchesPartialKey(string partialKey) { var matchingKeys = GetLocaleKeys().Where(x => x.Contains(partialKey)); if (!matchingKeys.Any()) { logger.Warning($"No locale keys found for: {partialKey}"); return string.Empty; } return GetText(randomUtil.GetArrayValue(matchingKeys)); } public string GetLocalisedValue(string key) { // On the initial localised request, hydrate server locales if (!_serverLocalesHydrated) { HydrateServerLocales(); } // get loaded locales for set key if (!_loadedLocales.TryGetValue(_serverLocale, 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 = 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; } protected string GetLocalised(string key, object? args) { var rawLocalizedString = GetLocalisedValue(key); if (args == null) { return rawLocalizedString; } var typeProperties = args.GetType().GetProperties(); foreach (var propertyInfo in typeProperties) { var localizedName = $"{{{{{propertyInfo.GetJsonName()}}}}}"; if (rawLocalizedString.Contains(localizedName)) { rawLocalizedString = rawLocalizedString.Replace(localizedName, propertyInfo.GetValue(args)?.ToString() ?? string.Empty); } } return rawLocalizedString; } protected string GetLocalised(string key, T? value) where T : IConvertible? { var rawLocalizedString = GetLocalisedValue(key); return rawLocalizedString.Replace("%s", value?.ToString() ?? string.Empty); } // gets the localized string directly protected string GetLocalised(string key) { return GetLocalisedValue(key); } }