完善语言文件的生成

This commit is contained in:
yxw
2025-12-14 18:27:21 +08:00
parent 54e9c7962d
commit 9edff983d8
36 changed files with 382 additions and 568 deletions

View File

@@ -29,7 +29,7 @@ builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.
// ע<><D7A2> LocalizationProvider (<28><><EFBFBD><EFBFBD> WASM)
// Use Scoped lifetime because LocalizationProvider depends on IJSRuntime (scoped) and IHttpClient etc.
builder.Services.AddScoped<ILocalizationProvider, WasmLocalizationProvider>();
builder.Services.AddScoped<ILocalizationProvider, LocalizationProvider>();
// ע<><D7A2> ILocalizationService <20><><EFBFBD><EFBFBD>ͬ<EFBFBD><CDAC> Culture <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><E4B4AB>
builder.Services.AddScoped<ILocalizationService, LocalizationService>();

View File

@@ -24,7 +24,7 @@ namespace Atomx.Admin.Client.Services
var value = _provider.GetString(name);
if (value == null)
{
// Avoid synchronous blocking during server prerender. Start background load and return key.
// 避免在服务端 prerender 阶段进行同步阻塞。以后台方式启动加载并返回 key
try
{
_ = _provider.LoadCultureAsync(_provider.CurrentCulture);

View File

@@ -1,16 +1,20 @@
using Microsoft.JSInterop;
using System.Collections.Concurrent;
using System.Globalization;
using System.Text.Json;
using Microsoft.JSInterop;
using Microsoft.Extensions.Localization;
namespace Atomx.Admin.Client.Services
{
/// <summary>
/// <20><EFBFBD><E1B9A9><EFBFBD><EFBFBD><EFBFBD><EFBFBD> JSON <20>ļ<EFBFBD><C4BC><EFBFBD><EFBFBD>ء<EFBFBD><D8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD>л<EFBFBD><D0BB><EFBFBD>ʵ<EFBFBD>֡<EFBFBD>
/// - <20><> Server <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> IWebHostEnvironment <20><> webroot<6F><74><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>ϵͳ<CFB5><CDB3>ȡ {culture}.json <20>ļ<EFBFBD><EFBFBD><EFBFBD>
/// - <20><> WASM <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʹ<EFBFBD><EFBFBD>ע<EFBFBD><EFBFBD><EFBFBD><EFBFBD> HttpClient <20><> /localization/{culture}.json <20><><EFBFBD>ز<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// ͬʱ<EFBFBD><EFBFBD><EFBFBD>л<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱд<EFBFBD><EFBFBD> Cookie <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ҳ<EFBFBD><EFBFBD> HTML lang <20><><EFBFBD>ԡ<EFBFBD>
/// <20><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> JSON <20>ļ<EFBFBD><C4BC>ļ<EFBFBD><EFBFBD>ء<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD>л<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ܡ<EFBFBD>
/// <EFBFBD><EFBFBD>Ҫְ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// - <20><> Server <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> prerender <20><> Blazor Server<65><72>ʱ<EFBFBD><CAB1><EFBFBD>Դ<EFBFBD> webroot ͬ<><CDAC><EFBFBD><EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD>ػ<EFBFBD> JSON <20>ļ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// <20>Ա<EFBFBD><EFBFBD>ڷ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ⱦ<EFBFBD>׶<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>á<EFBFBD>
/// - <20><> WASM <20><><EFBFBD><EFBFBD>ʱʹ<CAB1><CAB9>ע<EFBFBD><D7A2><EFBFBD><EFBFBD> HttpClient <20><> /localization/{culture}.json <20><><EFBFBD>ز<EFBFBD><D8B2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// - <20><><EFBFBD>л<EFBFBD><D0BB><EFBFBD><EFBFBD><EFBFBD>ʱд<CAB1><D0B4><EFBFBD><EFBFBD>Ϊ `atomx.culture` <20><> cookie<69><65><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˶<EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ҳ<EFBFBD><D2B3><EFBFBD><EFBFBD> HTML lang <20><><EFBFBD>ԡ<EFBFBD>
/// - <20><EFBFBD>¼<EFBFBD>֪ͨ LanguageChanged<65><64><EFBFBD><EFBFBD> UI <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӧ<EFBFBD><D3A6><EFBFBD>Ա<EFBFBD><D4B1><EFBFBD><EFBFBD><EFBFBD>
///
/// ˵<><CBB5><EFBFBD><EFBFBD>Ϊ<EFBFBD>˼<EFBFBD><CBBC><EFBFBD> Server <20><> WASM<53><4D><EFBFBD><EFBFBD> Provider <20><EFBFBD><E1BEA1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD>ѡ<EFBFBD><D1A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʵ<EFBFBD><CAB5><EFBFBD>Դ<EFBFBD><D4B4><EFBFBD>ط<EFBFBD>ʽ<EFBFBD><CABD>
/// </summary>
public interface ILocalizationProvider
{
@@ -25,6 +29,12 @@ namespace Atomx.Admin.Client.Services
event EventHandler<string>? LanguageChanged;
}
/// <summary>
/// LocalizationProvider <20><>ʵ<EFBFBD>֣<EFBFBD>
/// - ά<><CEAC>һ<EFBFBD><D2BB><EFBFBD><EFBFBD>̬<EFBFBD><CCAC><EFBFBD><EFBFBD><E6A3AC><EFBFBD><EFBFBD><EFBFBD>ظ<EFBFBD><D8B8><EFBFBD><EFBFBD><EFBFBD>/<2F><>ȡ<EFBFBD><C8A1>Դ<EFBFBD><D4B4>
/// - ֧<>ֶ<EFBFBD><D6B6><EFBFBD><EBA3A8> zh / en<65><6E><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> culture<72><65><EFBFBD><EFBFBD> zh-Hans / en-US<55><53>֮<EFBFBD><D6AE><EFBFBD><EFBFBD>ӳ<EFBFBD>
/// - <20><> Server <20><>֧<EFBFBD><D6A7>ͬ<EFBFBD><CDAC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> prerender <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// </summary>
public class LocalizationProvider : ILocalizationProvider
{
private readonly IServiceProvider _sp;
@@ -33,22 +43,26 @@ namespace Atomx.Admin.Client.Services
private readonly ILogger<LocalizationProvider> _logger;
private readonly ILocalizationService _localizationService;
// <20><><EFBFBD>棺culture -> translations
// Use a static concurrent dictionary so files loaded during middleware/server prerender
// are visible to provider instances created later in the same request pipeline.
// <20><><EFBFBD>棺culture -> translations<EFBFBD><EFBFBD>ʹ<EFBFBD><EFBFBD> ConcurrentDictionary <20><><EFBFBD>̰߳<DFB3>ȫ<EFBFBD>ع<EFBFBD><D8B9><EFBFBD><EFBFBD><EFBFBD>
// ʹ<EFBFBD>þ<EFBFBD>̬<EFBFBD>ֶ<EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>ʹ<EFBFBD>м<EFBFBD><EFBFBD><EFBFBD>/<2F><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͬһ<CDAC><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ܹ<EFBFBD><DCB9><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ѽ<EFBFBD><D1BC>صķ<D8B5><C4B7><EFBFBD><EBA3AC><EFBFBD><EFBFBD><EFBFBD>ظ<EFBFBD> I/O<><4F>
private static readonly ConcurrentDictionary<string, Dictionary<string, string>> _cache = new();
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> culture <20><>ӳ<EFBFBD><EFBFBD>
// ֧<EFBFBD>ֵĶ<EFBFBD><EFBFBD><EFBFBD>ӳ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>չ<EFBFBD><EFBFBD><EFBFBD>ڴ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ӡ<EFBFBD>
private static readonly Dictionary<string, string> ShortToCulture = new(StringComparer.OrdinalIgnoreCase)
{
{ "zh", "zh-Hans" },
{ "en", "en-US" }
};
// Ĭ<><C4AC><EFBFBD>Ļ<EFBFBD><C4BB><EFBFBD><EFBFBD><EFBFBD>δ<EFBFBD>ܴ<EFBFBD><DCB4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>/URL/Cookie <20>н<EFBFBD><D0BD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ļ<EFBFBD>ʱʹ<CAB1>ã<EFBFBD>
private string _currentCulture = "zh-Hans";
private const string CookieName = "atomx.culture";
/// <summary>
/// <20><><EFBFBD><EFBFBD><ECBAAF><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><D7A2><EFBFBD><EFBFBD>ȡ<EFBFBD><C8A1>Ҫ<EFBFBD><D2AA><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// ע<><EFBFBD><EFBFBD><ECBAAF><EFBFBD>в<EFBFBD>Ӧִ<D3A6>к<EFBFBD>ʱ<EFBFBD><CAB1> JS <20><><EFBFBD>ص<EFBFBD>ͬ<EFBFBD><CDAC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// </summary>
public LocalizationProvider(IServiceProvider sp, ILogger<LocalizationProvider> logger, IHttpClientFactory? httpClientFactory, IJSRuntime? jsRuntime, ILocalizationService localizationService)
{
_sp = sp;
@@ -57,22 +71,22 @@ namespace Atomx.Admin.Client.Services
_logger = logger;
_localizationService = localizationService;
// <20><><EFBFBD>ڹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>н<EFBFBD><EFBFBD><EFBFBD> JS <20><><EFBFBD><EFBFBD>ͬ<EFBFBD><CDAC> IO<49><4F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ը<EFBFBD><D4B8><EFBFBD><EFBFBD>߳<EFBFBD> culture <20><><EFBFBD>ó<EFBFBD>ʼֵ<CABC><D6B5><EFBFBD><EFBFBD>һ<EFBFBD><D2BB>Ϊ<EFBFBD><EFBFBD><EFBFBD><EFBFBD> culture
// <20><><EFBFBD>Ը<EFBFBD><EFBFBD>ݵ<EFBFBD>ǰ<EFBFBD>߳<EFBFBD> culture <20><>ʼ<EFBFBD><CABC> _currentCulture<72><65><EFBFBD><EFBFBD><EFBFBD>׳<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
try
{
var threadUi = CultureInfo.DefaultThreadCurrentUICulture ?? CultureInfo.CurrentUICulture;
if (!string.IsNullOrEmpty(threadUi?.Name))
{
_currentCulture = MapToFullCulture(threadUi!.Name);
_logger?.LogDebug("LocalizationProvider ctor detected thread UI culture: {Culture}", _currentCulture);
_logger.LogDebug("LocalizationProvider <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>߳<EFBFBD> UI <20>Ļ<EFBFBD>: {Culture}", _currentCulture);
}
}
catch (Exception ex)
{
_logger?.LogDebug(ex, "LocalizationProvider ctor failed to read thread culture");
_logger.LogDebug(ex, "LocalizationProvider <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȡ<EFBFBD>߳<EFBFBD><EFBFBD>Ļ<EFBFBD>ʧ<EFBFBD><EFBFBD>");
}
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Server<65><72>IWebHostEnvironment <EFBFBD><EFBFBD><EFBFBD>ã<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> JSRuntime <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ã<EFBFBD>˵<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͬ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ر<EFBFBD><EFBFBD>ػ<EFBFBD><EFBFBD>ļ<EFBFBD>
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Server <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>δ<EFBFBD><EFBFBD>ʹ<EFBFBD><EFBFBD> JSRuntime<6D><65><EFBFBD><EFBFBD>ζ<EFBFBD>ŷ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͬ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>ϵͳ<EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD><EFBFBD>ػ<EFBFBD><EFBFBD>ļ<EFBFBD><EFBFBD><EFBFBD>֧<EFBFBD><EFBFBD> prerender
try
{
var envType = Type.GetType("Microsoft.AspNetCore.Hosting.IWebHostEnvironment, Microsoft.AspNetCore.Hosting.Abstractions")
@@ -93,23 +107,23 @@ namespace Atomx.Admin.Client.Services
var json = File.ReadAllText(path);
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new Dictionary<string, string>();
_cache[_currentCulture] = dict;
_logger?.LogInformation("Loaded localization file for {Culture} from path {Path}, entries: {Count}", _currentCulture, path, dict.Count);
_logger.LogInformation("(Server ͬ<><CDAC>) <20><>·<EFBFBD><C2B7><EFBFBD><EFBFBD><EFBFBD>ر<EFBFBD><D8B1>ػ<EFBFBD><D8BB>ļ<EFBFBD> {Path}<7D><>Culture:{Culture}<7D><><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>:{Count}", path, _currentCulture, dict.Count);
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Failed to read localization file synchronously: {Path}", path);
_logger.LogWarning(ex, "(Server ͬ<><CDAC>) <20><>ȡ<EFBFBD><C8A1><EFBFBD>ػ<EFBFBD><D8BB>ļ<EFBFBD>ʧ<EFBFBD><CAA7>: {Path}", path);
}
}
else
{
_logger?.LogDebug("Localization file not found at {Path}", path);
_logger.LogDebug("<EFBFBD><EFBFBD><EFBFBD>ػ<EFBFBD><EFBFBD>ļ<EFBFBD>δ<EFBFBD><EFBFBD>·<EFBFBD><EFBFBD><EFBFBD>ҵ<EFBFBD>: {Path}", path);
}
}
}
}
catch (Exception ex)
{
_logger?.LogDebug(ex, "Synchronous file load attempt failed in ctor");
_logger.LogDebug(ex, "LocalizationProvider <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͬ<EFBFBD><CDAC><EFBFBD>ļ<EFBFBD><C4BC><EFBFBD><EFBFBD>س<EFBFBD><D8B3><EFBFBD>ʧ<EFBFBD><CAA7>");
}
}
@@ -117,6 +131,9 @@ namespace Atomx.Admin.Client.Services
public event EventHandler<string>? LanguageChanged;
/// <summary>
/// <20>ӻ<EFBFBD><D3BB><EFBFBD><EFBFBD>ж<EFBFBD>ȡָ<C8A1><D6B8><EFBFBD><EFBFBD><EFBFBD>ı<EFBFBD><C4B1>ػ<EFBFBD><D8BB>ַ<EFBFBD><D6B7><EFBFBD><EFBFBD><EFBFBD>δ<EFBFBD>ҵ<EFBFBD><D2B5><EFBFBD><EFBFBD><EFBFBD> null<6C><6C>
/// </summary>
public string? GetString(string key)
{
if (string.IsNullOrEmpty(key)) return null;
@@ -129,9 +146,14 @@ namespace Atomx.Admin.Client.Services
return null;
}
/// <summary>
/// <20><>ʼ<EFBFBD><CABC><EFBFBD><EFBFBD><EFBFBD>̣<EFBFBD><CCA3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ˣ<EFBFBD>
/// <20><><EFBFBD>ȼ<EFBFBD><C8BC><EFBFBD>URL <20><><EFBFBD><EFBFBD>ǰ׺ -> Cookie(atomx.culture) -> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> -> Ĭ<><C4AC>
/// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EBA3A8> zh/en<65><6E><EFBFBD><EFBFBD>ӳ<EFBFBD><D3B3>Ϊ<EFBFBD><CEAA><EFBFBD><EFBFBD><EFBFBD>Ļ<EFBFBD><C4BB><EFBFBD><EFBFBD><EFBFBD> zh-Hans/en-US<55><53><EFBFBD><EFBFBD>
/// </summary>
public async Task InitializeAsync()
{
_logger?.LogDebug("LocalizationProvider.InitializeAsync start. CurrentCulture={Culture}", _currentCulture);
_logger.LogDebug("LocalizationProvider.InitializeAsync <EFBFBD><EFBFBD>ʼ. CurrentCulture={Culture}", _currentCulture);
string? urlFirstSegment = null;
@@ -140,7 +162,7 @@ namespace Atomx.Admin.Client.Services
if (_jsRuntime != null && OperatingSystem.IsBrowser())
{
var path = await _jsRuntime.InvokeAsync<string>("eval", "location.pathname");
_logger?.LogDebug("JS location.pathname='{Path}'", path);
_logger.LogDebug("JS location.pathname='{Path}'", path);
if (!string.IsNullOrEmpty(path))
{
var trimmed = path.Trim('/');
@@ -148,19 +170,19 @@ namespace Atomx.Admin.Client.Services
{
var seg = trimmed.Split('/', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
urlFirstSegment = seg;
_logger?.LogDebug("Detected url first segment: {Segment}", urlFirstSegment);
_logger.LogDebug("<EFBFBD><EFBFBD><EFBFBD>⵽ URL <20>׶<EFBFBD>: {Segment}", urlFirstSegment);
}
}
}
}
catch (Exception ex)
{
_logger?.LogDebug(ex, "<22><>ȡ location.pathname ʧ<><CAA7>");
_logger.LogDebug(ex, "<22><>ȡ location.pathname ʧ<><CAA7>");
}
if (!string.IsNullOrEmpty(urlFirstSegment) && ShortToCulture.TryGetValue(urlFirstSegment, out var mapped))
{
_logger?.LogDebug("URL short segment '{Seg}' mapped to culture '{Culture}'", urlFirstSegment, mapped);
_logger.LogDebug("URL <EFBFBD><EFBFBD><EFBFBD><EFBFBD> '{Seg}' ӳ<><D3B3>Ϊ<EFBFBD>Ļ<EFBFBD> '{Culture}'", urlFirstSegment, mapped);
await SetCultureInternalAsync(mapped, persistCookie: false);
return;
}
@@ -170,18 +192,18 @@ namespace Atomx.Admin.Client.Services
if (_jsRuntime != null && OperatingSystem.IsBrowser())
{
var cookieVal = await _jsRuntime.InvokeAsync<string>("cookies.Read", CookieName);
_logger?.LogDebug("Cookie '{CookieName}'='{CookieVal}'", CookieName, cookieVal);
_logger.LogDebug("<EFBFBD><EFBFBD>ȡ Cookie '{CookieName}'='{CookieVal}'", CookieName, cookieVal);
if (!string.IsNullOrEmpty(cookieVal))
{
if (ShortToCulture.TryGetValue(cookieVal, out var mappedFromCookie))
{
_logger?.LogDebug("Cookie short '{Cookie}' mapped to {Culture}", cookieVal, mappedFromCookie);
_logger.LogDebug("Cookie <EFBFBD><EFBFBD><EFBFBD><EFBFBD> '{Cookie}' ӳ<EFBFBD><EFBFBD>Ϊ<EFBFBD>Ļ<EFBFBD> {Culture}", cookieVal, mappedFromCookie);
await SetCultureInternalAsync(mappedFromCookie, persistCookie: false);
return;
}
else
{
// <20><><EFBFBD><EFBFBD> cookie <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> culture<72><65><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʹ<EFBFBD><EFBFBD>
// cookie <20>п<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ѿ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> culture<72><65><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ƽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><EFBFBD>
await SetCultureInternalAsync(MapToFullCulture(cookieVal), persistCookie: false);
return;
}
@@ -190,7 +212,7 @@ namespace Atomx.Admin.Client.Services
}
catch (Exception ex)
{
_logger?.LogDebug(ex, "<22><>ȡ Cookie ʧ<><CAA7>");
_logger.LogDebug(ex, "<22><>ȡ Cookie ʧ<><CAA7>");
}
try
@@ -198,11 +220,11 @@ namespace Atomx.Admin.Client.Services
if (_jsRuntime != null && OperatingSystem.IsBrowser())
{
var browserLang = await _jsRuntime.InvokeAsync<string>("getBrowserLanguage");
_logger?.LogDebug("Browser language: {BrowserLang}", browserLang);
_logger.LogDebug("<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {BrowserLang}", browserLang);
if (!string.IsNullOrEmpty(browserLang))
{
var mappedFromBrowser = MapToFullCulture(browserLang);
_logger?.LogDebug("Browser mapped to {Culture}", mappedFromBrowser);
_logger.LogDebug("<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ӳ<EFBFBD><EFBFBD>Ϊ {Culture}", mappedFromBrowser);
await SetCultureInternalAsync(mappedFromBrowser, persistCookie: false);
return;
}
@@ -210,15 +232,18 @@ namespace Atomx.Admin.Client.Services
}
catch (Exception ex)
{
_logger?.LogDebug(ex, "<22><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD><CAA7>");
_logger.LogDebug(ex, "<22><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD><CAA7>");
}
// <20><><EFBFBD><EFBFBD>ȷ<EFBFBD><C8B7><EFBFBD><EFBFBD><EFBFBD>ص<EFBFBD>ǰ culture
_logger?.LogDebug("InitializeAsync falling back to current culture {Culture}", _currentCulture);
// <20><><EFBFBD>˵<EFBFBD><EFBFBD><EFBFBD>ǰĬ<EFBFBD><EFBFBD><EFBFBD>Ļ<EFBFBD><EFBFBD><EFBFBD>ȷ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Դ
_logger.LogDebug("InitializeAsync <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʹ<EFBFBD>õ<EFBFBD>ǰ<EFBFBD>Ļ<EFBFBD> {Culture}", _currentCulture);
await EnsureCultureLoadedAsync(_currentCulture);
await SetCultureInternalAsync(_currentCulture, persistCookie: false);
}
/// <summary>
/// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><ECB2BD><EFBFBD><EFBFBD><EFBFBD>Ļ<EFBFBD><C4BB><EFBFBD><EFBFBD>ɴ<EFBFBD><C9B4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> culture<72><65><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѡ<EFBFBD><D1A1><EFBFBD>Ƿ<EFBFBD><C7B7>־û<D6BE><C3BB><EFBFBD> Cookie
/// </summary>
public async Task SetCultureAsync(string cultureShortOrFull)
{
if (string.IsNullOrEmpty(cultureShortOrFull)) return;
@@ -235,10 +260,9 @@ namespace Atomx.Admin.Client.Services
public Task LoadCultureAsync(string culture) => EnsureCultureLoadedAsync(MapToFullCulture(culture));
/// <summary>
/// Server-side synchronous culture set used during prerender to ensure translations
/// are available immediately. This method will attempt to load localization
/// JSON from the server's webroot synchronously and set thread cultures.
/// </summary>
/// Server <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> prerender ʱ<><CAB1>ͬ<EFBFBD><CDAC><EFBFBD><EFBFBD><EFBFBD>ã<EFBFBD><C3A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>߳<EFBFBD> Culture <20><><EFBFBD><EFBFBD><EFBFBD>Դ<EFBFBD> webroot ͬ<><CDAC><EFBFBD><EFBFBD><EFBFBD>ر<EFBFBD><D8B1>ػ<EFBFBD><D8BB>ļ<EFBFBD><C4BC><EFBFBD>
/// <EFBFBD>÷<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> JS <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʺ<EFBFBD><CABA><EFBFBD><EFBFBD>м<EFBFBD><D0BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʹ<EFBFBD>á<EFBFBD>
/// </summary>
public void SetCultureForServer(string cultureShortOrFull)
{
try
@@ -246,7 +270,7 @@ namespace Atomx.Admin.Client.Services
var cultureFull = MapToFullCulture(cultureShortOrFull);
if (string.IsNullOrEmpty(cultureFull)) return;
// set thread culture
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>߳<EFBFBD> culture<72><65>Ӱ<EFBFBD><D3B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˴<EFBFBD><CBB4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> IStringLocalizer<65><72>
try
{
var ci = new CultureInfo(cultureFull);
@@ -256,7 +280,7 @@ namespace Atomx.Admin.Client.Services
}
catch { }
// try load from webroot synchronously via IWebHostEnvironment if available
// ͬ<EFBFBD><EFBFBD><EFBFBD><EFBFBD> webroot <20><><EFBFBD><EFBFBD> JSON <20><><EFBFBD>ػ<EFBFBD><D8BB>ļ<EFBFBD><C4BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڣ<EFBFBD>
try
{
var envType = Type.GetType("Microsoft.AspNetCore.Hosting.IWebHostEnvironment, Microsoft.AspNetCore.Hosting.Abstractions")
@@ -277,11 +301,11 @@ namespace Atomx.Admin.Client.Services
var json = File.ReadAllText(path);
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new Dictionary<string, string>();
_cache[cultureFull] = dict;
_logger?.LogInformation("(Server sync) Loaded localization file for {Culture} from path {Path}, entries: {Count}", cultureFull, path, dict.Count);
_logger.LogInformation("(Server ͬ<EFBFBD><EFBFBD>) <20><>Ϊ {Culture} <20><>·<EFBFBD><C2B7><EFBFBD><EFBFBD><EFBFBD>ر<EFBFBD><D8B1>ػ<EFBFBD><D8BB>ļ<EFBFBD><C4BC><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>: {Count}", cultureFull, dict.Count);
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "(Server sync) Failed to read localization file synchronously: {Path}", path);
_logger.LogWarning(ex, "(Server ͬ<EFBFBD><EFBFBD>) <20><>ȡ<EFBFBD><C8A1><EFBFBD>ػ<EFBFBD><D8BB>ļ<EFBFBD>ʧ<EFBFBD><CAA7>: {Path}", path);
}
}
}
@@ -289,18 +313,21 @@ namespace Atomx.Admin.Client.Services
}
catch (Exception ex)
{
_logger?.LogDebug(ex, "SetCultureForServer failed to load file for {Culture}", cultureFull);
_logger.LogDebug(ex, "SetCultureForServer <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>ʱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {Culture}", cultureFull);
}
}
catch (Exception ex)
{
_logger?.LogDebug(ex, "SetCultureForServer encountered error");
_logger.LogDebug(ex, "SetCultureForServer ִ<EFBFBD>й<EFBFBD><EFBFBD><EFBFBD><EFBFBD>з<EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
}
}
/// <summary>
/// <20>ڲ<EFBFBD><DAB2><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ļ<EFBFBD><C4BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫʱ<D2AA>־û<D6BE> Cookie<69><65><EFBFBD><EFBFBD><EFBFBD><EFBFBD> localization service <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>¼<EFBFBD><C2BC><EFBFBD>
/// </summary>
private async Task SetCultureInternalAsync(string cultureFull, bool persistCookie)
{
//_logger?.LogDebug("<22><><EFBFBD><EFBFBD><EFBFBD>ڲ<EFBFBD><DAB2>Ļ<EFBFBD><C4BB><EFBFBD><ECB2BD>ʼ: {Culture}, <20>־û<D6BE>={Persist}", cultureFull, persistCookie);
//_logger.LogDebug("<22><><EFBFBD><EFBFBD><EFBFBD>ڲ<EFBFBD><DAB2>Ļ<EFBFBD><C4BB><EFBFBD><ECB2BD>ʼ: {Culture}, <20>־û<D6BE>={Persist}", cultureFull, persistCookie);
await EnsureCultureLoadedAsync(cultureFull);
try
@@ -310,11 +337,11 @@ namespace Atomx.Admin.Client.Services
CultureInfo.DefaultThreadCurrentUICulture = ci;
_currentCulture = cultureFull;
_localizationService.SetLanguage(ci);
_logger?.LogDebug("Culture set to {Culture}", cultureFull);
_logger.LogDebug("<EFBFBD>Ļ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϊ {Culture}", cultureFull);
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "<22><><EFBFBD><EFBFBD> Culture ʧ<><CAA7>: {Culture}", cultureFull);
_logger.LogWarning(ex, "<22><><EFBFBD><EFBFBD> Culture ʧ<><CAA7>: {Culture}", cultureFull);
}
if (persistCookie && _jsRuntime != null)
@@ -322,11 +349,12 @@ namespace Atomx.Admin.Client.Services
try
{
var shortKey = ShortToCulture.FirstOrDefault(kv => string.Equals(kv.Value, cultureFull, StringComparison.OrdinalIgnoreCase)).Key ?? cultureFull;
// <20><> shortKey д<><D0B4> cookie<69><65><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Server ģʽ<C4A3>µ<EFBFBD><C2B5>м<EFBFBD><D0BC><EFBFBD><EFBFBD><EFBFBD>
await _jsRuntime.InvokeVoidAsync("cookies.Write", CookieName, shortKey, DateTime.UtcNow.AddYears(1).ToString("o"));
}
catch (Exception ex)
{
_logger?.LogDebug(ex, "д Cookie ʧ<><CAA7>");
_logger.LogDebug(ex, <EFBFBD><EFBFBD> Cookie ʧ<><CAA7>");
}
}
@@ -334,41 +362,46 @@ namespace Atomx.Admin.Client.Services
{
if (_jsRuntime != null)
{
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> HTML <20><> lang <20><><EFBFBD>ԣ<EFBFBD><D4A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϰ<EFBFBD>/SEO
await _jsRuntime.InvokeVoidAsync("setHtmlLang", cultureFull);
}
}
catch { }
// ֪ͨ<CDA8><D6AA><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
LanguageChanged?.Invoke(this, cultureFull);
}
/// <summary>
/// ȷ<><C8B7>ָ<EFBFBD><D6B8><EFBFBD>Ļ<EFBFBD><C4BB><EFBFBD> JSON <20>ļ<EFBFBD><C4BC>ѱ<EFBFBD><D1B1><EFBFBD><EFBFBD>ص<EFBFBD><D8B5><EFBFBD><EFBFBD><EFBFBD><E6A1A3><EFBFBD><EFBFBD>˳<EFBFBD><CBB3><EFBFBD><EFBFBD>WASM HttpClient -> <20>ļ<EFBFBD>ϵͳ -> <20><><EFBFBD>ֵ<EFBFBD>ռλ<D5BC><CEBB>
/// </summary>
private async Task EnsureCultureLoadedAsync(string cultureFull)
{
// Normalize possible short codes (e.g. zh -> zh-Hans, en -> en-US) and variants (zh-CN -> zh-Hans)
// <EFBFBD><EFBFBD>һ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> zh -> zh-Hans<6E><73>
cultureFull = MapToFullCulture(cultureFull);
if (string.IsNullOrEmpty(cultureFull)) return;
if (_cache.ContainsKey(cultureFull))
{
_logger?.LogDebug("EnsureCultureLoadedAsync: culture {Culture} already cached", cultureFull);
_logger.LogDebug("EnsureCultureLoadedAsync: <EFBFBD>Ļ<EFBFBD> {Culture} <EFBFBD>ѻ<EFBFBD><EFBFBD><EFBFBD>", cultureFull);
return;
}
// Prefer HttpClient when running in browser (WASM)
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>WASM<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʹ<EFBFBD><EFBFBD> HttpClient <20><><EFBFBD>ر<EFBFBD><D8B1>ػ<EFBFBD> JSON
if (_jsRuntime != null && OperatingSystem.IsBrowser())
{
_logger?.LogInformation("EnsureCultureLoadedAsync: running in browser, will attempt HttpClient for {Culture}", cultureFull);
_logger.LogInformation("EnsureCultureLoadedAsync: <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͨ<EFBFBD><EFBFBD> HttpClient <EFBFBD><EFBFBD><EFBFBD><EFBFBD> {Culture}", cultureFull);
try
{
var http = _sp.GetService(typeof(HttpClient)) as HttpClient;
if (http == null && _httpClientFactory != null)
{
_logger?.LogDebug("HttpClient not found from service provider, using factory");
_logger.LogDebug("δ<EFBFBD><EFBFBD> ServiceProvider <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> HttpClient<6E><74>ʹ<EFBFBD><CAB9> IHttpClientFactory <20><><EFBFBD><EFBFBD>");
http = _httpClientFactory.CreateClient();
}
else
{
_logger?.LogDebug("HttpClient resolved from service provider: {HasClient}", http != null);
_logger.LogDebug("<EFBFBD><EFBFBD> ServiceProvider <20><><EFBFBD><EFBFBD> HttpClient: {HasClient}", http != null);
}
if (http != null)
@@ -376,7 +409,7 @@ namespace Atomx.Admin.Client.Services
var url = $"/localization/{cultureFull}.json";
Uri? requestUri = null;
// If HttpClient has a BaseAddress, use it. Otherwise, if running in browser, build absolute URI from location.origin
// <EFBFBD><EFBFBD> HttpClient <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> BaseAddress ʱʹ<CAB1>ã<EFBFBD><C3A3><EFBFBD><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8> JS <20><>ȡ location.origin <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> URI
if (http.BaseAddress != null)
{
requestUri = new Uri(http.BaseAddress, url);
@@ -388,43 +421,42 @@ namespace Atomx.Admin.Client.Services
var origin = await _jsRuntime.InvokeAsync<string>("eval", "location.origin");
if (!string.IsNullOrEmpty(origin))
{
// ensure no double slashes
requestUri = new Uri(new Uri(origin), url);
}
}
catch (Exception jsEx)
{
_logger?.LogDebug(jsEx, "Failed to get location.origin from JS");
_logger.LogDebug(jsEx, "<EFBFBD><EFBFBD> JS <20><>ȡ location.origin ʧ<EFBFBD><EFBFBD>");
}
}
if (requestUri != null)
{
_logger?.LogInformation("Downloading localization from {Url}", requestUri);
_logger.LogInformation("<EFBFBD><EFBFBD> {Url} <20><><EFBFBD>ر<EFBFBD><D8B1>ػ<EFBFBD><D8BB><EFBFBD>Դ", requestUri);
var txt = await http.GetStringAsync(requestUri);
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(txt) ?? new Dictionary<string, string>();
_cache[cultureFull] = dict;
_logger?.LogInformation("Loaded localization via HttpClient for {Culture}, entries: {Count}", cultureFull, dict.Count);
_logger.LogInformation("ͨ<EFBFBD><EFBFBD> HttpClient Ϊ {Culture} <20><><EFBFBD>ص<EFBFBD><D8B5><EFBFBD><EFBFBD>ػ<EFBFBD><D8BB><EFBFBD><EFBFBD>ݣ<EFBFBD><DDA3><EFBFBD>Ŀ<EFBFBD><C4BF>: {Count}", cultureFull, dict.Count);
return;
}
else
{
_logger?.LogWarning("HttpClient has no BaseAddress and JSRuntime unavailable to construct absolute URL; skipping HttpClient load for {Culture}", cultureFull);
_logger.LogWarning("HttpClient <EFBFBD>޷<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> URL<52><4C><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8> HttpClient <EFBFBD><EFBFBD><EFBFBD><EFBFBD> {Culture}", cultureFull);
}
}
else
{
_logger?.LogWarning("No HttpClient available to load localization for {Culture}", cultureFull);
_logger.LogWarning("δ<EFBFBD>ҵ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>õ<EFBFBD> HttpClient <EFBFBD>Լ<EFBFBD><EFBFBD><EFBFBD> {Culture}", cultureFull);
}
}
catch (Exception ex)
{
_logger?.LogDebug(ex, <><CDA8> HttpClient <20><><EFBFBD>ر<EFBFBD><D8B1>ػ<EFBFBD><D8BB>ļ<EFBFBD>ʧ<EFBFBD><CAA7>: {Culture}", cultureFull);
_logger.LogDebug(ex, <><CDA8> HttpClient <20><><EFBFBD>ر<EFBFBD><D8B1>ػ<EFBFBD><D8BB>ļ<EFBFBD>ʧ<EFBFBD><CAA7>: {Culture}", cultureFull);
}
}
_logger?.LogDebug("EnsureCultureLoadedAsync trying filesystem for {Culture}", cultureFull);
// <20><><EFBFBD><EFBFBD>ͨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȡ IWebHostEnvironment<6E><74>Server ʱ<><CAB1><EFBFBD>ã<EFBFBD>
_logger.LogDebug("EnsureCultureLoadedAsync: <20><><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>ϵͳ<CFB5><CDB3><EFBFBD><EFBFBD> {Culture}", cultureFull);
// <20><><EFBFBD>ˣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͨ<EFBFBD><EFBFBD> IWebHostEnvironment <20><><EFBFBD>ļ<EFBFBD>ϵͳ<CFB5><CDB3>ȡ<EFBFBD><EFBFBD>Server ʱ<><CAB1><EFBFBD>ã<EFBFBD>
try
{
var envType = Type.GetType("Microsoft.AspNetCore.Hosting.IWebHostEnvironment, Microsoft.AspNetCore.Hosting.Abstractions")
@@ -438,67 +470,70 @@ namespace Atomx.Admin.Client.Services
var contentRootProp = envType.GetProperty("ContentRootPath");
var webRoot = webRootProp?.GetValue(env) as string ?? Path.Combine(contentRootProp?.GetValue(env) as string ?? ".", "wwwroot");
var path = Path.Combine(webRoot ?? ".", "localization", cultureFull + ".json");
_logger?.LogDebug("Looking for localization file at {Path}", path);
_logger.LogDebug("<EFBFBD><EFBFBD><EFBFBD>ұ<EFBFBD><EFBFBD>ػ<EFBFBD><EFBFBD>ļ<EFBFBD>·<EFBFBD><EFBFBD>: {Path}", path);
if (File.Exists(path))
{
var json = await File.ReadAllTextAsync(path);
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new Dictionary<string, string>();
_cache[cultureFull] = dict;
_logger?.LogInformation("Loaded localization from file for {Culture}, entries: {Count}", cultureFull, dict.Count);
_logger.LogInformation("<EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>Ϊ {Culture} <20><><EFBFBD>ر<EFBFBD><D8B1>ػ<EFBFBD><D8BB><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>: {Count}", cultureFull, dict.Count);
return;
}
else
{
_logger?.LogDebug("Localization file not found at {Path}", path);
// Fallback: check build output wwwroot under AppContext.BaseDirectory
_logger.LogDebug("δ<EFBFBD><EFBFBD>·<EFBFBD><EFBFBD><EFBFBD>ҵ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ػ<EFBFBD><EFBFBD>ļ<EFBFBD>: {Path}", path);
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>·<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ¼<EFBFBD><EFBFBD> wwwroot
try
{
var alt = Path.Combine(AppContext.BaseDirectory ?? ".", "wwwroot", "localization", cultureFull + ".json");
_logger?.LogDebug("Looking for localization file at alternative path {AltPath}", alt);
_logger.LogDebug("<EFBFBD><EFBFBD><EFBFBD>ұ<EFBFBD><EFBFBD><EFBFBD>·<EFBFBD><EFBFBD>: {AltPath}", alt);
if (File.Exists(alt))
{
var json2 = await File.ReadAllTextAsync(alt);
var dict2 = JsonSerializer.Deserialize<Dictionary<string, string>>(json2) ?? new Dictionary<string, string>();
_cache[cultureFull] = dict2;
_logger?.LogInformation("Loaded localization from alternative file path for {Culture}, entries: {Count}", cultureFull, dict2.Count);
_logger.LogInformation("<EFBFBD>ӱ<EFBFBD><EFBFBD><EFBFBD>·<EFBFBD><EFBFBD>Ϊ {Culture} <20><><EFBFBD>ص<EFBFBD><D8B5><EFBFBD><EFBFBD>ػ<EFBFBD><D8BB>ļ<EFBFBD><C4BC><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>: {Count}", cultureFull, dict2.Count);
return;
}
else
{
_logger?.LogDebug("Localization file not found at alternative path {AltPath}", alt);
_logger.LogDebug("<EFBFBD><EFBFBD><EFBFBD><EFBFBD>·<EFBFBD><EFBFBD>δ<EFBFBD>ҵ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ػ<EFBFBD><EFBFBD>ļ<EFBFBD>: {AltPath}", alt);
}
}
catch (Exception exAlt)
{
_logger?.LogDebug(exAlt, "Error while checking alternative localization path");
_logger.LogDebug(exAlt, "<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ñ<EFBFBD><EFBFBD>ػ<EFBFBD>·<EFBFBD><EFBFBD>ʱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
}
}
}
else
{
_logger?.LogDebug("IWebHostEnvironment not resolved from service provider");
_logger.LogDebug("<EFBFBD>޷<EFBFBD><EFBFBD><EFBFBD> ServiceProvider <20><>ȡ IWebHostEnvironment ʵ<><CAB5>");
}
}
else
{
_logger?.LogDebug("IWebHostEnvironment type not found via reflection");
_logger.LogDebug("ͨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>δ<EFBFBD><EFBFBD><EFBFBD>ҵ<EFBFBD> IWebHostEnvironment <EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
}
}
catch (Exception ex)
{
_logger?.LogDebug(ex, "<22><><EFBFBD>ļ<EFBFBD>ϵͳ<CFB5><CDB3><EFBFBD>ر<EFBFBD><D8B1>ػ<EFBFBD><D8BB>ļ<EFBFBD>ʧ<EFBFBD><CAA7>: {Culture}", cultureFull);
_logger.LogDebug(ex, "ͨ<EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>ϵͳ<EFBFBD><EFBFBD><EFBFBD>ر<EFBFBD><EFBFBD>ػ<EFBFBD><EFBFBD>ļ<EFBFBD>ʧ<EFBFBD><EFBFBD>: {Culture}", cultureFull);
}
_logger?.LogDebug("EnsureCultureLoadedAsync fallback to empty dict for {Culture}", cultureFull);
_logger.LogDebug("EnsureCultureLoadedAsync: <20><><EFBFBD><EFBFBD>Ϊ {Culture} <20>Ŀ<EFBFBD><C4BF>ֵ<EFBFBD>ռλ", cultureFull);
_cache[cultureFull] = new Dictionary<string, string>();
}
/// <summary>
/// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ַ<EFBFBD><D6B7><EFBFBD>ӳ<EFBFBD><D3B3>Ϊ<EFBFBD>ڲ<EFBFBD>ʹ<EFBFBD>õ<EFBFBD><C3B5><EFBFBD><EFBFBD><EFBFBD> culture<72><65><EFBFBD><EFBFBD><EFBFBD><EFBFBD> zh -> zh-Hans<6E><73>
/// </summary>
private string MapToFullCulture(string culture)
{
if (string.IsNullOrEmpty(culture)) return culture;
// direct mapping
// ֱ<EFBFBD><EFBFBD>ӳ<EFBFBD><EFBFBD>
if (ShortToCulture.TryGetValue(culture, out var mapped)) return mapped;
// consider prefix, e.g. zh-CN -> zh
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǰ׺<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> zh-CN -> zh
var prefix = culture.Split('-', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
if (!string.IsNullOrEmpty(prefix) && ShortToCulture.TryGetValue(prefix, out var mapped2)) return mapped2;
return culture;

View File

@@ -1,239 +0,0 @@
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;
}
}
}