In ABP, by default, only local JSON language handling is available. However, for business purposes, we may have many customization needs. This article introduces how to implement multi-language handling based on Redis, fetching language information from Redis.
ABP Official Documentation: https://docs.abp.io/en/abp/latest/Localization
ABP configures multi-language as follows:
services.Configure<AbpLocalizationOptions>(options =>
{
options.Resources
.Add<TestResource>("en") // Define the resource by "en" default culture
.AddVirtualJson("/Localization/Resources/Test") // Add strings from virtual json files
.AddBaseTypes(typeof(AbpValidationResource)); // Inherit from an existing resource
});
Based on this, let's implement direct multi-language handling.
First, we need to implement the ILocalizationResourceContributor interface to provide queries for multi-language strings.
The author uses FreeRedis to implement local caching and dynamic cache updates.
In Redis, we set up the key format: language:{language}; for example, language:zh-CN, and then use the hash type for the key to store string keywords and their corresponding language translations.
RedisResourceOptionscan be anything, such as the Redis key prefix.
public class RedisLocalizationResource : ILocalizationResourceContributor
{
private readonly RedisResourceOptions _options;
private readonly RedisClient.DatabaseHook _db;
private readonly ConcurrentDictionary<string, string> _languages = new ConcurrentDictionary<string, string>();
private readonly string _keyPrefix;
internal RedisLocalizationResource(RedisClient.DatabaseHook db, RedisResourceOptions options)
{
_options = options;
_keyPrefix = options.KeyPrefix;
_db = db;
}
/// <summary>
/// Indicates dynamic retrieval of language information
/// </summary>
public bool IsDynamic => true;
/// <summary>
/// Fills the dictionary; used only when IsDynamic = false
/// </summary>
public void Fill(string cultureName, Dictionary<string, LocalizedString> dictionary)
{
var hash = _db.HGetAll($"{_keyPrefix}:{cultureName}");
foreach (var item in hash)
{
dictionary.Add(item.Key, new LocalizedString(item.Key, item.Value));
}
}
/// <summary>
/// Fills the dictionary asynchronously; used only when IsDynamic = false
/// </summary>
public async Task FillAsync(string cultureName, Dictionary<string, LocalizedString> dictionary)
{
var hash = await _db.HGetAllAsync($"{_keyPrefix}:{cultureName}");
foreach (var item in hash)
{
dictionary.Add(item.Key, new LocalizedString(item.Key, item.Value));
}
}
/// <summary>
/// Retrieves localized strings
/// </summary>
/// <param name="cultureName">Language name</param>
/// <param name="name">Key</param>
/// <returns></returns>
public LocalizedString GetOrNull(string cultureName, string name)
{
var key = GetLanguageKey(cultureName);
if (key == default) return null!;
var value = _db.HGet(key, name);
if (!string.IsNullOrEmpty(value))
return new LocalizedString(name, value);
return new LocalizedString(name, name);
}
/// <summary>
/// Supported languages
/// </summary>
/// <returns></returns>
public async Task<IEnumerable<string>> GetSupportedCulturesAsync()
{
await Task.CompletedTask;
var languageKeys = new List<string>();
List<string> languages = new List<string>();
foreach (var keys in _db.Scan(_keyPrefix, 20, null))
{
languageKeys.AddRange(keys);
}
foreach (var key in languageKeys)
{
var language = key.Split(":").LastOrDefault();
if (language == null) continue;
languages.Add(language);
}
return languages;
}
/// <summary>
///
/// </summary>
/// <param name="context"></param>
public void Initialize(LocalizationResourceInitializationContext context)
{
_languages.Clear();
var languageKeys = new List<string>();
foreach (var keys in _db.Scan(_keyPrefix, 20, null))
{
languageKeys.AddRange(keys);
}
foreach (var key in languageKeys)
{
var language = key.Split(":").LastOrDefault();
if (language == null) continue;
_languages.TryAdd(language, key);
}
}
private DateTime _currentTime = DateTime.Now;
private string? GetLanguageKey(string cultureName)
{
var key = $"{_keyPrefix}:{cultureName}";
// The key does not exist locally
if (!_languages.ContainsKey(cultureName))
{
// Only check the key once every 5 seconds
if (DateTime.Now - _currentTime > TimeSpan.FromSeconds(5))
{
_currentTime = DateTime.Now;
var exist = _db.Exists(key);
if (!exist) return default;
_languages.TryAdd(cultureName, key);
}
else return null;
}
return key;
}
}
Next, we write an extension to inject this custom language service.
/// <summary>
/// Dynamic multi-language extension configuration
/// </summary>
public static class LocalizationExtensions
{
public static TLocalizationResource AddRedisResource<TLocalizationResource>(
[NotNull] this TLocalizationResource localizationResource,
int dbIndex = 0,
string keyPrefix = "language")
where TLocalizationResource : LocalizationResourceBase
{
ArgumentNullException.ThrowIfNull(keyPrefix);
keyPrefix = keyPrefix.ToLower();
var db = RedisHelper.Client.GetDatabase(dbIndex);
// Here we use FreeRedis to dynamically update local caching, providing responsiveness and performance
db.UseClientSideCaching(new ClientSideCachingOptions
{
// Client cache capacity
Capacity = 20,
// Filter
KeyFilter = key => key.StartsWith(keyPrefix),
// Check for long-unused cache
CheckExpired = (key, dt) => DateTime.Now.Subtract(dt) > TimeSpan.FromSeconds(5),
});
localizationResource.Contributors.Add(new RedisLocalizationResource(db, new RedisResourceOptions(dbIndex, keyPrefix)));
return localizationResource;
}
}
Then you can use it directly:
Configure<AbpLocalizationOptions>(options =>
{
options.Resources
// Set default language
.Add<TestResource>("en")
// Configure Redis information
.AddRedisResource(dbIndex: 0, keyPrefix: "language");
});
文章评论