2

造轮子之多语言管理 - 饭勺oO

 7 months ago
source link: https://www.cnblogs.com/fanshaoO/p/17757078.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

多语言也是我们经常能用到的东西,asp.net core中默认支持了多语言,可以使用.resx资源文件来管理多语言配置。
但是在修改资源文件后,我们的应用服务无法及时更新,属实麻烦一些。我们可以通过扩展IStringLocalizer,实现我们想要的多语言配置方式,比如Json配置,PO 文件配置,EF数据库配置等等。
这里我们选用数据库配置的方式,直接查询数据库的多语言配置进行转换。

创建表实体#

多语言管理只需要两个表结构,一个是多语言国家表,一个是多语言资源表。两者是一对多关系。

namespace Wheel.Domain.Localization
{
    public class LocalizationCulture : IEntity<int>
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public virtual List<LocalizationResource> Resources { get; set; }
    }
}
namespace Wheel.Domain.Localization
{
    public class LocalizationResource : IEntity<int>
    {
        public int Id { get; set; }
        public string Key { get; set; }
        public string Value { get; set; }
        public virtual int CultureId { get; set; }
        public virtual LocalizationCulture Culture { get; set; }
    }
}

修改DbContext#

添加DbSet和配置表结构:

#region Localization
public DbSet<LocalizationCulture> Cultures { get; set; }
public DbSet<LocalizationResource> Resources { get; set; }
#endregion
protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);

    ConfigureIdentity(builder);
    ConfigureLocalization(builder);
    ConfigurePermissionGrants(builder);
}
void ConfigureLocalization(ModelBuilder builder)
{
    builder.Entity<LocalizationCulture>(b =>
    {
        b.Property(a => a.Id).ValueGeneratedOnAdd();
        b.ToTable("LocalizationCulture"); 
        b.Property(a => a.Name).HasMaxLength(32);
        b.HasMany(a => a.Resources);
    });
    builder.Entity<LocalizationResource>(b =>
    {
        b.Property(a => a.Id).ValueGeneratedOnAdd();
        b.ToTable("LocalizationResource");
        b.HasOne(a => a.Culture);
        b.HasIndex(a => a.CultureId);
        b.Property(a => a.Key).HasMaxLength(256);
        b.Property(a => a.Value).HasMaxLength(1024);
    });
}

然后进行数据库迁移即可生成数据库表结构。

实现EF多语言#

这里我们需要实现一下EFStringLocalizerFactory和EFStringLocalizer,使用EFStringLocalizerFactory来创建EFStringLocalizer。

namespace Wheel.Localization
{
    public class EFStringLocalizerFactory : IStringLocalizerFactory, ISingletonDependency
    {
        IServiceProvider _serviceProvider;
        public EFStringLocalizerFactory(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        public IStringLocalizer Create(Type resourceSource)
        {
            var scope = _serviceProvider.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<WheelDbContext>();
            var cahce = scope.ServiceProvider.GetRequiredService<IMemoryCache>();
            return new EFStringLocalizer(db, cahce);
        }

        public IStringLocalizer Create(string baseName, string location)
        {
            var scope = _serviceProvider.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<WheelDbContext>();
            var cahce = scope.ServiceProvider.GetRequiredService<IMemoryCache>();
            return new EFStringLocalizer(db, cahce);
        }
    }
}

namespace Wheel.Localization
{
    public class EFStringLocalizer : IStringLocalizer
    {
        private readonly WheelDbContext _db;
        private readonly IMemoryCache _memoryCache;
        public EFStringLocalizer(WheelDbContext db, IMemoryCache memoryCache)
        {
            _db = db;
            _memoryCache = memoryCache;
        }

        public LocalizedString this[string name]
        {
            get
            {
                var value = GetString(name);
                return new LocalizedString(name, value ?? name, resourceNotFound: value == null);
            }
        }

        public LocalizedString this[string name, params object[] arguments]
        {
            get
            {
                var format = GetString(name);
                var value = string.Format(format ?? name, arguments);
                return new LocalizedString(name, value, resourceNotFound: format == null);
            }
        }

        public IStringLocalizer WithCulture(CultureInfo culture)
        {
            CultureInfo.DefaultThreadCurrentCulture = culture;
            return new EFStringLocalizer(_db, _memoryCache);
        }

        public IEnumerable<LocalizedString> GetAllStrings(bool includeAncestorCultures)
        {
            return _db.Resources
                .Include(r => r.Culture)
                .Where(r => r.Culture.Name == CultureInfo.CurrentCulture.Name)
                .Select(r => new LocalizedString(r.Key, r.Value, r.Value == null));
        }

        private string? GetString(string name)
        {
            if (_memoryCache.TryGetValue<string>($"{CultureInfo.CurrentCulture.Name}:{name}", out var value))
            {
                return value;
            }
            else
            {
                value = _db.Resources
                .Include(r => r.Culture)
                .Where(r => r.Culture.Name == CultureInfo.CurrentCulture.Name)
                .FirstOrDefault(r => r.Key == name)?.Value;
                if (!string.IsNullOrWhiteSpace(value))
                {
                    _memoryCache.Set($"{CultureInfo.CurrentCulture.Name}:{name}", value, TimeSpan.FromMinutes(1));
                }
                return value;
            }
        }
    }

    public class EFStringLocalizer<T> : IStringLocalizer<T>
    {
        private readonly WheelDbContext _db;
        private readonly IMemoryCache _memoryCache;
        public EFStringLocalizer(WheelDbContext db, IMemoryCache memoryCache)
        {
            _db = db;
            _memoryCache = memoryCache;
        }

        public LocalizedString this[string name]
        {
            get
            {
                var value = GetString(name);
                return new LocalizedString(name, value ?? name, resourceNotFound: value == null);
            }
        }

        public LocalizedString this[string name, params object[] arguments]
        {
            get
            {
                var format = GetString(name);
                var value = string.Format(format ?? name, arguments);
                return new LocalizedString(name, value, resourceNotFound: format == null);
            }
        }

        public IStringLocalizer WithCulture(CultureInfo culture)
        {
            CultureInfo.DefaultThreadCurrentCulture = culture;
            return new EFStringLocalizer(_db, _memoryCache);
        }

        public IEnumerable<LocalizedString> GetAllStrings(bool includeAncestorCultures)
        {
            return _db.Resources
                .Include(r => r.Culture)
                .Where(r => r.Culture.Name == CultureInfo.CurrentCulture.Name)
                .Select(r => new LocalizedString(r.Key, r.Value, true));
        }

        private string? GetString(string name)
        {
            if (_memoryCache.TryGetValue<string>($"{CultureInfo.CurrentCulture.Name}:{name}", out var value))
            {
                return value;
            }
            else
            {
                value = _db.Resources
                .Include(r => r.Culture)
                .Where(r => r.Culture.Name == CultureInfo.CurrentCulture.Name)
                .FirstOrDefault(r => r.Key == name)?.Value;
                if (!string.IsNullOrWhiteSpace(value))
                {
                    _memoryCache.Set($"{CultureInfo.CurrentCulture.Name}:{name}", value, TimeSpan.FromMinutes(1));
                }
                return value;
            }
        }
    }
}

这里的GetString方法,我们先通过缓存查询多语言内容,若查询不到再进数据库查询,减少数据库的并发量。
多语言国家编码直接使用CultureInfo.CurrentCulture.Name获取。无需传参配置。

启用多语言#

再Program中添加多语言代码:

builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");

app.UseRequestLocalization(new RequestLocalizationOptions
{
    ApplyCurrentCultureToResponseHeaders = true,
    DefaultRequestCulture = new RequestCulture("zh-CN"),
    SupportedCultures = new List<CultureInfo>
            {
                new CultureInfo("en"),
                new CultureInfo("zh-CN"),
            },
    SupportedUICultures = new List<CultureInfo>
            {
                new CultureInfo("en"),
                new CultureInfo("zh-CN"),
            }
});

这里配置默认语言是中文,同时支持英文和中文两种。

多语言管理API实现#

接下来我们实现LocalizationManage
ILocalizationManageAppService:

namespace Wheel.Services.LocalizationManage
{
    public interface ILocalizationManageAppService : ITransientDependency
    {
        Task<R<LocalizationCultureDto>> GetLocalizationCultureAsync(int id);
        Task<Page<LocalizationCultureDto>> GetLocalizationCulturePageListAsync(PageRequest input);
        Task<R<LocalizationCultureDto>> CreateLocalizationCultureAsync(CreateLocalizationCultureDto input);
        Task<R> DeleteLocalizationCultureAsync(int id);
        Task<R<LocalizationResourceDto>> CreateLocalizationResourceAsync(CreateLocalizationResourceDto input);
        Task<R> UpdateLocalizationResourceAsync(UpdateLocalizationResourceDto input);
        Task<R> DeleteLocalizationResourceAsync(int id);
    }
}

LocalizationManageAppService:


namespace Wheel.Services.LocalizationManage
{
    /// <summary>
    /// 多语言管理
    /// </summary>
    public class LocalizationManageAppService : WheelServiceBase, ILocalizationManageAppService
    {
        private readonly IBasicRepository<LocalizationCulture, int> _localizationCultureRepository;
        private readonly IBasicRepository<LocalizationResource, int> _localizationResourceRepository;

        public LocalizationManageAppService(IBasicRepository<LocalizationCulture, int> localizationCultureRepository, IBasicRepository<LocalizationResource, int> localizationResourceRepository)
        {
            _localizationCultureRepository = localizationCultureRepository;
            _localizationResourceRepository = localizationResourceRepository;
        }
        /// <summary>
        /// 获取地区多语言详情
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public async Task<R<LocalizationCultureDto>> GetLocalizationCultureAsync(int id)
        {
            var entity = await _localizationCultureRepository.FindAsync(id);

            return new R<LocalizationCultureDto>(Mapper.Map<LocalizationCultureDto>(entity));
        }
        /// <summary>
        /// 分页获取地区多语言列表
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public async Task<Page<LocalizationCultureDto>> GetLocalizationCulturePageListAsync(PageRequest input)
        {
            var (entities, total) = await _localizationCultureRepository
                .GetPageListAsync(a => true,
                (input.PageIndex - 1) * input.PageSize,
                input.PageSize,
                propertySelectors: a => a.Resources
                );

            return new Page<LocalizationCultureDto>(Mapper.Map<List<LocalizationCultureDto>>(entities), total);
        }
        /// <summary>
        /// 创建地区多语言
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public async Task<R<LocalizationCultureDto>> CreateLocalizationCultureAsync(CreateLocalizationCultureDto input)
        {
            var entity = Mapper.Map<LocalizationCulture>(input);
            entity = await _localizationCultureRepository.InsertAsync(entity);
            await UnitOfWork.SaveChangesAsync();
            return new R<LocalizationCultureDto>(Mapper.Map<LocalizationCultureDto>(entity));
        }
        /// <summary>
        /// 删除地区多语言
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public async Task<R> DeleteLocalizationCultureAsync(int id)
        {
            await _localizationCultureRepository.DeleteAsync(id);
            await UnitOfWork.SaveChangesAsync();
            return new R();
        }
        /// <summary>
        /// 创建多语言资源
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public async Task<R<LocalizationResourceDto>> CreateLocalizationResourceAsync(CreateLocalizationResourceDto input)
        {
            var entity = Mapper.Map<LocalizationResource>(input);
            entity = await _localizationResourceRepository.InsertAsync(entity);
            await UnitOfWork.SaveChangesAsync();
            return new R<LocalizationResourceDto>(Mapper.Map<LocalizationResourceDto>(entity));
        }
        /// <summary>
        /// 修改多语言资源
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public async Task<R> UpdateLocalizationResourceAsync(UpdateLocalizationResourceDto input)
        {
            await _localizationResourceRepository.UpdateAsync(a => a.Id == input.Id,
                a => a.SetProperty(b => b.Key, b => input.Key).SetProperty(b => b.Value, b => input.Value));
            await UnitOfWork.SaveChangesAsync();
            return new R();
        }
        /// <summary>
        /// 删除多语言资源
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public async Task<R> DeleteLocalizationResourceAsync(int id)
        {
            await _localizationResourceRepository.DeleteAsync(id);
            await UnitOfWork.SaveChangesAsync();
            return new R();
        }
    }
}

这里包含了多语言的CURD的实现
LocalizationManageController:

namespace Wheel.Controllers
{
    /// <summary>
    /// 多语言管理
    /// </summary>
    [Route("api/[controller]")]
    [ApiController]
    public class LocalizationManageController : WheelControllerBase
    {
        private readonly ILocalizationManageAppService _localizationManageAppService;

        public LocalizationManageController(ILocalizationManageAppService localizationManageAppService)
        {
            _localizationManageAppService = localizationManageAppService;
        }

        /// <summary>
        /// 获取地区多语言详情
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        [HttpGet("Culture/{id}")]
        public async Task<R<LocalizationCultureDto>> GetCulture(int id)
        {
            return await _localizationManageAppService.GetLocalizationCultureAsync(id);
        }
        /// <summary>
        /// 创建地区多语言
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        [HttpPost("Culture")]
        public async Task<R<LocalizationCultureDto>> CreateCulture(CreateLocalizationCultureDto input)
        {
            return await _localizationManageAppService.CreateLocalizationCultureAsync(input);
        }
        /// <summary>
        /// 删除地区多语言
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        [HttpDelete("Culture/{id}")]
        public async Task<R> DeleteCulture(int id)
        {
            return await _localizationManageAppService.DeleteLocalizationCultureAsync(id);
        }
        /// <summary>
        /// 分页获取地区多语言列表
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        [HttpGet("Culture")]
        public async Task<Page<LocalizationCultureDto>> GetCulturePageList([FromQuery]PageRequest input)
        {
            return await _localizationManageAppService.GetLocalizationCulturePageListAsync(input);
        }
        /// <summary>
        /// 创建多语言资源
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        [HttpPost("Resource")]
        public async Task<R<LocalizationResourceDto>> CreateResource(CreateLocalizationResourceDto input)
        {
            return await _localizationManageAppService.CreateLocalizationResourceAsync(input);
        }
        /// <summary>
        /// 修改多语言资源
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        [HttpPut("Resource")]
        public async Task<R> UpdateResource(UpdateLocalizationResourceDto input)
        {
            return await _localizationManageAppService.UpdateLocalizationResourceAsync(input);
        }
        /// <summary>
        /// 删除多语言资源
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        [HttpDelete("Resource/{id}")]
        public async Task<R> DeleteResource(int id)
        {
            return await _localizationManageAppService.DeleteLocalizationResourceAsync(id);
        }
        /// <summary>
        /// 获取多语言资源列表
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        [HttpGet("Resources")]
        [AllowAnonymous]
        public Task<R<Dictionary<string, string>>> GetResources()
        {
            var resources = L.GetAllStrings().ToDictionary(a=>a.Name, a=>a.Value);
            return Task.FromResult(new R<Dictionary<string, string>>(resources));
        }
    }
}

在控制器额外添加一个匿名访问的API,GetResources()用于客户端集成多语言配置。L是IStringLocalizer实例。

启用服务测试一下。

image.png


image.png


可以看到成功获取英文和中文的多语言列表。

就这样我们完成多语言管理的实现。

轮子仓库地址https://github.com/Wheel-Framework/Wheel
欢迎进群催更。

image.png

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK