239 lines
9.0 KiB
C#
239 lines
9.0 KiB
C#
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 readonly ILocalizationService _localizationService;
|
||
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, ILocalizationService localizationService)
|
||
{
|
||
_jsRuntime = jsRuntime;
|
||
_httpClient = httpClient;
|
||
_logger = logger;
|
||
_localStorage = localStorage;
|
||
_localizationService = localizationService;
|
||
}
|
||
|
||
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);
|
||
|
||
// ensure thread cultures and notify localization service
|
||
try
|
||
{
|
||
var ci = new CultureInfo(_currentCulture);
|
||
CultureInfo.DefaultThreadCurrentCulture = ci;
|
||
CultureInfo.DefaultThreadCurrentUICulture = ci;
|
||
_localizationService.SetLanguage(ci);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogDebug(ex, "Failed to set culture after initialize: {Culture}", _currentCulture);
|
||
}
|
||
|
||
_isInitialized = true;
|
||
LanguageChanged?.Invoke(this, _currentCulture);
|
||
}
|
||
|
||
public async Task SetCultureAsync(string cultureShortOrFull)
|
||
{
|
||
var full = MapToFullCulture(cultureShortOrFull);
|
||
_currentCulture = full;
|
||
|
||
await LoadCultureAsync(_currentCulture);
|
||
|
||
try
|
||
{
|
||
var ci = new CultureInfo(_currentCulture);
|
||
CultureInfo.DefaultThreadCurrentCulture = ci;
|
||
CultureInfo.DefaultThreadCurrentUICulture = ci;
|
||
_localizationService.SetLanguage(ci);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogDebug(ex, "Failed to set culture in SetCultureAsync: {Culture}", _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);
|
||
|
||
// ensure thread cultures and notify localization service when using cached data
|
||
try
|
||
{
|
||
var ci = new CultureInfo(cultureFull);
|
||
CultureInfo.DefaultThreadCurrentCulture = ci;
|
||
CultureInfo.DefaultThreadCurrentUICulture = ci;
|
||
_localizationService.SetLanguage(ci);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogDebug(ex, "Failed to set culture after loading from cache: {Culture}", 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);
|
||
|
||
// ensure thread cultures and notify localization service after fetching
|
||
try
|
||
{
|
||
var ci = new CultureInfo(cultureFull);
|
||
CultureInfo.DefaultThreadCurrentCulture = ci;
|
||
CultureInfo.DefaultThreadCurrentUICulture = ci;
|
||
_localizationService.SetLanguage(ci);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogDebug(ex, "Failed to set culture after fetching: {Culture}", cultureFull);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "Failed to load localization file for {Culture}", cultureFull);
|
||
}
|
||
finally
|
||
{
|
||
lock (_loadingCultures)
|
||
{
|
||
_loadingCultures.Remove(cultureFull);
|
||
}
|
||
}
|
||
|
||
// Notify listeners that the culture has been loaded
|
||
LanguageChanged?.Invoke(this, 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;
|
||
}
|
||
}
|
||
} |