fix localization

This commit is contained in:
yxw
2025-12-09 19:10:10 +08:00
parent 2318dff192
commit ed2e3ecd24
9 changed files with 81 additions and 131 deletions

View File

@@ -8,6 +8,13 @@
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode> <StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Remove="wwwroot\localization\**" />
<Content Remove="wwwroot\localization\**" />
<EmbeddedResource Remove="wwwroot\localization\**" />
<None Remove="wwwroot\localization\**" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AntDesign" Version="1.5.0" /> <PackageReference Include="AntDesign" Version="1.5.0" />
<PackageReference Include="AntDesign.ProLayout" Version="1.4.0" /> <PackageReference Include="AntDesign.ProLayout" Version="1.4.0" />
@@ -26,8 +33,4 @@
<ProjectReference Include="..\..\Atomx.Common\Atomx.Common.csproj" /> <ProjectReference Include="..\..\Atomx.Common\Atomx.Common.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\localization\" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,12 +1,9 @@
@page "/account/login" @page "/account/login"
@page "/{locale}/account/login" @page "/{locale}/account/login"
@using System.Text.Json
@layout EmptyLayout @layout EmptyLayout
@inject ILogger<Login> Logger @inject ILogger<Login> Logger
@inject IJSRuntime JS
@using Microsoft.Extensions.Localization
@inject IStringLocalizer<Login> L @inject IStringLocalizer<Login> L
@inject Atomx.Admin.Client.Services.ILocalizationProvider LocalizationProvider
<PageTitle>@L["login.title"]</PageTitle> <PageTitle>@L["login.title"]</PageTitle>
@@ -144,41 +141,30 @@ else
if (!OperatingSystem.IsBrowser()) if (!OperatingSystem.IsBrowser())
{ {
// Server 模式:使用浏览器发起的 fetch通过 JS并携带 credentials: 'include' // Server 模式:使用浏览器发起的 fetch通过 JS并携带 credentials: 'include'
Logger.LogInformation("Server 模式,使用浏览器 fetch 登录"); var jsResult = await JS.InvokeAsync<JsonElement>("ajax.Post", api, login);
var jsResult = await JS.InvokeAsync<JsonElement>("__atomx_post_json", api, login); var result = jsResult.ToJson().FromJson<ApiResult<AuthResponse>>();
if (result != null && result.Success)
var success = jsResult.TryGetProperty("success", out var sprop) && sprop.GetBoolean();
if (success && jsResult.TryGetProperty("data", out var dprop) && dprop.ValueKind == JsonValueKind.Object)
{ {
var token = dprop.TryGetProperty("token", out var t) ? t.GetString() ?? string.Empty : string.Empty; var auth = result.Data;
var refresh = dprop.TryGetProperty("refreshToken", out var r) ? r.GetString() ?? string.Empty : string.Empty; await localStorage.SetItemAsync(StorageKeys.AccessToken, auth.Token);
await localStorage.SetItemAsync(StorageKeys.RefreshToken, auth.RefreshToken);
// WASM 的 localStorage 在 Server Circuit 中无意义兼容auto模式写入 localStorage。 if (AuthStateProvider is PersistentAuthenticationStateProvider provider)
try
{ {
await localStorage.SetItemAsync(StorageKeys.AccessToken, token); provider.UpdateAuthenticationState(auth.Token);
await localStorage.SetItemAsync(StorageKeys.RefreshToken, refresh);
if (AuthStateProvider is PersistentAuthenticationStateProvider provider)
{
provider.UpdateAuthenticationState(token);
}
} }
catch { }
// 浏览器已通过 fetch 收到 Set-Cookie强制重载使 Circuit 使用新 Cookie。
Logger.LogInformation($"登录成功server 跳转: {ReturnUrl}"); Logger.LogInformation($"登录成功server 跳转: {ReturnUrl}");
Navigation.NavigateTo(ReturnUrl ?? "/", forceLoad: true); Navigation.NavigateTo(ReturnUrl ?? "/", forceLoad: true);
} }
else else
{ {
var msg = jsResult.TryGetProperty("message", out var m) ? m.GetString() ?? "登录失败" : "登录失败"; ModalService.Error(new ConfirmOptions() { Title = "提示", Content = result.Message });
ModalService.Error(new ConfirmOptions() { Title = "提示", Content = msg });
} }
} }
else else
{ {
// Wasm 模式:继续使用 HttpService之前逻辑保存 localStorage 并更新 AuthStateProvider // Wasm 模式:保存 localStorage 并更新 AuthStateProvider
var result = await HttpService.Post<ApiResult<AuthResponse>>(api, login); var result = await HttpService.Post<ApiResult<AuthResponse>>(api, login);
if (result.Success && result.Data != null) if (result.Success && result.Data != null)
{ {
@@ -225,35 +211,4 @@ else
login.Account = "admin"; login.Account = "admin";
login.Password = "admin888"; login.Password = "admin888";
} }
}
private string GetShortCulture(string current)
{
if (string.IsNullOrEmpty(current)) return current;
if (current.StartsWith("zh", StringComparison.OrdinalIgnoreCase)) return "zh";
if (current.StartsWith("en", StringComparison.OrdinalIgnoreCase)) return "en";
var prefix = current.Split('-', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
return prefix ?? current;
}
}
@* 页面内 JS 辅助:用于在 Server 模式下从浏览器发起 POST 并携带凭证,使浏览器接收 Set-Cookie *@
<script>
window.__atomx_post_json = async function (url, data) {
try {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(data)
});
const text = await res.text();
try {
return JSON.parse(text);
} catch {
return { success: res.ok, message: text };
}
} catch (err) {
return { success: false, message: err?.toString() ?? 'network error' };
}
};
</script>

View File

@@ -1,14 +1,12 @@
using Atomx.Admin.Client.Services; using Atomx.Admin.Client.Services;
using Atomx.Admin.Client.Utils; using Atomx.Admin.Client.Utils;
using Atomx.Admin.Client.Validators;
using Blazored.LocalStorage; using Blazored.LocalStorage;
using FluentValidation;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using System.Net.Http;
using FluentValidation;
using System.Linq;
using System.Reflection;
var builder = WebAssemblyHostBuilder.CreateDefault(args); var builder = WebAssemblyHostBuilder.CreateDefault(args);
@@ -70,7 +68,7 @@ builder.Services.AddScoped<HttpService>(sp =>
return new HttpService(httpClient, httpContextAccessor); return new HttpService(httpClient, httpContextAccessor);
}); });
builder.Services.AddValidatorsFromAssembly(typeof(Atomx.Admin.Client.Validators.LoginModelValidator).Assembly); builder.Services.AddValidatorsFromAssembly(typeof(LoginModelValidator).Assembly);
builder.Services.AddAntDesign(); builder.Services.AddAntDesign();

View File

@@ -300,7 +300,7 @@ namespace Atomx.Admin.Client.Services
private async Task SetCultureInternalAsync(string cultureFull, bool persistCookie) private async Task SetCultureInternalAsync(string cultureFull, bool persistCookie)
{ {
_logger?.LogDebug("SetCultureInternalAsync start: {Culture}, persist={Persist}", cultureFull, persistCookie); //_logger?.LogDebug("<22><><EFBFBD><EFBFBD><EFBFBD>ڲ<EFBFBD><DAB2>Ļ<EFBFBD><C4BB><EFBFBD><ECB2BD>ʼ: {Culture}, <EFBFBD>־û<EFBFBD>={Persist}", cultureFull, persistCookie);
await EnsureCultureLoadedAsync(cultureFull); await EnsureCultureLoadedAsync(cultureFull);
try try

View File

@@ -1,5 +1,6 @@
@using System.Net.Http @using System.Net.Http
@using System.Net.Http.Json @using System.Net.Http.Json
@using System.Text.Json
@using System.Security.Claims @using System.Security.Claims
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Authorization
@@ -32,10 +33,11 @@
@using Blazored.FluentValidation @using Blazored.FluentValidation
@inject IJSRuntime JS
@inject ILocalStorageService localStorage @inject ILocalStorageService localStorage
@inject ILocalizationProvider LocalizationProvider
@inject AuthenticationStateProvider AuthStateProvider @inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject HttpService HttpService @inject HttpService HttpService
@inject MessageService MessageService @inject MessageService MessageService
@inject ModalService ModalService @inject ModalService ModalService

View File

@@ -1,34 +1,58 @@
window.cookies = { window.cookies = {
Read: function (name) { Read: function (name) {
try { try {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
if (match) return decodeURIComponent(match[2]); if (match) return decodeURIComponent(match[2]);
return ''; return '';
} catch (e) { } catch (e) {
return ''; return '';
}
},
Write: function (name, value, expiresIso) {
try {
var expires = '';
if (expiresIso) {
expires = '; expires=' + new Date(expiresIso).toUTCString();
}
document.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/';
} catch (e) { }
},
Delete: function (cookie_name) {
document.cookie = cookie_name + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
}
};
window.ajax = {
Post: async function (url, data) {
try {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(data)
});
const text = await res.text();
try {
return JSON.parse(text);
} catch {
return { success: res.ok, message: text };
}
} catch (err) {
return { success: false, message: err?.toString() ?? 'network error' };
}
} }
},
Write: function (name, value, expiresIso) {
try {
var expires = '';
if (expiresIso) {
expires = '; expires=' + new Date(expiresIso).toUTCString();
}
document.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/';
} catch (e) { }
}
}; };
window.getBrowserLanguage = function () { window.getBrowserLanguage = function () {
try { try {
return (navigator.languages && navigator.languages[0]) || navigator.language || navigator.userLanguage || ''; return (navigator.languages && navigator.languages[0]) || navigator.language || navigator.userLanguage || '';
} catch (e) { } catch (e) {
return ''; return '';
} }
}; };
window.setHtmlLang = function (lang) { window.setHtmlLang = function (lang) {
try { try {
if (document && document.documentElement) document.documentElement.lang = lang || ''; if (document && document.documentElement) document.documentElement.lang = lang || '';
} catch (e) { } } catch (e) { }
}; };

View File

@@ -8,9 +8,13 @@
<ItemGroup> <ItemGroup>
<Compile Remove="Resources\**" /> <Compile Remove="Resources\**" />
<Compile Remove="wwwroot\js\**" />
<Content Remove="Resources\**" /> <Content Remove="Resources\**" />
<Content Remove="wwwroot\js\**" />
<EmbeddedResource Remove="Resources\**" /> <EmbeddedResource Remove="Resources\**" />
<EmbeddedResource Remove="wwwroot\js\**" />
<None Remove="Resources\**" /> <None Remove="Resources\**" />
<None Remove="wwwroot\js\**" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -43,8 +47,4 @@
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\js\" />
</ItemGroup>
</Project> </Project>

View File

@@ -155,38 +155,6 @@ builder.Services.AddValidatorsFromAssembly(typeof(Atomx.Admin.Client.Validators.
var app = builder.Build(); var app = builder.Build();
// Preload localization files on startup to ensure server-side rendering finds translations
try
{
using var scope = app.Services.CreateScope();
var logger = scope.ServiceProvider.GetService<ILoggerFactory>()?.CreateLogger("LocalizationStartup");
var provider = scope.ServiceProvider.GetService<ILocalizationProvider>();
if (provider != null)
{
logger?.LogInformation("Preloading localization files for server startup");
// preload supported cultures
var supported = new[] { "zh-Hans", "en-US" };
foreach (var c in supported)
{
try
{
provider.LoadCultureAsync(c).GetAwaiter().GetResult();
logger?.LogInformation("Preloaded localization for {Culture}", c);
}
catch (Exception ex)
{
logger?.LogWarning(ex, "Failed to preload localization for {Culture}", c);
}
}
}
}
catch (Exception ex)
{
// don't block startup
var l = app.Services.GetService<ILoggerFactory>()?.CreateLogger("LocalizationStartup");
l?.LogWarning(ex, "Failed to run localization preload");
}
app.AddDataMigrate(); app.AddDataMigrate();
// Forwarded headers<72><73><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> // Forwarded headers<72><73><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>

View File

@@ -68,7 +68,7 @@
}, },
"Seq": { "Seq": {
"ServerUrl": "http://log.firestones.cn/", "ServerUrl": "http://log.atomlust.com/",
"ApiKey": "bBWmvSE2LJh4KsMeidvF", "ApiKey": "bBWmvSE2LJh4KsMeidvF",
"MinimumLevel": "Warning", "MinimumLevel": "Warning",
"LevelOverride": { "LevelOverride": {