完善语言文件的生成

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) // ע<><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, WasmLocalizationProvider>(); builder.Services.AddScoped<ILocalizationProvider, LocalizationProvider>();
// ע<><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>();

View File

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

View File

@@ -1,16 +1,20 @@
using Microsoft.JSInterop;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Globalization; using System.Globalization;
using System.Text.Json; using System.Text.Json;
using Microsoft.JSInterop;
using Microsoft.Extensions.Localization;
namespace Atomx.Admin.Client.Services namespace Atomx.Admin.Client.Services
{ {
/// <summary> /// <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><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>
/// - <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> /// <EFBFBD><EFBFBD>Ҫְ<EFBFBD><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> /// - <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>
/// ͬʱ<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>׶<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> /// </summary>
public interface ILocalizationProvider public interface ILocalizationProvider
{ {
@@ -25,6 +29,12 @@ namespace Atomx.Admin.Client.Services
event EventHandler<string>? LanguageChanged; 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 public class LocalizationProvider : ILocalizationProvider
{ {
private readonly IServiceProvider _sp; private readonly IServiceProvider _sp;
@@ -33,22 +43,26 @@ namespace Atomx.Admin.Client.Services
private readonly ILogger<LocalizationProvider> _logger; private readonly ILogger<LocalizationProvider> _logger;
private readonly ILocalizationService _localizationService; private readonly ILocalizationService _localizationService;
// <20><><EFBFBD>棺culture -> translations // <20><><EFBFBD>棺culture -> translations<EFBFBD><EFBFBD>ʹ<EFBFBD><EFBFBD> ConcurrentDictionary <20><><EFBFBD>̰߳<DFB3>ȫ<EFBFBD>ع<EFBFBD><D8B9><EFBFBD><EFBFBD><EFBFBD>
// Use a static concurrent dictionary so files loaded during middleware/server prerender // ʹ<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>
// are visible to provider instances created later in the same request pipeline.
private static readonly ConcurrentDictionary<string, Dictionary<string, string>> _cache = new(); 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) private static readonly Dictionary<string, string> ShortToCulture = new(StringComparer.OrdinalIgnoreCase)
{ {
{ "zh", "zh-Hans" }, { "zh", "zh-Hans" },
{ "en", "en-US" } { "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 string _currentCulture = "zh-Hans";
private const string CookieName = "atomx.culture"; 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) public LocalizationProvider(IServiceProvider sp, ILogger<LocalizationProvider> logger, IHttpClientFactory? httpClientFactory, IJSRuntime? jsRuntime, ILocalizationService localizationService)
{ {
_sp = sp; _sp = sp;
@@ -57,22 +71,22 @@ namespace Atomx.Admin.Client.Services
_logger = logger; _logger = logger;
_localizationService = localizationService; _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 try
{ {
var threadUi = CultureInfo.DefaultThreadCurrentUICulture ?? CultureInfo.CurrentUICulture; var threadUi = CultureInfo.DefaultThreadCurrentUICulture ?? CultureInfo.CurrentUICulture;
if (!string.IsNullOrEmpty(threadUi?.Name)) if (!string.IsNullOrEmpty(threadUi?.Name))
{ {
_currentCulture = MapToFullCulture(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) 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 try
{ {
var envType = Type.GetType("Microsoft.AspNetCore.Hosting.IWebHostEnvironment, Microsoft.AspNetCore.Hosting.Abstractions") 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 json = File.ReadAllText(path);
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new Dictionary<string, string>(); var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new Dictionary<string, string>();
_cache[_currentCulture] = dict; _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) 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 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) 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; 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) public string? GetString(string key)
{ {
if (string.IsNullOrEmpty(key)) return null; if (string.IsNullOrEmpty(key)) return null;
@@ -129,9 +146,14 @@ namespace Atomx.Admin.Client.Services
return null; 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() public async Task InitializeAsync()
{ {
_logger?.LogDebug("LocalizationProvider.InitializeAsync start. CurrentCulture={Culture}", _currentCulture); _logger.LogDebug("LocalizationProvider.InitializeAsync <EFBFBD><EFBFBD>ʼ. CurrentCulture={Culture}", _currentCulture);
string? urlFirstSegment = null; string? urlFirstSegment = null;
@@ -140,7 +162,7 @@ namespace Atomx.Admin.Client.Services
if (_jsRuntime != null && OperatingSystem.IsBrowser()) if (_jsRuntime != null && OperatingSystem.IsBrowser())
{ {
var path = await _jsRuntime.InvokeAsync<string>("eval", "location.pathname"); 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)) if (!string.IsNullOrEmpty(path))
{ {
var trimmed = path.Trim('/'); var trimmed = path.Trim('/');
@@ -148,19 +170,19 @@ namespace Atomx.Admin.Client.Services
{ {
var seg = trimmed.Split('/', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); var seg = trimmed.Split('/', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
urlFirstSegment = seg; urlFirstSegment = seg;
_logger?.LogDebug("Detected url first segment: {Segment}", urlFirstSegment); _logger.LogDebug("<EFBFBD><EFBFBD><EFBFBD>⵽ URL <20>׶<EFBFBD>: {Segment}", urlFirstSegment);
} }
} }
} }
} }
catch (Exception ex) 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)) 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); await SetCultureInternalAsync(mapped, persistCookie: false);
return; return;
} }
@@ -170,18 +192,18 @@ namespace Atomx.Admin.Client.Services
if (_jsRuntime != null && OperatingSystem.IsBrowser()) if (_jsRuntime != null && OperatingSystem.IsBrowser())
{ {
var cookieVal = await _jsRuntime.InvokeAsync<string>("cookies.Read", CookieName); 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 (!string.IsNullOrEmpty(cookieVal))
{ {
if (ShortToCulture.TryGetValue(cookieVal, out var mappedFromCookie)) 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); await SetCultureInternalAsync(mappedFromCookie, persistCookie: false);
return; return;
} }
else 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); await SetCultureInternalAsync(MapToFullCulture(cookieVal), persistCookie: false);
return; return;
} }
@@ -190,7 +212,7 @@ namespace Atomx.Admin.Client.Services
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger?.LogDebug(ex, "<22><>ȡ Cookie ʧ<><CAA7>"); _logger.LogDebug(ex, "<22><>ȡ Cookie ʧ<><CAA7>");
} }
try try
@@ -198,11 +220,11 @@ namespace Atomx.Admin.Client.Services
if (_jsRuntime != null && OperatingSystem.IsBrowser()) if (_jsRuntime != null && OperatingSystem.IsBrowser())
{ {
var browserLang = await _jsRuntime.InvokeAsync<string>("getBrowserLanguage"); 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)) if (!string.IsNullOrEmpty(browserLang))
{ {
var mappedFromBrowser = MapToFullCulture(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); await SetCultureInternalAsync(mappedFromBrowser, persistCookie: false);
return; return;
} }
@@ -210,15 +232,18 @@ namespace Atomx.Admin.Client.Services
} }
catch (Exception ex) 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 // <20><><EFBFBD>˵<EFBFBD><EFBFBD><EFBFBD>ǰĬ<EFBFBD><EFBFBD><EFBFBD>Ļ<EFBFBD><EFBFBD><EFBFBD>ȷ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Դ
_logger?.LogDebug("InitializeAsync falling back to current culture {Culture}", _currentCulture); _logger.LogDebug("InitializeAsync <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʹ<EFBFBD>õ<EFBFBD>ǰ<EFBFBD>Ļ<EFBFBD> {Culture}", _currentCulture);
await EnsureCultureLoadedAsync(_currentCulture); await EnsureCultureLoadedAsync(_currentCulture);
await SetCultureInternalAsync(_currentCulture, persistCookie: false); 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) public async Task SetCultureAsync(string cultureShortOrFull)
{ {
if (string.IsNullOrEmpty(cultureShortOrFull)) return; if (string.IsNullOrEmpty(cultureShortOrFull)) return;
@@ -235,9 +260,8 @@ namespace Atomx.Admin.Client.Services
public Task LoadCultureAsync(string culture) => EnsureCultureLoadedAsync(MapToFullCulture(culture)); public Task LoadCultureAsync(string culture) => EnsureCultureLoadedAsync(MapToFullCulture(culture));
/// <summary> /// <summary>
/// Server-side synchronous culture set used during prerender to ensure translations /// 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>
/// are available immediately. This method will attempt to load localization /// <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>
/// JSON from the server's webroot synchronously and set thread cultures.
/// </summary> /// </summary>
public void SetCultureForServer(string cultureShortOrFull) public void SetCultureForServer(string cultureShortOrFull)
{ {
@@ -246,7 +270,7 @@ namespace Atomx.Admin.Client.Services
var cultureFull = MapToFullCulture(cultureShortOrFull); var cultureFull = MapToFullCulture(cultureShortOrFull);
if (string.IsNullOrEmpty(cultureFull)) return; 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 try
{ {
var ci = new CultureInfo(cultureFull); var ci = new CultureInfo(cultureFull);
@@ -256,7 +280,7 @@ namespace Atomx.Admin.Client.Services
} }
catch { } 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 try
{ {
var envType = Type.GetType("Microsoft.AspNetCore.Hosting.IWebHostEnvironment, Microsoft.AspNetCore.Hosting.Abstractions") 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 json = File.ReadAllText(path);
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new Dictionary<string, string>(); var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new Dictionary<string, string>();
_cache[cultureFull] = dict; _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) 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) 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) 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) 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); await EnsureCultureLoadedAsync(cultureFull);
try try
@@ -310,11 +337,11 @@ namespace Atomx.Admin.Client.Services
CultureInfo.DefaultThreadCurrentUICulture = ci; CultureInfo.DefaultThreadCurrentUICulture = ci;
_currentCulture = cultureFull; _currentCulture = cultureFull;
_localizationService.SetLanguage(ci); _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) 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) if (persistCookie && _jsRuntime != null)
@@ -322,11 +349,12 @@ namespace Atomx.Admin.Client.Services
try try
{ {
var shortKey = ShortToCulture.FirstOrDefault(kv => string.Equals(kv.Value, cultureFull, StringComparison.OrdinalIgnoreCase)).Key ?? cultureFull; 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")); await _jsRuntime.InvokeVoidAsync("cookies.Write", CookieName, shortKey, DateTime.UtcNow.AddYears(1).ToString("o"));
} }
catch (Exception ex) 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) 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); await _jsRuntime.InvokeVoidAsync("setHtmlLang", cultureFull);
} }
} }
catch { } catch { }
// ֪ͨ<CDA8><D6AA><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
LanguageChanged?.Invoke(this, cultureFull); 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) 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); cultureFull = MapToFullCulture(cultureFull);
if (string.IsNullOrEmpty(cultureFull)) return; if (string.IsNullOrEmpty(cultureFull)) return;
if (_cache.ContainsKey(cultureFull)) if (_cache.ContainsKey(cultureFull))
{ {
_logger?.LogDebug("EnsureCultureLoadedAsync: culture {Culture} already cached", cultureFull); _logger.LogDebug("EnsureCultureLoadedAsync: <EFBFBD>Ļ<EFBFBD> {Culture} <EFBFBD>ѻ<EFBFBD><EFBFBD><EFBFBD>", cultureFull);
return; 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()) 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 try
{ {
var http = _sp.GetService(typeof(HttpClient)) as HttpClient; var http = _sp.GetService(typeof(HttpClient)) as HttpClient;
if (http == null && _httpClientFactory != null) 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(); http = _httpClientFactory.CreateClient();
} }
else 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) if (http != null)
@@ -376,7 +409,7 @@ namespace Atomx.Admin.Client.Services
var url = $"/localization/{cultureFull}.json"; var url = $"/localization/{cultureFull}.json";
Uri? requestUri = null; 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) if (http.BaseAddress != null)
{ {
requestUri = new Uri(http.BaseAddress, url); requestUri = new Uri(http.BaseAddress, url);
@@ -388,43 +421,42 @@ namespace Atomx.Admin.Client.Services
var origin = await _jsRuntime.InvokeAsync<string>("eval", "location.origin"); var origin = await _jsRuntime.InvokeAsync<string>("eval", "location.origin");
if (!string.IsNullOrEmpty(origin)) if (!string.IsNullOrEmpty(origin))
{ {
// ensure no double slashes
requestUri = new Uri(new Uri(origin), url); requestUri = new Uri(new Uri(origin), url);
} }
} }
catch (Exception jsEx) 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) 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 txt = await http.GetStringAsync(requestUri);
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(txt) ?? new Dictionary<string, string>(); var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(txt) ?? new Dictionary<string, string>();
_cache[cultureFull] = dict; _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; return;
} }
else 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 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) 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); _logger.LogDebug("EnsureCultureLoadedAsync: <20><><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>ϵͳ<CFB5><CDB3><EFBFBD><EFBFBD> {Culture}", cultureFull);
// <20><><EFBFBD><EFBFBD>ͨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȡ IWebHostEnvironment<6E><74>Server ʱ<><CAB1><EFBFBD>ã<EFBFBD> // <20><><EFBFBD>ˣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͨ<EFBFBD><EFBFBD> IWebHostEnvironment <20><><EFBFBD>ļ<EFBFBD>ϵͳ<CFB5><CDB3>ȡ<EFBFBD><EFBFBD>Server ʱ<><CAB1><EFBFBD>ã<EFBFBD>
try try
{ {
var envType = Type.GetType("Microsoft.AspNetCore.Hosting.IWebHostEnvironment, Microsoft.AspNetCore.Hosting.Abstractions") 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 contentRootProp = envType.GetProperty("ContentRootPath");
var webRoot = webRootProp?.GetValue(env) as string ?? Path.Combine(contentRootProp?.GetValue(env) as string ?? ".", "wwwroot"); var webRoot = webRootProp?.GetValue(env) as string ?? Path.Combine(contentRootProp?.GetValue(env) as string ?? ".", "wwwroot");
var path = Path.Combine(webRoot ?? ".", "localization", cultureFull + ".json"); 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)) if (File.Exists(path))
{ {
var json = await File.ReadAllTextAsync(path); var json = await File.ReadAllTextAsync(path);
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new Dictionary<string, string>(); var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new Dictionary<string, string>();
_cache[cultureFull] = dict; _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; return;
} }
else 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);
// Fallback: check build output wwwroot under AppContext.BaseDirectory // <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 try
{ {
var alt = Path.Combine(AppContext.BaseDirectory ?? ".", "wwwroot", "localization", cultureFull + ".json"); 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)) if (File.Exists(alt))
{ {
var json2 = await File.ReadAllTextAsync(alt); var json2 = await File.ReadAllTextAsync(alt);
var dict2 = JsonSerializer.Deserialize<Dictionary<string, string>>(json2) ?? new Dictionary<string, string>(); var dict2 = JsonSerializer.Deserialize<Dictionary<string, string>>(json2) ?? new Dictionary<string, string>();
_cache[cultureFull] = dict2; _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; return;
} }
else 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) 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 else
{ {
_logger?.LogDebug("IWebHostEnvironment not resolved from service provider"); _logger.LogDebug("<EFBFBD>޷<EFBFBD><EFBFBD><EFBFBD> ServiceProvider <20><>ȡ IWebHostEnvironment ʵ<><CAB5>");
} }
} }
else 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) 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>(); _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) private string MapToFullCulture(string culture)
{ {
if (string.IsNullOrEmpty(culture)) return culture; if (string.IsNullOrEmpty(culture)) return culture;
// direct mapping // ֱ<EFBFBD><EFBFBD>ӳ<EFBFBD><EFBFBD>
if (ShortToCulture.TryGetValue(culture, out var mapped)) return mapped; 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(); var prefix = culture.Split('-', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
if (!string.IsNullOrEmpty(prefix) && ShortToCulture.TryGetValue(prefix, out var mapped2)) return mapped2; if (!string.IsNullOrEmpty(prefix) && ShortToCulture.TryGetValue(prefix, out var mapped2)) return mapped2;
return culture; 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;
}
}
}

View File

@@ -12,7 +12,6 @@ using Atomx.Utils.Models;
using MapsterMapper; using MapsterMapper;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;
namespace Atomx.Admin.Controllers namespace Atomx.Admin.Controllers
{ {
@@ -206,17 +205,65 @@ namespace Atomx.Admin.Controllers
} }
//异步更新对应的json文件 //异步更新对应的json文件
var wwwroot = _environment.WebRootPath; var language = await _cacheService.GetLanguageById(model.LanguageId);
var dic = new Dictionary<string, string>(); var culture = language?.Culture ?? string.Empty;
dic.Add(data.Name,data.Value); if (!string.IsNullOrEmpty(culture))
{
_backgroundService.UpdateLocalizationFile(wwwroot, model.Culture, dic.ToJson()); var wwwroot = _environment.WebRootPath;
var path = Path.Combine(wwwroot, "localization");
var dic = new Dictionary<string, string>();
dic.Add(data.Name, data.Value);
_backgroundService.UpdateLocalizationFile(path, culture, dic.ToJson());
}
result = result.IsSuccess(true); result = result.IsSuccess(true);
return new JsonResult(result); return new JsonResult(result);
} }
/// <summary>
/// 删除多语言翻译数据
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpPost("delete")]
public async Task<IActionResult> DeleteAsync(long id)
{
var result = new ApiResult<bool>();
var data = _dbContext.LocaleResources.SingleOrDefault(p => p.Id == id);
if (data == null)
{
result = result.IsFail("数据不存在,请更换!");
return new JsonResult(result);
}
_dbContext.LocaleResources.Remove(data);
_dbContext.SaveChanges();
result = result.IsSuccess(true);
return new JsonResult(result);
}
/// <summary>
/// 重构多语言文件
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpPost("rebuild")]
public async Task<IActionResult> RebuidCultureFile(long id)
{
var result = new ApiResult<bool>();
var data = _dbContext.Languages.SingleOrDefault(p => p.Id == id);
if (data == null)
{
result = result.IsFail("数据不存在,请更换!");
return new JsonResult(result);
}
var wwwroot = _environment.WebRootPath;
var path = Path.Combine(wwwroot, "localization");
_backgroundService.RebuildLocalizationFile(path, data.Culture);
result = result.IsSuccess(true);
return new JsonResult(result);
}
/// <summary> /// <summary>
/// 获取对应的多语言文化数据的版本 /// 获取对应的多语言文化数据的版本
/// </summary> /// </summary>

View File

@@ -60,7 +60,6 @@ namespace Atomx.Admin.Extensions
language.Culture = "zh-cn"; language.Culture = "zh-cn";
language.CreateTime = DateTime.UtcNow; language.CreateTime = DateTime.UtcNow;
language.UpdateTime = DateTime.UtcNow; language.UpdateTime = DateTime.UtcNow;
language.Id = 1;
language.Name = "中文"; language.Name = "中文";
language.DisplayOrder = 0; language.DisplayOrder = 0;
language.Enabled = true; language.Enabled = true;

View File

@@ -61,7 +61,7 @@ namespace Atomx.Admin.Middlewares
{ {
logger?.LogDebug("ILocalizationProvider not registered in RequestServices"); logger?.LogDebug("ILocalizationProvider not registered in RequestServices");
} }
else if (providerObj is Atomx.Admin.Services.ServerLocalizationProvider provider) else if (providerObj is Atomx.Admin.Client.Services.LocalizationProvider provider)
{ {
logger?.LogDebug("Calling SetCultureForServer on LocalizationProvider with {Culture}", cultureName); logger?.LogDebug("Calling SetCultureForServer on LocalizationProvider with {Culture}", cultureName);
_= provider.SetCultureAsync(cultureName); _= provider.SetCultureAsync(cultureName);

View File

@@ -68,7 +68,7 @@ builder.Services.AddHttpContextAccessor();
builder.Services.AddHttpClientApiService(builder.Configuration["WebApi:ServerUrl"] ?? "http://localhost"); builder.Services.AddHttpClientApiService(builder.Configuration["WebApi:ServerUrl"] ?? "http://localhost");
// ע<><EFBFBD>ػ<EFBFBD><D8BB><EFBFBD><E1B9A9><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> // ע<><EFBFBD>ػ<EFBFBD><D8BB><EFBFBD><E1B9A9><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
builder.Services.AddScoped<ILocalizationProvider, ServerLocalizationProvider>(); builder.Services.AddScoped<ILocalizationProvider, LocalizationProvider>();
builder.Services.AddScoped<ILocalizationService, LocalizationService>(); builder.Services.AddScoped<ILocalizationService, LocalizationService>();
builder.Services.AddTransient(typeof(IStringLocalizer<>), typeof(JsonStringLocalizer<>)); builder.Services.AddTransient(typeof(IStringLocalizer<>), typeof(JsonStringLocalizer<>));

View File

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

View File

@@ -6,6 +6,12 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Remove="Utils\**" />
<EmbeddedResource Remove="Utils\**" />
<None Remove="Utils\**" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Mapster.DependencyInjection" Version="1.0.1" /> <PackageReference Include="Mapster.DependencyInjection" Version="1.0.1" />
</ItemGroup> </ItemGroup>
@@ -14,8 +20,4 @@
<ProjectReference Include="..\Atomx.Utils\Atomx.Utils.csproj" /> <ProjectReference Include="..\Atomx.Utils\Atomx.Utils.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Utils\" />
</ItemGroup>
</Project> </Project>

View File

@@ -30,7 +30,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 邮件地址 /// 邮件地址
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string Email { get; set; } = string.Empty; public string Email { get; set; } = string.Empty;
/// <summary> /// <summary>
@@ -42,7 +42,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 公司 /// 公司
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string Company { get; set; } = string.Empty; public string Company { get; set; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -24,7 +24,7 @@ namespace Atomx.Common.Entities
/// 地区名称 /// 地区名称
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -45,31 +45,31 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// Meta描述介绍 /// Meta描述介绍
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string MetaDescription { get; set; } = string.Empty; public string MetaDescription { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Meta关键词 /// Meta关键词
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string MetaKeywords { get; set; } = string.Empty; public string MetaKeywords { get; set; } = string.Empty;
/// <summary> /// <summary>
/// 过滤属性IDs /// 过滤属性IDs
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string FilterAttributes { get; set; } = string.Empty; public string FilterAttributes { get; set; } = string.Empty;
/// <summary> /// <summary>
/// 分类图片 /// 分类图片
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string Image { get; set; } = string.Empty; public string Image { get; set; } = string.Empty;
/// <summary> /// <summary>
/// 分类页 banner /// 分类页 banner
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string Banner { get; set; } = string.Empty; public string Banner { get; set; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -37,7 +37,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 国旗 /// 国旗
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string FlagImage { get; set; } = string.Empty; public string FlagImage { get; set; } = string.Empty;
/// <summary> /// <summary>
@@ -53,7 +53,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 多语言资源的版本,可以是时间戳或哈希 /// 多语言资源的版本,可以是时间戳或哈希
/// </summary> /// </summary>
[Column(TypeName = "varchar(25)")] [Column(TypeName = "varchar(256)")]
public string ResourceVersion { get; set; } = string.Empty; public string ResourceVersion { get; set; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -24,7 +24,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 资源名称 /// 资源名称
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -34,7 +34,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 数据KEY /// 数据KEY
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string Key { get; set; } = string.Empty; public string Key { get; set; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -48,7 +48,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// Meta标题 /// Meta标题
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string MetaKeywords { get; set; } = string.Empty; public string MetaKeywords { get; set; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -69,7 +69,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 备注说明 /// 备注说明
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string Note { get; set; } = string.Empty; public string Note { get; set; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -35,7 +35,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 分类图标 /// 分类图标
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string Icon { get; set; } = string.Empty; public string Icon { get; set; } = string.Empty;
/// <summary> /// <summary>
@@ -47,7 +47,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 菜单链接的URL /// 菜单链接的URL
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string Url { get; set; } = string.Empty; public string Url { get; set; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -75,7 +75,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 邮件地址 /// 邮件地址
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string Email { get; set; } = string.Empty; public string Email { get; set; } = string.Empty;
/// <summary> /// <summary>
@@ -87,7 +87,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 公司 /// 公司
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string Company { get; set; } = string.Empty; public string Company { get; set; } = string.Empty;
/// <summary> /// <summary>
@@ -303,13 +303,13 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 订单备注说明 /// 订单备注说明
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string? Remark { get; set; } = string.Empty; public string? Remark { get; set; } = string.Empty;
/// <summary> /// <summary>
/// 卖家下单备注信息 /// 卖家下单备注信息
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string? Message { get; set; } = string.Empty; public string? Message { get; set; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -74,7 +74,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 商品名称 /// 商品名称
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string Title { get; set; } = string.Empty; public string Title { get; set; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -44,7 +44,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 收款账号 /// 收款账号
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string Account { get; set; } = string.Empty; public string Account { get; set; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -22,7 +22,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 权限点说明 /// 权限点说明
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -56,7 +56,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 产品名称 /// 产品名称
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string Title { get; set; } = string.Empty; public string Title { get; set; } = string.Empty;
/// <summary> /// <summary>
@@ -80,13 +80,13 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 产品卖点 /// 产品卖点
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string? Feature { get; set; } = string.Empty; public string? Feature { get; set; } = string.Empty;
/// <summary> /// <summary>
/// 产品简介 /// 产品简介
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string? Description { get; set; } = string.Empty; public string? Description { get; set; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -110,7 +110,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 备注说明 /// 备注说明
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string Note { get; set; } = string.Empty; public string Note { get; set; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -40,7 +40,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 属性图片 /// 属性图片
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string Image { get; set; } = string.Empty; public string Image { get; set; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -85,7 +85,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 备注说明 /// 备注说明
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string Note { get; set; } = string.Empty; public string Note { get; set; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -50,7 +50,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// SEO Title /// SEO Title
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string MetaTitle { get; set; } = string.Empty; public string MetaTitle { get; set; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -29,7 +29,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 说明 /// 说明
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -21,7 +21,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 字段KEY /// 字段KEY
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string MetaKey { get; set; } = string.Empty; public string MetaKey { get; set; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -29,7 +29,7 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 备注说明 /// 备注说明
/// </summary> /// </summary>
[Column(TypeName = "varchar(255)")] [Column(TypeName = "varchar(256)")]
public string Note { get; set; } = string.Empty; public string Note { get; set; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -34,63 +34,97 @@ namespace Atomx.Core.Jos
try try
{ {
var translations = data.FromJson<Dictionary<string, string>>(); var translations = data.FromJson<Dictionary<string, string>>();
if (translations == null)
var fileName = $"{culture}.json";
var filePath = Path.Combine(path, fileName);
if (!Directory.Exists(path))
{ {
Directory.CreateDirectory(path); _logger.LogError("No translations provided for culture: {Culture}", culture);
_logger.LogInformation("Created Resources directory: {Path}", path);
} }
else
var fileData = new Dictionary<string, string>();
if (File.Exists(filePath))
{ {
var json = await File.ReadAllTextAsync(filePath);
fileData = JsonSerializer.Deserialize<Dictionary<string, string>>(json, new JsonSerializerOptions var fileName = $"{culture}.json";
var filePath = Path.Combine(path, fileName);
if (!Directory.Exists(path))
{ {
PropertyNamingPolicy = JsonNamingPolicy.CamelCase Directory.CreateDirectory(path);
}) ?? new Dictionary<string, string>(); _logger.LogInformation("Created Resources directory: {Path}", path);
}
var fileData = new Dictionary<string, string>();
if (File.Exists(filePath))
{
var json = await File.ReadAllTextAsync(filePath);
fileData = JsonSerializer.Deserialize<Dictionary<string, string>>(json, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
}) ?? new Dictionary<string, string>();
}
foreach (var item in translations)
{
fileData[item.Key] = item.Value;
}
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
var updatedJson = JsonSerializer.Serialize(fileData, options);
await File.WriteAllTextAsync(filePath, updatedJson);
// 更新文件后,更新数据库中的资源版本
string fileHash;
using (var sha256 = SHA256.Create())
using (var stream = File.OpenRead(filePath))
{
var hashBytes = sha256.ComputeHash(stream);
fileHash = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
Console.WriteLine(fileHash);
}
var language = _dbContext.Languages.FirstOrDefault(l => l.Culture == culture);
if (language != null)
{
language.UpdateTime = DateTime.UtcNow;
language.ResourceVersion = fileHash;
await _dbContext.SaveChangesAsync();
await _cacheService.GetLanguageById(language.Id, language);
}
_logger.LogInformation("Saved localization file for culture: {Culture} with {Count} translations. File hash: {Hash}",
culture, translations.Count, fileHash);
} }
foreach (var item in translations)
{
fileData[item.Key] = item.Value;
}
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
var updatedJson = JsonSerializer.Serialize(fileData, options);
await File.WriteAllTextAsync(filePath, updatedJson);
// 更新文件后,更新数据库中的资源版本
string fileHash;
using (var sha256 = SHA256.Create())
using (var stream = File.OpenRead(filePath))
{
var hashBytes = sha256.ComputeHash(stream);
fileHash = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
var language = _dbContext.Languages.FirstOrDefault(l => l.Culture == culture);
if (language != null)
{
language.UpdateTime = DateTime.UtcNow;
language.ResourceVersion = fileHash;
await _dbContext.SaveChangesAsync();
await _cacheService.GetLanguageById(language.Id, language);
}
_logger.LogInformation("Saved localization file for culture: {Culture} with {Count} translations. File hash: {Hash}",
culture, translations.Count, fileHash);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error saving localization file for culture: {Culture}", culture); _logger.LogError(ex, "Error saving localization file for culture: {Culture}", culture);
} }
} }
/// <summary>
/// 重构指定文化的本地化文件
/// </summary>
/// <param name="path"></param>
/// <param name="culture"></param>
[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
[DisableConcurrentExecution(60)]
public void RebuildCultureFile(string path, string culture)
{
var language = _dbContext.Languages.FirstOrDefault(l => l.Culture == culture);
if (language != null)
{
var translations = _dbContext.LocaleResources
.Where(lr => lr.LanguageId == language.Id)
.ToDictionary(lr => lr.Name, lr => lr.Value);
var data = JsonSerializer.Serialize(translations, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
});
_ = ExecuteAsync(path, culture, data);
}
_logger.LogError("Language not found for culture: {Culture}, cannot rebuild localization file.", culture);
}
} }
} }

View File

@@ -13,6 +13,16 @@ namespace Atomx.Core.Jos
/// <param name="data"></param> /// <param name="data"></param>
/// <returns></returns> /// <returns></returns>
string UpdateLocalizationFile(string path, string culture, string data); string UpdateLocalizationFile(string path, string culture, string data);
/// <summary>
/// 重构翻译本地化文件
/// </summary>
/// <param name="path"></param>
/// <param name="culture"></param>
/// <returns></returns>
string RebuildLocalizationFile(string path, string culture);
string SendSMSVerificationCode(string phoneNumber, string code, TimeSpan validDuration); string SendSMSVerificationCode(string phoneNumber, string code, TimeSpan validDuration);
} }
@@ -41,6 +51,18 @@ namespace Atomx.Core.Jos
return jobId; return jobId;
} }
/// <summary>
/// 重构翻译本地化文件
/// </summary>
/// <param name="path"></param>
/// <param name="culture"></param>
/// <returns></returns>
public string RebuildLocalizationFile(string path, string culture)
{
var jobId = _backgroundJobClient.Enqueue<LocalizationJob>(job => job.RebuildCultureFile(path, culture));
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;

View File

@@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Atomx.Data.Migrations namespace Atomx.Data.Migrations
{ {
[DbContext(typeof(DataContext))] [DbContext(typeof(DataContext))]
[Migration("20251203190956_0.1")] [Migration("20251214094758_0.1")]
partial class _01 partial class _01
{ {
/// <inheritdoc /> /// <inheritdoc />
@@ -20,7 +20,7 @@ namespace Atomx.Data.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "10.0.0") .HasAnnotation("ProductVersion", "10.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -43,7 +43,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Company") b.Property<string>("Company")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<int>("Count") b.Property<int>("Count")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -60,7 +60,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Email") b.Property<string>("Email")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<string>("FullAddress") b.Property<string>("FullAddress")
.IsRequired() .IsRequired()
@@ -254,7 +254,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<int>("NumericISOCode") b.Property<int>("NumericISOCode")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -282,7 +282,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Banner") b.Property<string>("Banner")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<int>("Count") b.Property<int>("Count")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -301,22 +301,22 @@ namespace Atomx.Data.Migrations
b.Property<string>("FilterAttributes") b.Property<string>("FilterAttributes")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<string>("Image") b.Property<string>("Image")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<bool>("IsNode") b.Property<bool>("IsNode")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<string>("MetaDescription") b.Property<string>("MetaDescription")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<string>("MetaKeywords") b.Property<string>("MetaKeywords")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
@@ -470,7 +470,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("FlagImage") b.Property<string>("FlagImage")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
@@ -478,7 +478,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("ResourceVersion") b.Property<string>("ResourceVersion")
.IsRequired() .IsRequired()
.HasColumnType("varchar(25)"); .HasColumnType("varchar(256)");
b.Property<string>("Title") b.Property<string>("Title")
.IsRequired() .IsRequired()
@@ -502,7 +502,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<DateTime?>("UpdateTime") b.Property<DateTime?>("UpdateTime")
.HasColumnType("timestamptz"); .HasColumnType("timestamptz");
@@ -526,7 +526,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Key") b.Property<string>("Key")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<int>("LanguageNumber") b.Property<int>("LanguageNumber")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -566,7 +566,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Icon") b.Property<string>("Icon")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<bool>("IsLink") b.Property<bool>("IsLink")
.HasColumnType("boolean"); .HasColumnType("boolean");
@@ -594,7 +594,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Url") b.Property<string>("Url")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.HasKey("Id"); b.HasKey("Id");
@@ -646,7 +646,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Account") b.Property<string>("Account")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<string>("Config") b.Property<string>("Config")
.IsRequired() .IsRequired()
@@ -698,7 +698,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Description") b.Property<string>("Description")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
@@ -736,7 +736,7 @@ namespace Atomx.Data.Migrations
.HasColumnType("timestamptz"); .HasColumnType("timestamptz");
b.Property<string>("Description") b.Property<string>("Description")
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<int>("DisplayOrder") b.Property<int>("DisplayOrder")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -746,7 +746,7 @@ namespace Atomx.Data.Migrations
.HasColumnType("text"); .HasColumnType("text");
b.Property<string>("Feature") b.Property<string>("Feature")
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<string>("Image") b.Property<string>("Image")
.HasColumnType("varchar(256)"); .HasColumnType("varchar(256)");
@@ -804,7 +804,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Title") b.Property<string>("Title")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<DateTime?>("UpdateTime") b.Property<DateTime?>("UpdateTime")
.HasColumnType("timestamptz"); .HasColumnType("timestamptz");
@@ -890,7 +890,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Note") b.Property<string>("Note")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<decimal>("Price") b.Property<decimal>("Price")
.HasColumnType("decimal(18,4)"); .HasColumnType("decimal(18,4)");
@@ -1001,7 +1001,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Image") b.Property<string>("Image")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<bool>("IsAddWeight") b.Property<bool>("IsAddWeight")
.HasColumnType("boolean"); .HasColumnType("boolean");
@@ -1086,7 +1086,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Note") b.Property<string>("Note")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<long?>("Operator") b.Property<long?>("Operator")
.HasColumnType("bigint"); .HasColumnType("bigint");
@@ -1170,7 +1170,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Description") b.Property<string>("Description")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<bool>("Enabled") b.Property<bool>("Enabled")
.HasColumnType("boolean"); .HasColumnType("boolean");

View File

@@ -19,9 +19,9 @@ namespace Atomx.Data.Migrations
Id = table.Column<long>(type: "bigint", nullable: false), Id = table.Column<long>(type: "bigint", nullable: false),
UserId = table.Column<long>(type: "bigint", nullable: false), UserId = table.Column<long>(type: "bigint", nullable: false),
Name = table.Column<string>(type: "varchar(128)", nullable: false), Name = table.Column<string>(type: "varchar(128)", nullable: false),
Email = table.Column<string>(type: "varchar(255)", nullable: false), Email = table.Column<string>(type: "varchar(256)", nullable: false),
Phone = table.Column<string>(type: "varchar(20)", nullable: false), Phone = table.Column<string>(type: "varchar(20)", nullable: false),
Company = table.Column<string>(type: "varchar(255)", nullable: false), Company = table.Column<string>(type: "varchar(256)", nullable: false),
CountryId = table.Column<long>(type: "bigint", nullable: false), CountryId = table.Column<long>(type: "bigint", nullable: false),
Country = table.Column<string>(type: "varchar(50)", nullable: false), Country = table.Column<string>(type: "varchar(50)", nullable: false),
ProvinceId = table.Column<long>(type: "bigint", nullable: false), ProvinceId = table.Column<long>(type: "bigint", nullable: false),
@@ -102,7 +102,7 @@ namespace Atomx.Data.Migrations
{ {
Id = table.Column<long>(type: "bigint", nullable: false), Id = table.Column<long>(type: "bigint", nullable: false),
ParentId = table.Column<long>(type: "bigint", nullable: false), ParentId = table.Column<long>(type: "bigint", nullable: false),
Name = table.Column<string>(type: "varchar(255)", nullable: false), Name = table.Column<string>(type: "varchar(256)", nullable: false),
Initial = table.Column<string>(type: "varchar(1)", nullable: false), Initial = table.Column<string>(type: "varchar(1)", nullable: false),
TwoLetterISOCode = table.Column<string>(type: "varchar(2)", nullable: false), TwoLetterISOCode = table.Column<string>(type: "varchar(2)", nullable: false),
ThreeLetterISOCode = table.Column<string>(type: "varchar(3)", nullable: false), ThreeLetterISOCode = table.Column<string>(type: "varchar(3)", nullable: false),
@@ -126,11 +126,11 @@ namespace Atomx.Data.Migrations
ParentId = table.Column<long>(type: "bigint", nullable: false), ParentId = table.Column<long>(type: "bigint", nullable: false),
Name = table.Column<string>(type: "varchar(25)", nullable: false), Name = table.Column<string>(type: "varchar(25)", nullable: false),
Slug = table.Column<string>(type: "varchar(50)", nullable: false), Slug = table.Column<string>(type: "varchar(50)", nullable: false),
MetaDescription = table.Column<string>(type: "varchar(255)", nullable: false), MetaDescription = table.Column<string>(type: "varchar(256)", nullable: false),
MetaKeywords = table.Column<string>(type: "varchar(255)", nullable: false), MetaKeywords = table.Column<string>(type: "varchar(256)", nullable: false),
FilterAttributes = table.Column<string>(type: "varchar(255)", nullable: false), FilterAttributes = table.Column<string>(type: "varchar(256)", nullable: false),
Image = table.Column<string>(type: "varchar(255)", nullable: false), Image = table.Column<string>(type: "varchar(256)", nullable: false),
Banner = table.Column<string>(type: "varchar(255)", nullable: false), Banner = table.Column<string>(type: "varchar(256)", nullable: false),
IsNode = table.Column<bool>(type: "boolean", nullable: false), IsNode = table.Column<bool>(type: "boolean", nullable: false),
Enabled = table.Column<bool>(type: "boolean", nullable: false), Enabled = table.Column<bool>(type: "boolean", nullable: false),
Depth = table.Column<int>(type: "integer", nullable: false), Depth = table.Column<int>(type: "integer", nullable: false),
@@ -200,10 +200,10 @@ namespace Atomx.Data.Migrations
Name = table.Column<string>(type: "varchar(50)", nullable: false), Name = table.Column<string>(type: "varchar(50)", nullable: false),
Title = table.Column<string>(type: "varchar(50)", nullable: false), Title = table.Column<string>(type: "varchar(50)", nullable: false),
Culture = table.Column<string>(type: "varchar(25)", nullable: false), Culture = table.Column<string>(type: "varchar(25)", nullable: false),
FlagImage = table.Column<string>(type: "varchar(255)", nullable: false), FlagImage = table.Column<string>(type: "varchar(256)", nullable: false),
DisplayOrder = table.Column<int>(type: "integer", nullable: false), DisplayOrder = table.Column<int>(type: "integer", nullable: false),
Enabled = table.Column<bool>(type: "boolean", nullable: false), Enabled = table.Column<bool>(type: "boolean", nullable: false),
ResourceVersion = table.Column<string>(type: "varchar(25)", nullable: false), ResourceVersion = table.Column<string>(type: "varchar(256)", nullable: false),
CreateTime = table.Column<DateTime>(type: "timestamptz", nullable: false), CreateTime = table.Column<DateTime>(type: "timestamptz", nullable: false),
UpdateTime = table.Column<DateTime>(type: "timestamptz", nullable: true) UpdateTime = table.Column<DateTime>(type: "timestamptz", nullable: true)
}, },
@@ -218,7 +218,7 @@ namespace Atomx.Data.Migrations
{ {
Id = table.Column<long>(type: "bigint", nullable: false), Id = table.Column<long>(type: "bigint", nullable: false),
LanguageId = table.Column<int>(type: "integer", nullable: false), LanguageId = table.Column<int>(type: "integer", nullable: false),
Name = table.Column<string>(type: "varchar(255)", nullable: false), Name = table.Column<string>(type: "varchar(256)", nullable: false),
Value = table.Column<string>(type: "varchar(1000)", nullable: false), Value = table.Column<string>(type: "varchar(1000)", nullable: false),
UpdateTime = table.Column<DateTime>(type: "timestamptz", nullable: true) UpdateTime = table.Column<DateTime>(type: "timestamptz", nullable: true)
}, },
@@ -235,7 +235,7 @@ namespace Atomx.Data.Migrations
LanguageNumber = table.Column<int>(type: "integer", nullable: false), LanguageNumber = table.Column<int>(type: "integer", nullable: false),
Type = table.Column<int>(type: "integer", nullable: false), Type = table.Column<int>(type: "integer", nullable: false),
EntityId = table.Column<long>(type: "bigint", nullable: false), EntityId = table.Column<long>(type: "bigint", nullable: false),
Key = table.Column<string>(type: "varchar(255)", nullable: false), Key = table.Column<string>(type: "varchar(256)", nullable: false),
Value = table.Column<string>(type: "text", nullable: false) Value = table.Column<string>(type: "text", nullable: false)
}, },
constraints: table => constraints: table =>
@@ -251,9 +251,9 @@ namespace Atomx.Data.Migrations
Type = table.Column<int>(type: "integer", nullable: false), Type = table.Column<int>(type: "integer", nullable: false),
ParentId = table.Column<long>(type: "bigint", nullable: false), ParentId = table.Column<long>(type: "bigint", nullable: false),
Name = table.Column<string>(type: "varchar(50)", nullable: false), Name = table.Column<string>(type: "varchar(50)", nullable: false),
Icon = table.Column<string>(type: "varchar(255)", nullable: false), Icon = table.Column<string>(type: "varchar(256)", nullable: false),
Key = table.Column<string>(type: "varchar(50)", nullable: false), Key = table.Column<string>(type: "varchar(50)", nullable: false),
Url = table.Column<string>(type: "varchar(255)", nullable: false), Url = table.Column<string>(type: "varchar(256)", nullable: false),
Code = table.Column<string>(type: "varchar(50)", nullable: false), Code = table.Column<string>(type: "varchar(50)", nullable: false),
IsLink = table.Column<bool>(type: "boolean", nullable: false), IsLink = table.Column<bool>(type: "boolean", nullable: false),
Enabled = table.Column<bool>(type: "boolean", nullable: false), Enabled = table.Column<bool>(type: "boolean", nullable: false),
@@ -297,7 +297,7 @@ namespace Atomx.Data.Migrations
Name = table.Column<string>(type: "varchar(20)", nullable: false), Name = table.Column<string>(type: "varchar(20)", nullable: false),
Title = table.Column<string>(type: "varchar(20)", nullable: false), Title = table.Column<string>(type: "varchar(20)", nullable: false),
Description = table.Column<string>(type: "varchar(512)", nullable: false), Description = table.Column<string>(type: "varchar(512)", nullable: false),
Account = table.Column<string>(type: "varchar(255)", nullable: false), Account = table.Column<string>(type: "varchar(256)", nullable: false),
Config = table.Column<string>(type: "text", nullable: false), Config = table.Column<string>(type: "text", nullable: false),
DisplayOrder = table.Column<int>(type: "integer", nullable: false), DisplayOrder = table.Column<int>(type: "integer", nullable: false),
Status = table.Column<int>(type: "integer", nullable: false), Status = table.Column<int>(type: "integer", nullable: false),
@@ -315,7 +315,7 @@ namespace Atomx.Data.Migrations
{ {
Id = table.Column<long>(type: "bigint", nullable: false), Id = table.Column<long>(type: "bigint", nullable: false),
Name = table.Column<string>(type: "varchar(128)", nullable: false), Name = table.Column<string>(type: "varchar(128)", nullable: false),
Description = table.Column<string>(type: "varchar(255)", nullable: false), Description = table.Column<string>(type: "varchar(256)", nullable: false),
Category = table.Column<string>(type: "varchar(128)", nullable: false) Category = table.Column<string>(type: "varchar(128)", nullable: false)
}, },
constraints: table => constraints: table =>
@@ -344,7 +344,7 @@ namespace Atomx.Data.Migrations
MarketPrice = table.Column<decimal>(type: "numeric(18,4)", nullable: false), MarketPrice = table.Column<decimal>(type: "numeric(18,4)", nullable: false),
Price = table.Column<decimal>(type: "numeric(18,4)", nullable: false), Price = table.Column<decimal>(type: "numeric(18,4)", nullable: false),
Mark = table.Column<int>(type: "integer", nullable: false), Mark = table.Column<int>(type: "integer", nullable: false),
Note = table.Column<string>(type: "varchar(255)", nullable: false), Note = table.Column<string>(type: "varchar(256)", nullable: false),
Enabled = table.Column<bool>(type: "boolean", nullable: false), Enabled = table.Column<bool>(type: "boolean", nullable: false),
UpdateTime = table.Column<DateTime>(type: "timestamptz", nullable: true) UpdateTime = table.Column<DateTime>(type: "timestamptz", nullable: true)
}, },
@@ -419,7 +419,7 @@ namespace Atomx.Data.Migrations
OptionId = table.Column<long>(type: "bigint", nullable: false), OptionId = table.Column<long>(type: "bigint", nullable: false),
ProductAttributeRelationId = table.Column<long>(type: "bigint", nullable: false), ProductAttributeRelationId = table.Column<long>(type: "bigint", nullable: false),
Name = table.Column<string>(type: "varchar(50)", nullable: false), Name = table.Column<string>(type: "varchar(50)", nullable: false),
Image = table.Column<string>(type: "varchar(255)", nullable: false), Image = table.Column<string>(type: "varchar(256)", nullable: false),
Weight = table.Column<decimal>(type: "numeric(18,4)", nullable: false), Weight = table.Column<decimal>(type: "numeric(18,4)", nullable: false),
WeightUnit = table.Column<int>(type: "integer", nullable: false), WeightUnit = table.Column<int>(type: "integer", nullable: false),
IsAddWeight = table.Column<bool>(type: "boolean", nullable: false), IsAddWeight = table.Column<bool>(type: "boolean", nullable: false),
@@ -466,7 +466,7 @@ namespace Atomx.Data.Migrations
Weight = table.Column<decimal>(type: "numeric(18,4)", nullable: false), Weight = table.Column<decimal>(type: "numeric(18,4)", nullable: false),
BeforeStock = table.Column<int>(type: "integer", nullable: false), BeforeStock = table.Column<int>(type: "integer", nullable: false),
AfterStock = table.Column<int>(type: "integer", nullable: false), AfterStock = table.Column<int>(type: "integer", nullable: false),
Note = table.Column<string>(type: "varchar(255)", nullable: false), Note = table.Column<string>(type: "varchar(256)", nullable: false),
CreateTime = table.Column<DateTime>(type: "timestamptz", nullable: false) CreateTime = table.Column<DateTime>(type: "timestamptz", nullable: false)
}, },
constraints: table => constraints: table =>
@@ -486,12 +486,12 @@ namespace Atomx.Data.Migrations
CategoryPath = table.Column<string>(type: "varchar(200)", nullable: true), CategoryPath = table.Column<string>(type: "varchar(200)", nullable: true),
CategoryId = table.Column<long>(type: "bigint", nullable: false), CategoryId = table.Column<long>(type: "bigint", nullable: false),
ManufacturerId = table.Column<long>(type: "bigint", nullable: false), ManufacturerId = table.Column<long>(type: "bigint", nullable: false),
Title = table.Column<string>(type: "varchar(255)", nullable: false), Title = table.Column<string>(type: "varchar(256)", nullable: false),
SIN = table.Column<string>(type: "varchar(20)", nullable: true), SIN = table.Column<string>(type: "varchar(20)", nullable: true),
Image = table.Column<string>(type: "varchar(256)", nullable: true), Image = table.Column<string>(type: "varchar(256)", nullable: true),
Photos = table.Column<string>(type: "text", nullable: true), Photos = table.Column<string>(type: "text", nullable: true),
Feature = table.Column<string>(type: "varchar(255)", nullable: true), Feature = table.Column<string>(type: "varchar(256)", nullable: true),
Description = table.Column<string>(type: "varchar(255)", nullable: true), Description = table.Column<string>(type: "varchar(256)", nullable: true),
SpecificationJson = table.Column<string>(type: "text", nullable: true), SpecificationJson = table.Column<string>(type: "text", nullable: true),
Weight = table.Column<decimal>(type: "numeric(6,2)", nullable: true), Weight = table.Column<decimal>(type: "numeric(6,2)", nullable: true),
MinWeight = table.Column<decimal>(type: "numeric(6,2)", nullable: true), MinWeight = table.Column<decimal>(type: "numeric(6,2)", nullable: true),
@@ -544,7 +544,7 @@ namespace Atomx.Data.Migrations
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Type = table.Column<int>(type: "integer", nullable: false), Type = table.Column<int>(type: "integer", nullable: false),
Name = table.Column<string>(type: "text", nullable: false), Name = table.Column<string>(type: "text", nullable: false),
Description = table.Column<string>(type: "varchar(255)", nullable: false), Description = table.Column<string>(type: "varchar(256)", nullable: false),
Permission = table.Column<string>(type: "text", nullable: false), Permission = table.Column<string>(type: "text", nullable: false),
IsSystemRole = table.Column<bool>(type: "boolean", nullable: false), IsSystemRole = table.Column<bool>(type: "boolean", nullable: false),
Count = table.Column<int>(type: "integer", nullable: false), Count = table.Column<int>(type: "integer", nullable: false),

View File

@@ -17,7 +17,7 @@ namespace Atomx.Data.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "10.0.0") .HasAnnotation("ProductVersion", "10.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -40,7 +40,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Company") b.Property<string>("Company")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<int>("Count") b.Property<int>("Count")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -57,7 +57,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Email") b.Property<string>("Email")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<string>("FullAddress") b.Property<string>("FullAddress")
.IsRequired() .IsRequired()
@@ -251,7 +251,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<int>("NumericISOCode") b.Property<int>("NumericISOCode")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -279,7 +279,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Banner") b.Property<string>("Banner")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<int>("Count") b.Property<int>("Count")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -298,22 +298,22 @@ namespace Atomx.Data.Migrations
b.Property<string>("FilterAttributes") b.Property<string>("FilterAttributes")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<string>("Image") b.Property<string>("Image")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<bool>("IsNode") b.Property<bool>("IsNode")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<string>("MetaDescription") b.Property<string>("MetaDescription")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<string>("MetaKeywords") b.Property<string>("MetaKeywords")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
@@ -467,7 +467,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("FlagImage") b.Property<string>("FlagImage")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
@@ -475,7 +475,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("ResourceVersion") b.Property<string>("ResourceVersion")
.IsRequired() .IsRequired()
.HasColumnType("varchar(25)"); .HasColumnType("varchar(256)");
b.Property<string>("Title") b.Property<string>("Title")
.IsRequired() .IsRequired()
@@ -499,7 +499,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<DateTime?>("UpdateTime") b.Property<DateTime?>("UpdateTime")
.HasColumnType("timestamptz"); .HasColumnType("timestamptz");
@@ -523,7 +523,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Key") b.Property<string>("Key")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<int>("LanguageNumber") b.Property<int>("LanguageNumber")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -563,7 +563,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Icon") b.Property<string>("Icon")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<bool>("IsLink") b.Property<bool>("IsLink")
.HasColumnType("boolean"); .HasColumnType("boolean");
@@ -591,7 +591,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Url") b.Property<string>("Url")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.HasKey("Id"); b.HasKey("Id");
@@ -643,7 +643,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Account") b.Property<string>("Account")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<string>("Config") b.Property<string>("Config")
.IsRequired() .IsRequired()
@@ -695,7 +695,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Description") b.Property<string>("Description")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
@@ -733,7 +733,7 @@ namespace Atomx.Data.Migrations
.HasColumnType("timestamptz"); .HasColumnType("timestamptz");
b.Property<string>("Description") b.Property<string>("Description")
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<int>("DisplayOrder") b.Property<int>("DisplayOrder")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -743,7 +743,7 @@ namespace Atomx.Data.Migrations
.HasColumnType("text"); .HasColumnType("text");
b.Property<string>("Feature") b.Property<string>("Feature")
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<string>("Image") b.Property<string>("Image")
.HasColumnType("varchar(256)"); .HasColumnType("varchar(256)");
@@ -801,7 +801,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Title") b.Property<string>("Title")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<DateTime?>("UpdateTime") b.Property<DateTime?>("UpdateTime")
.HasColumnType("timestamptz"); .HasColumnType("timestamptz");
@@ -887,7 +887,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Note") b.Property<string>("Note")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<decimal>("Price") b.Property<decimal>("Price")
.HasColumnType("decimal(18,4)"); .HasColumnType("decimal(18,4)");
@@ -998,7 +998,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Image") b.Property<string>("Image")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<bool>("IsAddWeight") b.Property<bool>("IsAddWeight")
.HasColumnType("boolean"); .HasColumnType("boolean");
@@ -1083,7 +1083,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Note") b.Property<string>("Note")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<long?>("Operator") b.Property<long?>("Operator")
.HasColumnType("bigint"); .HasColumnType("bigint");
@@ -1167,7 +1167,7 @@ namespace Atomx.Data.Migrations
b.Property<string>("Description") b.Property<string>("Description")
.IsRequired() .IsRequired()
.HasColumnType("varchar(255)"); .HasColumnType("varchar(256)");
b.Property<bool>("Enabled") b.Property<bool>("Enabled")
.HasColumnType("boolean"); .HasColumnType("boolean");