Custom Implementation of Multilingual Processing in ABP

2023年7月27日 40点热度 1人点赞 0条评论
内容目录

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.

RedisResourceOptions can 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");
});

痴者工良

高级程序员劝退师

文章评论