多語言也是我們經常能用到的東西,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; }
}
}
新增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);
});
}
這裡我們需要實現一下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"),
}
});
接下來我們實現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範例。
啟用服務測試一下。
可以看到成功獲取英文和中文的多語言列表。
就這樣我們完成多語言管理的實現。
輪子倉庫地址https://github.com/Wheel-Framework/Wheel
歡迎進群催更。