chore
This commit is contained in:
@@ -29,7 +29,7 @@ builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.
|
|||||||
|
|
||||||
// ע<><D7A2> LocalizationProvider (<28><><EFBFBD><EFBFBD> WASM)
|
// ע<><D7A2> LocalizationProvider (<28><><EFBFBD><EFBFBD> WASM)
|
||||||
// Use Scoped lifetime because LocalizationProvider depends on IJSRuntime (scoped) and IHttpClient etc.
|
// Use Scoped lifetime because LocalizationProvider depends on IJSRuntime (scoped) and IHttpClient etc.
|
||||||
builder.Services.AddScoped<ILocalizationProvider, LocalizationProvider>();
|
builder.Services.AddScoped<ILocalizationProvider, WasmLocalizationProvider>();
|
||||||
// ע<><D7A2> ILocalizationService <20><><EFBFBD><EFBFBD>ͬ<EFBFBD><CDAC> Culture <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>䴫<EFBFBD><E4B4AB>
|
// ע<><D7A2> ILocalizationService <20><><EFBFBD><EFBFBD>ͬ<EFBFBD><CDAC> Culture <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>䴫<EFBFBD><E4B4AB>
|
||||||
builder.Services.AddScoped<ILocalizationService, LocalizationService>();
|
builder.Services.AddScoped<ILocalizationService, LocalizationService>();
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ namespace Atomx.Admin.Client.Services
|
|||||||
|
|
||||||
private const string CookieName = "atomx.culture";
|
private const string CookieName = "atomx.culture";
|
||||||
|
|
||||||
public LocalizationProvider(IServiceProvider sp, IHttpClientFactory? httpClientFactory, IJSRuntime? jsRuntime, ILogger<LocalizationProvider> logger, ILocalizationService localizationService)
|
public LocalizationProvider(IServiceProvider sp, ILogger<LocalizationProvider> logger, IHttpClientFactory? httpClientFactory, IJSRuntime? jsRuntime, ILocalizationService localizationService)
|
||||||
{
|
{
|
||||||
_sp = sp;
|
_sp = sp;
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
using Blazored.LocalStorage;
|
||||||
|
|
||||||
|
namespace Atomx.Admin.Client.Services
|
||||||
|
{
|
||||||
|
public class WasmLocalizationProvider : ILocalizationProvider
|
||||||
|
{
|
||||||
|
private readonly IJSRuntime _jsRuntime;
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly ILogger<WasmLocalizationProvider> _logger;
|
||||||
|
private readonly ILocalStorageService _localStorage;
|
||||||
|
private static readonly ConcurrentDictionary<string, Dictionary<string, string>> _cache = new();
|
||||||
|
private readonly HashSet<string> _loadingCultures = new();
|
||||||
|
private string _currentCulture = "zh-Hans";
|
||||||
|
private bool _isInitialized = false;
|
||||||
|
|
||||||
|
private const string LocalizationStorageKey = "Localization_{0}";
|
||||||
|
private const string LocalizationVersionKey = "LocalizationVersion_{0}";
|
||||||
|
|
||||||
|
public WasmLocalizationProvider(IJSRuntime jsRuntime, HttpClient httpClient, ILogger<WasmLocalizationProvider> logger, ILocalStorageService localStorage)
|
||||||
|
{
|
||||||
|
_jsRuntime = jsRuntime;
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_logger = logger;
|
||||||
|
_localStorage = localStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CurrentCulture => _currentCulture;
|
||||||
|
public bool IsInitialized => _isInitialized;
|
||||||
|
|
||||||
|
public event EventHandler<string>? LanguageChanged;
|
||||||
|
|
||||||
|
public string? GetString(string key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key)) return null;
|
||||||
|
|
||||||
|
if (_cache.TryGetValue(_currentCulture, out var dict) && dict.TryGetValue(key, out var val))
|
||||||
|
{
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
if (_isInitialized) return;
|
||||||
|
|
||||||
|
await LoadCultureAsync(_currentCulture);
|
||||||
|
_isInitialized = true;
|
||||||
|
LanguageChanged?.Invoke(this, _currentCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetCultureAsync(string cultureShortOrFull)
|
||||||
|
{
|
||||||
|
_currentCulture = MapToFullCulture(cultureShortOrFull);
|
||||||
|
await LoadCultureAsync(_currentCulture);
|
||||||
|
LanguageChanged?.Invoke(this, _currentCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadCultureAsync(string culture)
|
||||||
|
{
|
||||||
|
var cultureFull = MapToFullCulture(culture);
|
||||||
|
|
||||||
|
// Step 1: Check in-memory cache
|
||||||
|
if (_cache.ContainsKey(cultureFull)) return;
|
||||||
|
|
||||||
|
lock (_loadingCultures)
|
||||||
|
{
|
||||||
|
if (_loadingCultures.Contains(cultureFull))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Culture {Culture} is already being loaded.", cultureFull);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_loadingCultures.Add(cultureFull);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Step 2: Check local storage for cached data
|
||||||
|
var localDataKey = string.Format(LocalizationStorageKey, cultureFull);
|
||||||
|
var localVersionKey = string.Format(LocalizationVersionKey, cultureFull);
|
||||||
|
|
||||||
|
var cachedVersion = await _localStorage.GetItemAsync<string>(localVersionKey);
|
||||||
|
var cachedData = await _localStorage.GetItemAsync<Dictionary<string, string>>(localDataKey);
|
||||||
|
|
||||||
|
if (cachedData != null)
|
||||||
|
{
|
||||||
|
_cache[cultureFull] = cachedData;
|
||||||
|
_logger.LogInformation("Loaded localization for {Culture} from local storage.", cultureFull);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Validate version with server
|
||||||
|
var versionUrl = $"api/localeresource/version/{cultureFull}";
|
||||||
|
var serverVersion = await _httpClient.GetStringAsync(versionUrl);
|
||||||
|
|
||||||
|
if (cachedVersion == serverVersion && cachedData != null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Localization data for {Culture} is up-to-date.", cultureFull);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Fetch from server if version mismatch or no cached data
|
||||||
|
var url = $"/localization/{cultureFull}.json";
|
||||||
|
var json = await _httpClient.GetStringAsync(url);
|
||||||
|
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new Dictionary<string, string>();
|
||||||
|
_cache[cultureFull] = dict;
|
||||||
|
|
||||||
|
// Step 5: Update local storage
|
||||||
|
await _localStorage.SetItemAsync(localVersionKey, serverVersion);
|
||||||
|
await _localStorage.SetItemAsync(localDataKey, dict);
|
||||||
|
|
||||||
|
_logger.LogInformation("Loaded localization file for {Culture} from server and updated local storage.", cultureFull);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to load localization file for {Culture}", cultureFull);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
lock (_loadingCultures)
|
||||||
|
{
|
||||||
|
_loadingCultures.Remove(cultureFull);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string MapToFullCulture(string culture)
|
||||||
|
{
|
||||||
|
return culture switch
|
||||||
|
{
|
||||||
|
"zh" => "zh-Hans",
|
||||||
|
"en" => "en-US",
|
||||||
|
_ => culture
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <20>ӷ<EFBFBD><D3B7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD>ػ<EFBFBD><D8BB><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="culture"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private Dictionary<string, string> FetchFromServer(string culture)
|
||||||
|
{
|
||||||
|
var url = $"/localization/{culture}.json";
|
||||||
|
var json = _httpClient.GetStringAsync(url).Result;
|
||||||
|
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new Dictionary<string, string>();
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <20><><EFBFBD>鱾<EFBFBD>ػ<EFBFBD><D8BB>汾<EFBFBD>Ƿ<EFBFBD><C7B7><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="versionKey"></param>
|
||||||
|
/// <param name="culture"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private async Task<bool> CheckVersionAsync(string versionKey, string culture)
|
||||||
|
{
|
||||||
|
var cachedVersion = await _localStorage.GetItemAsync<string>(versionKey);
|
||||||
|
if(string.IsNullOrEmpty(cachedVersion))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var versionUrl = $"api/localeresource/version/{culture}";
|
||||||
|
var serverVersion = await _httpClient.GetStringAsync(versionUrl);
|
||||||
|
|
||||||
|
if (cachedVersion != serverVersion)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,8 +66,8 @@ builder.Services.AddHttpContextAccessor();
|
|||||||
// HttpClient <20><><EFBFBD><EFBFBD><EFBFBD>ݷ<EFBFBD><DDB7><EFBFBD>
|
// HttpClient <20><><EFBFBD><EFBFBD><EFBFBD>ݷ<EFBFBD><DDB7><EFBFBD>
|
||||||
builder.Services.AddHttpClientApiService(builder.Configuration["WebApi:ServerUrl"] ?? "http://localhost");
|
builder.Services.AddHttpClientApiService(builder.Configuration["WebApi:ServerUrl"] ?? "http://localhost");
|
||||||
|
|
||||||
// ע<><EFBFBD> LocalizationProvider <20><> Server ģʽ<EFBFBD><EFBFBD>ʹ<EFBFBD><EFBFBD>
|
// ע<>뱾<EFBFBD>ػ<EFBFBD><EFBFBD>ṩ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
builder.Services.AddScoped<ILocalizationProvider, LocalizationProvider>();
|
builder.Services.AddScoped<ILocalizationProvider, ServerLocalizationProvider>();
|
||||||
builder.Services.AddScoped<ILocalizationService, LocalizationService>();
|
builder.Services.AddScoped<ILocalizationService, LocalizationService>();
|
||||||
builder.Services.AddTransient(typeof(IStringLocalizer<>), typeof(JsonStringLocalizer<>));
|
builder.Services.AddTransient(typeof(IStringLocalizer<>), typeof(JsonStringLocalizer<>));
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ builder.Services.AddStackExchangeRedisCache(options =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Hangfire <20><><EFBFBD><EFBFBD>
|
// Hangfire <20><><EFBFBD><EFBFBD>
|
||||||
var hangfireConnection = builder.Configuration.GetConnectionString("HangfireConnection")
|
var hangfireConnection = builder.Configuration.GetConnectionString("HangfireConnection")
|
||||||
?? throw new InvalidOperationException("Connection string 'HangfireConnection' not found.");
|
?? throw new InvalidOperationException("Connection string 'HangfireConnection' not found.");
|
||||||
|
|
||||||
// <20><><EFBFBD><EFBFBD>hangfire<72><65><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>PostgreSQL<51>洢
|
// <20><><EFBFBD><EFBFBD>hangfire<72><65><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>PostgreSQL<51>洢
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
using Atomx.Admin.Client.Services;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Atomx.Admin.Services
|
||||||
|
{
|
||||||
|
public class ServerLocalizationProvider : ILocalizationProvider
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly ILogger<ServerLocalizationProvider> _logger;
|
||||||
|
private static readonly ConcurrentDictionary<string, Dictionary<string, string>> _cache = new();
|
||||||
|
private string _currentCulture = "zh-Hans";
|
||||||
|
|
||||||
|
public ServerLocalizationProvider(IServiceProvider serviceProvider, ILogger<ServerLocalizationProvider> logger)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CurrentCulture => _currentCulture;
|
||||||
|
|
||||||
|
public event EventHandler<string>? LanguageChanged;
|
||||||
|
|
||||||
|
public string? GetString(string key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key)) return null;
|
||||||
|
|
||||||
|
if (_cache.TryGetValue(_currentCulture, out var dict) && dict.TryGetValue(key, out var val))
|
||||||
|
{
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
await LoadCultureAsync(_currentCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetCultureAsync(string cultureShortOrFull)
|
||||||
|
{
|
||||||
|
_currentCulture = MapToFullCulture(cultureShortOrFull);
|
||||||
|
await LoadCultureAsync(_currentCulture);
|
||||||
|
LanguageChanged?.Invoke(this, _currentCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task LoadCultureAsync(string culture)
|
||||||
|
{
|
||||||
|
var cultureFull = MapToFullCulture(culture);
|
||||||
|
if (_cache.ContainsKey(cultureFull)) return Task.CompletedTask;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var env = _serviceProvider.GetService(typeof(Microsoft.AspNetCore.Hosting.IWebHostEnvironment)) as Microsoft.AspNetCore.Hosting.IWebHostEnvironment;
|
||||||
|
if (env != null)
|
||||||
|
{
|
||||||
|
var path = Path.Combine(env.WebRootPath, "localization", cultureFull + ".json");
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(path);
|
||||||
|
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new Dictionary<string, string>();
|
||||||
|
_cache[cultureFull] = dict;
|
||||||
|
_logger.LogInformation("Loaded localization file for {Culture} from {Path}", cultureFull, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to load localization file for {Culture}", cultureFull);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string MapToFullCulture(string culture)
|
||||||
|
{
|
||||||
|
return culture switch
|
||||||
|
{
|
||||||
|
"zh" => "zh-Hans",
|
||||||
|
"en" => "en-US",
|
||||||
|
_ => culture
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
using Hangfire;
|
using Atomx.Utils.Json;
|
||||||
|
using Hangfire;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Atomx.Core.Jos
|
namespace Atomx.Core.Jos
|
||||||
{
|
{
|
||||||
@@ -20,16 +22,56 @@ namespace Atomx.Core.Jos
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||||
[DisableConcurrentExecution(60)]
|
[DisableConcurrentExecution(60)]
|
||||||
public void Execute(string path)
|
public async Task ExecuteAsync(string path, string culture, string data)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("LocalizationJob executed at: {time}", DateTimeOffset.Now);
|
try
|
||||||
// 在这里添加多语言本地化的具体任务逻辑
|
{
|
||||||
}
|
var translations = data.FromJson<Dictionary<string, string>>();
|
||||||
|
|
||||||
public void ResetCache()
|
var fileName = $"{culture}.json";
|
||||||
{
|
var filePath = Path.Combine(path, fileName);
|
||||||
_logger.LogInformation("LocalizationJob ResetCache executed at: {time}", DateTimeOffset.Now);
|
if (!Directory.Exists(filePath))
|
||||||
// 在这里添加重置缓存的具体逻辑
|
{
|
||||||
|
Directory.CreateDirectory(filePath);
|
||||||
|
_logger.LogInformation("Created Resources directory: {Path}", filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = await File.ReadAllTextAsync(filePath);
|
||||||
|
var fileData = JsonSerializer.Deserialize<Dictionary<string, string>>(json, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
});
|
||||||
|
if (fileData == null)
|
||||||
|
{
|
||||||
|
fileData = new Dictionary<string, string>();
|
||||||
|
}
|
||||||
|
foreach (var item in translations)
|
||||||
|
{
|
||||||
|
if (fileData.ContainsKey(item.Key))
|
||||||
|
{
|
||||||
|
fileData[item.Key] = item.Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fileData.Add(item.Key, item.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var options = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
WriteIndented = true
|
||||||
|
};
|
||||||
|
json = JsonSerializer.Serialize(fileData, options);
|
||||||
|
await File.WriteAllTextAsync(filePath, json);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_logger.LogInformation("Saved localization file for culture: {Culture} with {Count} translations",
|
||||||
|
culture, translations.Count);
|
||||||
|
}
|
||||||
|
catch(Exception ex) {
|
||||||
|
_logger.LogError(ex, "Error saving localization file for culture: {Culture}", culture);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ namespace Atomx.Core.Jos
|
|||||||
{
|
{
|
||||||
public partial interface IBackgroundJobsService
|
public partial interface IBackgroundJobsService
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 更新本地化文件
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path"></param>
|
||||||
|
/// <param name="culture"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
string UpdateLocalizationFile(string path, string culture, string data);
|
||||||
string SendSMSVerificationCode(string phoneNumber, string code, TimeSpan validDuration);
|
string SendSMSVerificationCode(string phoneNumber, string code, TimeSpan validDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,6 +31,19 @@ namespace Atomx.Core.Jos
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新本地化文件
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path"></param>
|
||||||
|
/// <param name="culture"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string UpdateLocalizationFile(string path, string culture, string data)
|
||||||
|
{
|
||||||
|
var jobId = _backgroundJobClient.Enqueue<LocalizationJob>(job => job.ExecuteAsync(path, culture, data));
|
||||||
|
return jobId;
|
||||||
|
}
|
||||||
|
|
||||||
public string SendSMSVerificationCode(string phoneNumber, string code, TimeSpan validDuration)
|
public string SendSMSVerificationCode(string phoneNumber, string code, TimeSpan validDuration)
|
||||||
{
|
{
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{
|
{
|
||||||
public partial interface ICacheService
|
public partial interface ICacheService
|
||||||
{
|
{
|
||||||
Task LoadLocale(string Culture);
|
//Task LoadLocale(string Culture);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user