chore fix
This commit is contained in:
141
Atomx.Admin/Atomx.Admin.Client/Services/JsonStringLocalizer.cs
Normal file
141
Atomx.Admin/Atomx.Admin.Client/Services/JsonStringLocalizer.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
using Microsoft.Extensions.Localization;
|
||||
using System.Globalization;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Atomx.Admin.Client.Services
|
||||
{
|
||||
public class JsonStringLocalizer : IStringLocalizer
|
||||
{
|
||||
private readonly string _resourcesPath;
|
||||
private readonly Dictionary<string, Dictionary<string, string>> _resourcesCache = new();
|
||||
private readonly object _lock = new();
|
||||
private readonly HttpClient? _httpClient;
|
||||
|
||||
// resourcesPath 应为相对于站点根的“目录”名称,例如 "Localization"
|
||||
// 在 Blazor WebAssembly 场景下,会使用注入的 HttpClient 从 wwwroot/Localization/{culture}.json 获取资源
|
||||
public JsonStringLocalizer(string resourcesPath, HttpClient? httpClient = null)
|
||||
{
|
||||
_resourcesPath = (resourcesPath ?? "Localization").Trim('/'); // 规范化
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
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 IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
|
||||
{
|
||||
var culture = CultureInfo.CurrentUICulture.Name;
|
||||
var resources = LoadResources(culture);
|
||||
return resources.Select(r => new LocalizedString(r.Key, r.Value, false));
|
||||
}
|
||||
|
||||
private string? GetString(string name)
|
||||
{
|
||||
var culture = CultureInfo.CurrentUICulture.Name;
|
||||
var resources = LoadResources(culture);
|
||||
|
||||
// 尝试当前文化
|
||||
if (resources.TryGetValue(name, out var value))
|
||||
return value;
|
||||
|
||||
// 如果还找不到,尝试英文作为后备
|
||||
if (culture != "en-US")
|
||||
{
|
||||
var enResources = LoadResources("en-US");
|
||||
if (enResources.TryGetValue(name, out var enValue))
|
||||
return enValue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Dictionary<string, string> LoadResources(string culture)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_resourcesCache.TryGetValue(culture, out var cachedResources))
|
||||
return cachedResources;
|
||||
|
||||
var resources = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var fileName = $"{culture}.json";
|
||||
|
||||
// 在浏览器(WASM)环境下,通过 HttpClient 从静态资源目录获取
|
||||
if (OperatingSystem.IsBrowser() && _httpClient != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 构造相对 URL,例如 "Localization/zh-Hans.json"
|
||||
var relativeUrl = $"{_resourcesPath}/{fileName}";
|
||||
var json = _httpClient.GetStringAsync(relativeUrl).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
var jsonResources = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||
if (jsonResources != null)
|
||||
{
|
||||
foreach (var item in jsonResources)
|
||||
{
|
||||
resources[item.Key] = item.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error loading localization file {fileName} via HttpClient: {ex.Message}");
|
||||
}
|
||||
|
||||
_resourcesCache[culture] = resources;
|
||||
return resources;
|
||||
}
|
||||
|
||||
// 非浏览器(例如 Server 或在 prerender 阶段)尝试从文件系统读取。
|
||||
// 尝试几种可能的路径:基路径为 AppContext.BaseDirectory 或当前工作目录,或直接使用传入的路径。
|
||||
try
|
||||
{
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(AppContext.BaseDirectory, _resourcesPath, fileName),
|
||||
Path.Combine(Directory.GetCurrentDirectory(), _resourcesPath, fileName),
|
||||
Path.Combine(_resourcesPath, fileName),
|
||||
};
|
||||
|
||||
var filePath = candidates.FirstOrDefault(File.Exists);
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
var json = File.ReadAllText(filePath);
|
||||
var jsonResources = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||
if (jsonResources != null)
|
||||
{
|
||||
foreach (var item in jsonResources)
|
||||
{
|
||||
resources[item.Key] = item.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error loading localization file {fileName} from disk: {ex.Message}");
|
||||
}
|
||||
|
||||
_resourcesCache[culture] = resources;
|
||||
return resources;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Microsoft.Extensions.Localization;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Atomx.Admin.Client.Services
|
||||
{
|
||||
public class JsonStringLocalizerFactory : IStringLocalizerFactory
|
||||
{
|
||||
private readonly string _resourcesPath;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public JsonStringLocalizerFactory(string resourcesPath, HttpClient httpClient)
|
||||
{
|
||||
_resourcesPath = (resourcesPath ?? "Localization").Trim('/');
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public IStringLocalizer Create(Type resourceSource)
|
||||
{
|
||||
return new JsonStringLocalizer(_resourcesPath, _httpClient);
|
||||
}
|
||||
|
||||
public IStringLocalizer Create(string baseName, string location)
|
||||
{
|
||||
return new JsonStringLocalizer(_resourcesPath, _httpClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
129
Atomx.Admin/Atomx.Admin.Client/Services/LanguageProvider.cs
Normal file
129
Atomx.Admin/Atomx.Admin.Client/Services/LanguageProvider.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Atomx.Admin.Client.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// 语言提供者服务
|
||||
/// </summary>
|
||||
public class LanguageProvider
|
||||
{
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
private readonly NavigationManager _navigationManager;
|
||||
private string _currentLanguage = "zh-Hans";
|
||||
|
||||
public event Action? OnLanguageChanged;
|
||||
|
||||
public LanguageProvider(IJSRuntime jsRuntime, NavigationManager navigationManager)
|
||||
{
|
||||
_jsRuntime = jsRuntime;
|
||||
_navigationManager = navigationManager;
|
||||
}
|
||||
|
||||
public string CurrentLanguage
|
||||
{
|
||||
get => _currentLanguage;
|
||||
private set
|
||||
{
|
||||
if (_currentLanguage != value)
|
||||
{
|
||||
_currentLanguage = value;
|
||||
|
||||
// 设置全局线程文化,确保 IStringLocalizer 等在随后的渲染中读取到新文化
|
||||
try
|
||||
{
|
||||
var ci = new CultureInfo(value);
|
||||
CultureInfo.DefaultThreadCurrentCulture = ci;
|
||||
CultureInfo.DefaultThreadCurrentUICulture = ci;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略无效 culture 字符串
|
||||
}
|
||||
|
||||
OnLanguageChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<string> SupportedLanguages { get; } = new()
|
||||
{
|
||||
"zh-Hans", // 简体中文
|
||||
"en-US" // 英文(美国)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 初始化语言
|
||||
/// </summary>
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
// 尝试从本地存储获取保存的语言
|
||||
try
|
||||
{
|
||||
var savedLanguage = await _jsRuntime.InvokeAsync<string>("localStorage.getItem", "preferred-language");
|
||||
if (!string.IsNullOrEmpty(savedLanguage) && SupportedLanguages.Contains(savedLanguage))
|
||||
{
|
||||
CurrentLanguage = savedLanguage;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 从浏览器获取语言
|
||||
var browserLanguage = await _jsRuntime.InvokeAsync<string>("getBrowserLanguage");
|
||||
CurrentLanguage = GetSupportedLanguage(browserLanguage);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// JS互操作可能不可用(在预渲染时)
|
||||
CurrentLanguage = "zh-Hans";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换语言
|
||||
/// </summary>
|
||||
public async Task ChangeLanguageAsync(string languageCode)
|
||||
{
|
||||
if (SupportedLanguages.Contains(languageCode) && CurrentLanguage != languageCode)
|
||||
{
|
||||
CurrentLanguage = languageCode;
|
||||
|
||||
// 保存到本地存储
|
||||
try
|
||||
{
|
||||
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", "preferred-language", languageCode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略错误
|
||||
}
|
||||
|
||||
// setter 已触发 OnLanguageChanged
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取支持的语言
|
||||
/// </summary>
|
||||
private string GetSupportedLanguage(string browserLanguage)
|
||||
{
|
||||
if (string.IsNullOrEmpty(browserLanguage))
|
||||
return "zh-Hans";
|
||||
|
||||
// 检查完全匹配
|
||||
if (SupportedLanguages.Contains(browserLanguage))
|
||||
return browserLanguage;
|
||||
|
||||
// 检查中性语言匹配
|
||||
var neutralLanguage = browserLanguage.Split('-')[0];
|
||||
foreach (var supported in SupportedLanguages)
|
||||
{
|
||||
if (supported.StartsWith(neutralLanguage))
|
||||
return supported;
|
||||
}
|
||||
|
||||
return "zh-Hans"; // 默认语言
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,322 +0,0 @@
|
||||
using Atomx.Common.Models;
|
||||
using Microsoft.JSInterop;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Atomx.Admin.Client.Services
|
||||
{
|
||||
public interface ILocalizationService
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据name获取制定文化语言的译文
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
Task<string?> GetStringAsync(string name, string? culture = null);
|
||||
|
||||
/// <summary>
|
||||
/// 把本地化文化语言加载到内存中
|
||||
/// </summary>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
Task<bool> LoadResourcesAsync(string culture);
|
||||
|
||||
|
||||
event EventHandler<string>? ResourcesUpdated;
|
||||
}
|
||||
|
||||
public class LocalizationClientService : ILocalizationService, IAsyncDisposable
|
||||
{
|
||||
private readonly HttpService _httpService;
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
private readonly ILogger<LocalizationClientService> _logger;
|
||||
|
||||
private readonly Dictionary<string, Dictionary<string, string>> _resources = new();
|
||||
private readonly Dictionary<string, string> _versions = new();
|
||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
|
||||
public event EventHandler<string>? ResourcesUpdated;
|
||||
|
||||
public LocalizationClientService(
|
||||
HttpService httpService,
|
||||
IJSRuntime jsRuntime,
|
||||
ILogger<LocalizationClientService> logger)
|
||||
{
|
||||
_httpService = httpService;
|
||||
_jsRuntime = jsRuntime;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据name获取制定文化语言的译文
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<string?> GetStringAsync(string name, string? culture = null)
|
||||
{
|
||||
culture ??= await GetCurrentCultureAsync();
|
||||
|
||||
if (_resources.TryGetValue(culture, out var cultureResources))
|
||||
{
|
||||
if (cultureResources.TryGetValue(name, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
// 键不存在,触发资源更新检查
|
||||
_ = Task.Run(async () => await CheckAndUpdateResourcesAsync(culture));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 资源未加载,立即加载
|
||||
await LoadResourcesAsync(culture);
|
||||
|
||||
// 重试获取
|
||||
if (_resources.TryGetValue(culture, out cultureResources) &&
|
||||
cultureResources.TryGetValue(name, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogWarning("Localization key not found: {Key} for culture: {Culture}", name, culture);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把本地化文化语言加载到内存中
|
||||
/// </summary>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> LoadResourcesAsync(string culture)
|
||||
{
|
||||
await _semaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
// 先尝试从localStorage加载
|
||||
if (await TryLoadFromLocalStorage(culture))
|
||||
{
|
||||
// 检查服务器版本,如果需要更新则从服务器加载
|
||||
if (await CheckAndUpdateFromServer(culture))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return true; // 本地版本仍然有效
|
||||
}
|
||||
|
||||
// 从服务器加载
|
||||
return await LoadFromServer(culture);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从单例对象中获取版本记录
|
||||
/// </summary>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<string?> GetResourceVersionAsync(string culture)
|
||||
{
|
||||
if (_versions.TryGetValue(culture, out var version))
|
||||
{
|
||||
return version;
|
||||
}
|
||||
|
||||
var storedVersion = await GetStoredVersion(culture);
|
||||
return storedVersion;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查更新资源
|
||||
/// </summary>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<bool> CheckAndUpdateResourcesAsync(string culture)
|
||||
{
|
||||
return await CheckAndUpdateFromServer(culture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从本地加载文化语言数据
|
||||
/// </summary>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<bool> TryLoadFromLocalStorage(string culture)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resourcesJson = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", $"locales_{culture}");
|
||||
var version = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", $"locales_version_{culture}");
|
||||
|
||||
if (string.IsNullOrEmpty(resourcesJson) || string.IsNullOrEmpty(version))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var resources = JsonSerializer.Deserialize<Dictionary<string, string>>(resourcesJson);
|
||||
|
||||
if (resources != null && version != null)
|
||||
{
|
||||
_resources[culture] = resources;
|
||||
_versions[culture] = version;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to load resources from localStorage for culture: {Culture}", culture);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从服务器更新获取文化语言数据
|
||||
/// </summary>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<bool> LoadFromServer(string culture)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpService.Get<ApiResult<LocalizationFile>>($"/api/localeresource/resources/{culture}");
|
||||
if (response.Success)
|
||||
{
|
||||
_resources[culture] = response.Data.Translations;
|
||||
_versions[culture] = response.Data.ResourceVersion;
|
||||
// 保存到localStorage
|
||||
await SaveToLocalStorage(culture, response.Data.Translations, response.Data.ResourceVersion);
|
||||
ResourcesUpdated?.Invoke(this, culture);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to load resources from server for culture: {Culture}", culture);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 根据版本信息上从服务器数据对比版本更新数据
|
||||
/// </summary>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<bool> CheckAndUpdateFromServer(string culture)
|
||||
{
|
||||
try
|
||||
{
|
||||
var currentVersion = await GetResourceVersionAsync(culture);
|
||||
var serverVersion = await GetServerVersion(culture);
|
||||
|
||||
if (serverVersion != null && (currentVersion == null || serverVersion != currentVersion))
|
||||
{
|
||||
_logger.LogInformation("Updating resources for culture: {Culture}", culture);
|
||||
return await LoadFromServer(culture);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to check and update resources for culture: {Culture}", culture);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从服务器获取当前多语言文化数据版本信息
|
||||
/// </summary>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<string?> GetServerVersion(string culture)
|
||||
{
|
||||
try
|
||||
{
|
||||
var api = $"/api/localeresource/version/{culture}";
|
||||
var result = await _httpService.Get<ApiResult<string>>(api);
|
||||
if (result.Success)
|
||||
{
|
||||
return result.Data;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to get server version for culture: {Culture}", culture);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取存储在本地的文化语言版本信息
|
||||
/// </summary>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<string?> GetStoredVersion(string culture)
|
||||
{
|
||||
try
|
||||
{
|
||||
var version = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", $"locales_version_{culture}");
|
||||
if (!string.IsNullOrEmpty(version))
|
||||
{
|
||||
return version;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to get stored version for culture: {Culture}", culture);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 存储版本和文化语言信息
|
||||
/// </summary>
|
||||
/// <param name="culture"></param>
|
||||
/// <param name="resources"></param>
|
||||
/// <param name="version"></param>
|
||||
/// <returns></returns>
|
||||
private async Task SaveToLocalStorage(string culture, Dictionary<string, string> resources, string version)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resourcesJson = JsonSerializer.Serialize(resources);
|
||||
|
||||
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", $"locales_{culture}", resourcesJson);
|
||||
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", $"locales_version_{culture}", version);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to save resources to localStorage for culture: {Culture}", culture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取浏览器上的文化语言信息
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private async Task<string> GetCurrentCultureAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _jsRuntime.InvokeAsync<string>("blazorCulture.get");
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "en-US"; // 默认文化
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_semaphore?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user