实现多语言切换
This commit is contained in:
@@ -25,4 +25,8 @@
|
|||||||
<ProjectReference Include="..\..\Atomx.Common\Atomx.Common.csproj" />
|
<ProjectReference Include="..\..\Atomx.Common\Atomx.Common.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="wwwroot\localization\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
82
Atomx.Admin/Atomx.Admin.Client/Components/LangSelector.razor
Normal file
82
Atomx.Admin/Atomx.Admin.Client/Components/LangSelector.razor
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
@inject ILocalizationProvider LocalizationProvider
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
|
||||||
|
<select @onchange="OnChange" value="@selected">
|
||||||
|
@foreach (var item in options)
|
||||||
|
{
|
||||||
|
<option value="@item.Key">@item.Value</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private string selected = "zh";
|
||||||
|
|
||||||
|
private readonly Dictionary<string, string> options = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ "zh", "<22><><EFBFBD><EFBFBD>" },
|
||||||
|
{ "en", "English" }
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
var current = LocalizationProvider.CurrentCulture;
|
||||||
|
var mapping = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ "zh", "zh-Hans" },
|
||||||
|
{ "en", "en-US" }
|
||||||
|
};
|
||||||
|
var found = mapping.FirstOrDefault(kv => string.Equals(kv.Value, current, StringComparison.OrdinalIgnoreCase)).Key;
|
||||||
|
if (!string.IsNullOrEmpty(found)) selected = found;
|
||||||
|
|
||||||
|
LocalizationProvider.LanguageChanged += (s, c) =>
|
||||||
|
{
|
||||||
|
var found2 = mapping.FirstOrDefault(kv => string.Equals(kv.Value, c, StringComparison.OrdinalIgnoreCase)).Key;
|
||||||
|
if (!string.IsNullOrEmpty(found2)) selected = found2;
|
||||||
|
StateHasChanged();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnChange(ChangeEventArgs e)
|
||||||
|
{
|
||||||
|
if (e?.Value is string val)
|
||||||
|
{
|
||||||
|
selected = val;
|
||||||
|
await LocalizationProvider.SetCultureAsync(selected);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Use NavigationManager to inspect and modify the current client-side path
|
||||||
|
var relative = Navigation.ToBaseRelativePath(Navigation.Uri).Trim('/');
|
||||||
|
var segments = string.IsNullOrEmpty(relative) ? Array.Empty<string>() : relative.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var mapping = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ "zh", "zh-Hans" },
|
||||||
|
{ "en", "en-US" }
|
||||||
|
};
|
||||||
|
|
||||||
|
string newUrl;
|
||||||
|
if (segments.Length > 0 && mapping.ContainsKey(segments[0]))
|
||||||
|
{
|
||||||
|
// replace existing locale prefix
|
||||||
|
var remaining = segments.Length > 1 ? "/" + string.Join('/', segments.Skip(1)) : "/";
|
||||||
|
newUrl = "/" + selected + remaining;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// keep current path, but trigger remount by navigating to same URI
|
||||||
|
newUrl = Navigation.Uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
// trigger client-side navigation (no hard reload) so components remount and re-evaluate localizer
|
||||||
|
Navigation.NavigateTo(newUrl, forceLoad: false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// fallback: just request UI refresh
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,41 @@
|
|||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
@inject Atomx.Admin.Client.Services.ILocalizationProvider LocalizationProvider
|
||||||
|
|
||||||
<div style="min-height:100vh">
|
<div style="min-height:100vh">
|
||||||
@Body
|
@Body
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
if (LocalizationProvider != null)
|
||||||
|
{
|
||||||
|
LocalizationProvider.LanguageChanged += OnLanguageChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLanguageChanged(object? sender, string culture)
|
||||||
|
{
|
||||||
|
_ = InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender && LocalizationProvider != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await LocalizationProvider.InitializeAsync();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (LocalizationProvider != null)
|
||||||
|
{
|
||||||
|
LocalizationProvider.LanguageChanged -= OnLanguageChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
@inject ILogger<MainLayout> _logger
|
@inject ILogger<MainLayout> _logger
|
||||||
|
@inject Atomx.Admin.Client.Services.ILocalizationProvider LocalizationProvider
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ChildContent>
|
<ChildContent>
|
||||||
@@ -131,5 +133,46 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
if (LocalizationProvider != null)
|
||||||
|
{
|
||||||
|
LocalizationProvider.LanguageChanged += OnLanguageChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLanguageChanged(object? sender, string culture)
|
||||||
|
{
|
||||||
|
// ensure UI updates on the SyncContext; small delay to let cache update
|
||||||
|
_ = InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Force route and page components to remount and re-render translations
|
||||||
|
Navigation.NavigateTo(Navigation.Uri, forceLoad: false);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender && LocalizationProvider != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await LocalizationProvider.InitializeAsync();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (LocalizationProvider != null)
|
||||||
|
{
|
||||||
|
LocalizationProvider.LanguageChanged -= OnLanguageChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,87 @@
|
|||||||
@page "/counter"
|
@page "/counter"
|
||||||
|
@page "/{locale}/counter"
|
||||||
|
|
||||||
<PageTitle>Counter</PageTitle>
|
@using Microsoft.Extensions.Localization
|
||||||
|
@inject IStringLocalizer<Counter> L
|
||||||
|
@inject Atomx.Admin.Client.Services.ILocalizationProvider LocalizationProvider
|
||||||
|
|
||||||
<h1>Counter</h1>
|
<PageTitle>@L["site.name"]</PageTitle>
|
||||||
|
|
||||||
<p role="status">Current count: @currentCount</p>
|
<h1>@L["site.name"]</h1>
|
||||||
|
|
||||||
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
|
<p role="status">@(L["current.count"] ?? "Current count"): @currentCount</p>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" @onclick="IncrementCount">@(L["click.me"] ?? "Click me")</button>
|
||||||
|
|
||||||
|
<Atomx.Admin.Client.Components.LangSelector />
|
||||||
|
|
||||||
|
<div style="margin-top:16px;">
|
||||||
|
<strong>Quick links:</strong>
|
||||||
|
<span style="padding-left:10px;"><NavLink href="/account/login">Login</NavLink></span>
|
||||||
|
<span style="padding-left:10px;"><NavLink href="/weather">Weather</NavLink></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top:16px;">
|
||||||
|
<strong>zh Quick links:</strong>
|
||||||
|
<span style="padding-left:10px;"><NavLink href="/zh/account/login">Login</NavLink></span>
|
||||||
|
<span style="padding-left:10px;"><NavLink href="/zh/weather">Weather</NavLink></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top:16px;">
|
||||||
|
<strong>en Quick links:</strong>
|
||||||
|
<span style="padding-left:10px;"><NavLink href="/en/account/login">Login</NavLink></span>
|
||||||
|
<span style="padding-left:10px;"><NavLink href="/en/weather">Weather</NavLink></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string Locale { get; set; } = string.Empty;
|
||||||
|
|
||||||
private int currentCount = 0;
|
private int currentCount = 0;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
if (LocalizationProvider != null)
|
||||||
|
{
|
||||||
|
LocalizationProvider.LanguageChanged += OnLanguageChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If running in browser, ensure current culture loaded (WASM loads asynchronously)
|
||||||
|
if (OperatingSystem.IsBrowser() && LocalizationProvider != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await LocalizationProvider.LoadCultureAsync(LocalizationProvider.CurrentCulture);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLanguageChanged(object? sender, string culture)
|
||||||
|
{
|
||||||
|
_ = InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (LocalizationProvider != null)
|
||||||
|
{
|
||||||
|
LocalizationProvider.LanguageChanged -= OnLanguageChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void IncrementCount()
|
private void IncrementCount()
|
||||||
{
|
{
|
||||||
currentCount++;
|
currentCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
123
Atomx.Admin/Atomx.Admin.Client/Pages/DebugLocalization.razor
Normal file
123
Atomx.Admin/Atomx.Admin.Client/Pages/DebugLocalization.razor
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
@page "/debug/localization"
|
||||||
|
@inject Atomx.Admin.Client.Services.ILocalizationProvider LocalizationProvider
|
||||||
|
@inject IHttpClientFactory HttpClientFactory
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
@inject ILogger<DebugLocalization> Logger
|
||||||
|
@inject HttpClient Http
|
||||||
|
@inject NavigationManager Nav
|
||||||
|
|
||||||
|
<h3>Localization Debug</h3>
|
||||||
|
|
||||||
|
<p>CurrentCulture: <b>@LocalizationProvider.CurrentCulture</b></p>
|
||||||
|
|
||||||
|
<button @onclick="SetProviderZh">Set culture zh</button>
|
||||||
|
<button @onclick="SetProviderEn">Set culture en</button>
|
||||||
|
<button @onclick="FetchFiles">Fetch files via HttpClient</button>
|
||||||
|
<button @onclick="ReadCookie">Read cookie</button>
|
||||||
|
|
||||||
|
<h4>Provider Results</h4>
|
||||||
|
<ul>
|
||||||
|
@foreach (var kv in providerResults)
|
||||||
|
{
|
||||||
|
<li>@kv</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>HTTP Fetch Results</h4>
|
||||||
|
<ul>
|
||||||
|
@foreach (var kv in httpResults)
|
||||||
|
{
|
||||||
|
<li>@kv</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>JS Read</h4>
|
||||||
|
<p>@jsResult</p>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private List<string> providerResults = new();
|
||||||
|
private List<string> httpResults = new();
|
||||||
|
private string jsResult = string.Empty;
|
||||||
|
|
||||||
|
private async Task SetProviderZh()
|
||||||
|
{
|
||||||
|
providerResults.Clear();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Use short code; provider will map to full culture and set active culture
|
||||||
|
await LocalizationProvider.SetCultureAsync("zh");
|
||||||
|
providerResults.Add($"Set culture to {LocalizationProvider.CurrentCulture}");
|
||||||
|
providerResults.Add($"site.name={LocalizationProvider.GetString("site.name")}");
|
||||||
|
providerResults.Add($"login.title={LocalizationProvider.GetString("login.title")}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
providerResults.Add("Set provider zh failed: " + ex.Message);
|
||||||
|
Logger.LogError(ex, "Set provider zh failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SetProviderEn()
|
||||||
|
{
|
||||||
|
providerResults.Clear();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await LocalizationProvider.SetCultureAsync("en");
|
||||||
|
providerResults.Add($"Set culture to {LocalizationProvider.CurrentCulture}");
|
||||||
|
providerResults.Add($"site.name={LocalizationProvider.GetString("site.name")}");
|
||||||
|
providerResults.Add($"login.title={LocalizationProvider.GetString("login.title")}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
providerResults.Add("Set provider en failed: " + ex.Message);
|
||||||
|
Logger.LogError(ex, "Set provider en failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task FetchFiles()
|
||||||
|
{
|
||||||
|
httpResults.Clear();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Prefer injected HttpClient that has BaseAddress set in Program.cs for WASM
|
||||||
|
var client = Http ?? HttpClientFactory.CreateClient();
|
||||||
|
|
||||||
|
var urlZ = new Uri(new Uri(Nav.BaseUri), $"localization/zh-Hans.json");
|
||||||
|
httpResults.Add($"GET {urlZ}");
|
||||||
|
var resZ = await client.GetAsync(urlZ);
|
||||||
|
httpResults.Add($"=> {resZ.StatusCode}");
|
||||||
|
if (resZ.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var txt = await resZ.Content.ReadAsStringAsync();
|
||||||
|
httpResults.Add("zh content len=" + txt.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
var urlE = new Uri(new Uri(Nav.BaseUri), $"localization/en-US.json");
|
||||||
|
httpResults.Add($"GET {urlE}");
|
||||||
|
var resE = await client.GetAsync(urlE);
|
||||||
|
httpResults.Add($"=> {resE.StatusCode}");
|
||||||
|
if (resE.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var txt = await resE.Content.ReadAsStringAsync();
|
||||||
|
httpResults.Add("en content len=" + txt.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
httpResults.Add("Fetch error: " + ex.Message);
|
||||||
|
Logger.LogError(ex, "FetchFiles error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReadCookie()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
jsResult = await JS.InvokeAsync<string>("CookieReader.Read", "atomx.culture");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
jsResult = "JS error: " + ex.Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,11 @@
|
|||||||
@layout EmptyLayout
|
@layout EmptyLayout
|
||||||
@inject ILogger<Login> Logger
|
@inject ILogger<Login> Logger
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
|
@using Microsoft.Extensions.Localization
|
||||||
|
@inject IStringLocalizer<Login> L
|
||||||
|
@inject Atomx.Admin.Client.Services.ILocalizationProvider LocalizationProvider
|
||||||
|
|
||||||
<PageTitle>登录</PageTitle>
|
<PageTitle>@L["login.title"]</PageTitle>
|
||||||
|
|
||||||
@if (!dataLoaded)
|
@if (!dataLoaded)
|
||||||
{
|
{
|
||||||
@@ -20,29 +23,29 @@ else
|
|||||||
<Form @ref="form" Model="@login" OnFinish="LoginAsync">
|
<Form @ref="form" Model="@login" OnFinish="LoginAsync">
|
||||||
<FluentValidationValidator />
|
<FluentValidationValidator />
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<AntDesign.Input Placeholder="登录账号" Size="InputSize.Large" @bind-Value="@login.Account">
|
<AntDesign.Input Placeholder="@L["login.account.placeholder"]" Size="InputSize.Large" @bind-Value="@login.Account">
|
||||||
<Prefix><Icon Type="user" /></Prefix>
|
<Prefix><Icon Type="user" /></Prefix>
|
||||||
</AntDesign.Input>
|
</AntDesign.Input>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<AntDesign.Input Placeholder="登录密码" Size="InputSize.Large" @bind-Value="@login.Password" Type="InputType.Password">
|
<AntDesign.Input Placeholder="@L["login.password.placeholder"]" Size="InputSize.Large" @bind-Value="@login.Password" Type="InputType.Password">
|
||||||
<Prefix><Icon Type="lock" /></Prefix>
|
<Prefix><Icon Type="lock" /></Prefix>
|
||||||
</AntDesign.Input>
|
</AntDesign.Input>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<a style="float: left;">
|
<a style="float: left;">
|
||||||
忘记密码
|
@L["login.forgot"]
|
||||||
</a>
|
</a>
|
||||||
<a style="float: right;">
|
<a style="float: right;">
|
||||||
<NavLink href="/register">马上注册</NavLink>
|
<NavLink href="/register">@L["login.register"]</NavLink>
|
||||||
</a>
|
</a>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<Button Type="ButtonType.Primary" HtmlType="submit" Class="submit" Size="ButtonSize.Large" Block>登录</Button>
|
<Button Type="ButtonType.Primary" HtmlType="submit" Class="submit" Size="ButtonSize.Large" Block>@L["login.submit"]</Button>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<a @onclick="setAccount">
|
<a @onclick="setAccount">
|
||||||
设置开发帐号
|
@L["login.setdev"]
|
||||||
</a>
|
</a>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -50,9 +53,31 @@ else
|
|||||||
</GridCol>
|
</GridCol>
|
||||||
</GridRow>
|
</GridRow>
|
||||||
<GridRow Style="padding-top:40px">
|
<GridRow Style="padding-top:40px">
|
||||||
<span style="font-size:12px;padding-right:3px;">Copyright © 2025-@DateTime.Now.Year Atomlust.com All rights reserved.</span>
|
<span style="font-size:12px;padding-right:3px;">@L["copyright"]</span>
|
||||||
<span style="font-size:12px">runing as @handler</span>
|
<span style="font-size:12px">runing as @handler</span>
|
||||||
</GridRow>
|
</GridRow>
|
||||||
|
|
||||||
|
<GridRow>
|
||||||
|
<Atomx.Admin.Client.Components.LangSelector />
|
||||||
|
</GridRow>
|
||||||
|
|
||||||
|
<GridRow Style="padding-top:10px">
|
||||||
|
<div>
|
||||||
|
<strong>Quick links:</strong>
|
||||||
|
<span style="padding-left:10px;"><NavLink href="/counter">Counter</NavLink></span>
|
||||||
|
<span style="padding-left:10px;"><NavLink href="/weather">Weather</NavLink></span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>zh Quick links:</strong>
|
||||||
|
<span style="padding-left:10px;"><NavLink href="/zh/counter">Counter</NavLink></span>
|
||||||
|
<span style="padding-left:10px;"><NavLink href="/zh/weather">Weather</NavLink></span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>en Quick links:</strong>
|
||||||
|
<span style="padding-left:10px;"><NavLink href="/en/counter">Counter</NavLink></span>
|
||||||
|
<span style="padding-left:10px;"><NavLink href="/en/weather">Weather</NavLink></span>
|
||||||
|
</div>
|
||||||
|
</GridRow>
|
||||||
</Flex>
|
</Flex>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +111,11 @@ else
|
|||||||
{
|
{
|
||||||
handler = "Server";
|
handler = "Server";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (LocalizationProvider != null)
|
||||||
|
{
|
||||||
|
LocalizationProvider.LanguageChanged += OnLanguageChanged;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
@@ -103,6 +133,25 @@ else
|
|||||||
dataLoaded = true;
|
dataLoaded = true;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure culture loaded on client so translations are available
|
||||||
|
if (firstRender && OperatingSystem.IsBrowser() && LocalizationProvider != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await LocalizationProvider.LoadCultureAsync(LocalizationProvider.CurrentCulture);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnLanguageChanged(object? sender, string culture)
|
||||||
|
{
|
||||||
|
// ensure UI updates on Blazor sync context
|
||||||
|
await InvokeAsync(() => {
|
||||||
|
dataLoaded = true; // ensure UI is rendered
|
||||||
|
StateHasChanged();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoginAsync()
|
private async Task LoginAsync()
|
||||||
@@ -200,6 +249,15 @@ 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 *@
|
@* 页面内 JS 辅助:用于在 Server 模式下从浏览器发起 POST 并携带凭证,使浏览器接收 Set-Cookie *@
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
@page "/weather"
|
@page "/weather"
|
||||||
|
@page "/{locale}/weather"
|
||||||
|
|
||||||
<PageTitle>Weather</PageTitle>
|
@using Microsoft.Extensions.Localization
|
||||||
|
@inject IStringLocalizer<Weather> L
|
||||||
|
@inject Atomx.Admin.Client.Services.ILocalizationProvider LocalizationProvider
|
||||||
|
|
||||||
<h1>Weather</h1>
|
<PageTitle>@L["weather.title"]</PageTitle>
|
||||||
|
|
||||||
<p>This component demonstrates showing data.</p>
|
<h1>@L["weather.title"]</h1>
|
||||||
|
|
||||||
|
<p>@L["weather.summary"]</p>
|
||||||
|
|
||||||
|
<div style="margin-top:12px; margin-bottom:12px;">
|
||||||
|
<strong>Quick links:</strong>
|
||||||
|
<span style="padding-left:10px;"><NavLink href="/account/login">Login</NavLink></span>
|
||||||
|
<span style="padding-left:10px;"><NavLink href="/counter">Counter</NavLink></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
@if (forecasts == null)
|
@if (forecasts == null)
|
||||||
{
|
{
|
||||||
@@ -16,7 +27,7 @@ else
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
<th aria-label="Temperature in Celsius">Temp. (C)</th>
|
<th aria-label="Temperature in Celsius">@L["weather.temperature"]</th>
|
||||||
<th aria-label="Temperature in Farenheit">Temp. (F)</th>
|
<th aria-label="Temperature in Farenheit">Temp. (F)</th>
|
||||||
<th>Summary</th>
|
<th>Summary</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -36,10 +47,22 @@ else
|
|||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public string Locale { get; set; } = string.Empty;
|
||||||
private WeatherForecast[]? forecasts;
|
private WeatherForecast[]? forecasts;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
if (LocalizationProvider != null)
|
||||||
|
{
|
||||||
|
LocalizationProvider.LanguageChanged += OnLanguageChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OperatingSystem.IsBrowser() && LocalizationProvider != null)
|
||||||
|
{
|
||||||
|
try { await LocalizationProvider.LoadCultureAsync(LocalizationProvider.CurrentCulture); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
// Simulate asynchronous loading to demonstrate a loading indicator
|
// Simulate asynchronous loading to demonstrate a loading indicator
|
||||||
await Task.Delay(500);
|
await Task.Delay(500);
|
||||||
|
|
||||||
@@ -53,6 +76,10 @@ else
|
|||||||
}).ToArray();
|
}).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnLanguageChanged(object? s, string c) => _ = InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
public void Dispose() { if (LocalizationProvider != null) LocalizationProvider.LanguageChanged -= OnLanguageChanged; }
|
||||||
|
|
||||||
private class WeatherForecast
|
private class WeatherForecast
|
||||||
{
|
{
|
||||||
public DateOnly Date { get; set; }
|
public DateOnly Date { get; set; }
|
||||||
@@ -60,4 +87,13 @@ else
|
|||||||
public string? Summary { get; set; }
|
public string? Summary { get; set; }
|
||||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,14 @@ builder.Services.AddScoped<IconsExtension>();
|
|||||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||||
|
|
||||||
|
|
||||||
// ע<><D7A2>IStringLocalizer
|
// ע<><D7A2> LocalizationProvider (<28><><EFBFBD><EFBFBD> WASM)
|
||||||
builder.Services.AddTransient(typeof(IStringLocalizer<>), typeof(StringLocalizer<>));
|
// Use Scoped lifetime because LocalizationProvider depends on IJSRuntime (scoped) and IHttpClient etc.
|
||||||
|
builder.Services.AddScoped<ILocalizationProvider, LocalizationProvider>();
|
||||||
|
// ע<><D7A2> ILocalizationService <20><><EFBFBD><EFBFBD>ͬ<EFBFBD><CDAC> Culture <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>䴫<EFBFBD><E4B4AB>
|
||||||
|
builder.Services.AddScoped<ILocalizationService, LocalizationService>();
|
||||||
|
|
||||||
|
// ע<><D7A2> IStringLocalizer<T> ʵ<><CAB5>
|
||||||
|
builder.Services.AddTransient(typeof(IStringLocalizer<>), typeof(JsonStringLocalizer<>));
|
||||||
// <20><><EFBFBD>ӱ<EFBFBD><D3B1>ػ<EFBFBD><D8BB><EFBFBD><EFBFBD><EFBFBD>
|
// <20><><EFBFBD>ӱ<EFBFBD><D3B1>ػ<EFBFBD><D8BB><EFBFBD><EFBFBD><EFBFBD>
|
||||||
builder.Services.AddLocalization();
|
builder.Services.AddLocalization();
|
||||||
|
|
||||||
@@ -64,4 +70,6 @@ builder.Services.AddScoped<HttpService>(sp =>
|
|||||||
builder.Services.AddAntDesign();
|
builder.Services.AddAntDesign();
|
||||||
|
|
||||||
|
|
||||||
await builder.Build().RunAsync();
|
var host = builder.Build();
|
||||||
|
|
||||||
|
await host.RunAsync();
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
<Router AppAssembly="typeof(Program).Assembly">
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
@inject Atomx.Admin.Client.Services.ILocalizationProvider LocalizationProvider
|
||||||
|
|
||||||
|
<Router AppAssembly="typeof(Program).Assembly">
|
||||||
<Found Context="routeData">
|
<Found Context="routeData">
|
||||||
<CascadingValue Value="routeData">
|
<CascadingValue Value="routeData">
|
||||||
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
|
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
|
||||||
@@ -11,3 +14,23 @@
|
|||||||
</Found>
|
</Found>
|
||||||
</Router>
|
</Router>
|
||||||
<AntContainer />
|
<AntContainer />
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private bool _initialized;
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender && !_initialized)
|
||||||
|
{
|
||||||
|
_initialized = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (LocalizationProvider != null)
|
||||||
|
{
|
||||||
|
await LocalizationProvider.InitializeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
517
Atomx.Admin/Atomx.Admin.Client/Services/LocalizationProvider.cs
Normal file
517
Atomx.Admin/Atomx.Admin.Client/Services/LocalizationProvider.cs
Normal file
@@ -0,0 +1,517 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
|
|
||||||
|
namespace Atomx.Admin.Client.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <20>ṩ<EFBFBD><E1B9A9><EFBFBD><EFBFBD><EFBFBD><EFBFBD> JSON <20>ļ<EFBFBD><C4BC><EFBFBD><EFBFBD>ء<EFBFBD><D8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD>л<EFBFBD><D0BB><EFBFBD>ʵ<EFBFBD>֡<EFBFBD>
|
||||||
|
/// - <20><> Server <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> IWebHostEnvironment <20><> webroot<6F><74><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>ϵͳ<CFB5><CDB3>ȡ {culture}.json <20>ļ<EFBFBD><C4BC><EFBFBD>
|
||||||
|
/// - <20><> WASM <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʹ<EFBFBD><CAB9>ע<EFBFBD><D7A2><EFBFBD><EFBFBD> HttpClient <20><> /localization/{culture}.json <20><><EFBFBD>ز<EFBFBD><D8B2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
/// ͬʱ<CDAC><CAB1><EFBFBD>л<EFBFBD><D0BB><EFBFBD><EFBFBD><EFBFBD>ʱд<CAB1><D0B4> Cookie <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ҳ<EFBFBD><D2B3> HTML lang <20><><EFBFBD>ԡ<EFBFBD>
|
||||||
|
/// </summary>
|
||||||
|
public interface ILocalizationProvider
|
||||||
|
{
|
||||||
|
string CurrentCulture { get; }
|
||||||
|
string? GetString(string key);
|
||||||
|
Task SetCultureAsync(string cultureShortOrFull);
|
||||||
|
Task InitializeAsync();
|
||||||
|
/// <summary>
|
||||||
|
/// <20><>ָ֤<D6A4><D6B8><EFBFBD>Ļ<EFBFBD><C4BB><EFBFBD><EFBFBD><EFBFBD>Դ<EFBFBD>ѱ<EFBFBD><D1B1><EFBFBD><EFBFBD>أ<EFBFBD><D8A3>첽<EFBFBD><ECB2BD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ⲿ<EFBFBD><E2B2BF><EFBFBD><EFBFBD>Ҫʱ<D2AA><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ء<EFBFBD>
|
||||||
|
/// </summary>
|
||||||
|
Task LoadCultureAsync(string culture);
|
||||||
|
event EventHandler<string>? LanguageChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LocalizationProvider : ILocalizationProvider
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _sp;
|
||||||
|
private readonly IHttpClientFactory? _httpClientFactory;
|
||||||
|
private readonly IJSRuntime? _jsRuntime;
|
||||||
|
private readonly ILogger<LocalizationProvider> _logger;
|
||||||
|
private readonly ILocalizationService _localizationService;
|
||||||
|
|
||||||
|
// <20><><EFBFBD>棺culture -> translations
|
||||||
|
private readonly Dictionary<string, Dictionary<string, string>> _cache = new();
|
||||||
|
|
||||||
|
// <20><><EFBFBD>뵽<EFBFBD><EBB5BD><EFBFBD><EFBFBD> culture <20><>ӳ<EFBFBD><D3B3>
|
||||||
|
private static readonly Dictionary<string, string> ShortToCulture = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ "zh", "zh-Hans" },
|
||||||
|
{ "en", "en-US" }
|
||||||
|
};
|
||||||
|
|
||||||
|
private string _currentCulture = "zh-Hans";
|
||||||
|
|
||||||
|
private const string CookieName = "atomx.culture";
|
||||||
|
|
||||||
|
public LocalizationProvider(IServiceProvider sp, IHttpClientFactory? httpClientFactory, IJSRuntime? jsRuntime, ILogger<LocalizationProvider> logger, ILocalizationService localizationService)
|
||||||
|
{
|
||||||
|
_sp = sp;
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
|
_jsRuntime = jsRuntime;
|
||||||
|
_logger = logger;
|
||||||
|
_localizationService = localizationService;
|
||||||
|
|
||||||
|
// <20><><EFBFBD>ڹ<EFBFBD><DAB9>캯<EFBFBD><ECBAAF><EFBFBD>н<EFBFBD><D0BD><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><CEAA><EFBFBD><EFBFBD> culture
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var threadUi = CultureInfo.DefaultThreadCurrentUICulture ?? CultureInfo.CurrentUICulture;
|
||||||
|
if (!string.IsNullOrEmpty(threadUi?.Name))
|
||||||
|
{
|
||||||
|
_currentCulture = MapToFullCulture(threadUi!.Name);
|
||||||
|
_logger?.LogDebug("LocalizationProvider ctor detected thread UI culture: {Culture}", _currentCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogDebug(ex, "LocalizationProvider ctor failed to read thread culture");
|
||||||
|
}
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Server<65><72>IWebHostEnvironment <20><><EFBFBD>ã<EFBFBD><C3A3><EFBFBD><EFBFBD><EFBFBD> JSRuntime <20><><EFBFBD><EFBFBD><EFBFBD>ã<EFBFBD>˵<EFBFBD><CBB5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͬ<EFBFBD><CDAC><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD><C4BC><EFBFBD><EFBFBD>ر<EFBFBD><D8B1>ػ<EFBFBD><D8BB>ļ<EFBFBD>
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var envType = Type.GetType("Microsoft.AspNetCore.Hosting.IWebHostEnvironment, Microsoft.AspNetCore.Hosting.Abstractions")
|
||||||
|
?? Type.GetType("Microsoft.AspNetCore.Hosting.IWebHostEnvironment");
|
||||||
|
if (envType != null)
|
||||||
|
{
|
||||||
|
var env = _sp.GetService(envType);
|
||||||
|
if (env != null && _jsRuntime == null)
|
||||||
|
{
|
||||||
|
var webRootProp = envType.GetProperty("WebRootPath");
|
||||||
|
var contentRootProp = envType.GetProperty("ContentRootPath");
|
||||||
|
var webRoot = webRootProp?.GetValue(env) as string ?? Path.Combine(contentRootProp?.GetValue(env) as string ?? ".", "wwwroot");
|
||||||
|
var path = Path.Combine(webRoot ?? ".", "localization", _currentCulture + ".json");
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(path);
|
||||||
|
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new Dictionary<string, string>();
|
||||||
|
_cache[_currentCulture] = dict;
|
||||||
|
_logger?.LogInformation("Loaded localization file for {Culture} from path {Path}, entries: {Count}", _currentCulture, path, dict.Count);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogWarning(ex, "Failed to read localization file synchronously: {Path}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("Localization file not found at {Path}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogDebug(ex, "Synchronous file load attempt failed in ctor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("LocalizationProvider.InitializeAsync start. CurrentCulture={Culture}", _currentCulture);
|
||||||
|
|
||||||
|
string? urlFirstSegment = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_jsRuntime != null && OperatingSystem.IsBrowser())
|
||||||
|
{
|
||||||
|
var path = await _jsRuntime.InvokeAsync<string>("eval", "location.pathname");
|
||||||
|
_logger?.LogDebug("JS location.pathname='{Path}'", path);
|
||||||
|
if (!string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
var trimmed = path.Trim('/');
|
||||||
|
if (!string.IsNullOrEmpty(trimmed))
|
||||||
|
{
|
||||||
|
var seg = trimmed.Split('/', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
|
||||||
|
urlFirstSegment = seg;
|
||||||
|
_logger?.LogDebug("Detected url first segment: {Segment}", urlFirstSegment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogDebug(ex, "<22><>ȡ location.pathname ʧ<><CAA7>");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(urlFirstSegment) && ShortToCulture.TryGetValue(urlFirstSegment, out var mapped))
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("URL short segment '{Seg}' mapped to culture '{Culture}'", urlFirstSegment, mapped);
|
||||||
|
await SetCultureInternalAsync(mapped, persistCookie: false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_jsRuntime != null && OperatingSystem.IsBrowser())
|
||||||
|
{
|
||||||
|
var cookieVal = await _jsRuntime.InvokeAsync<string>("CookieReader.Read", CookieName);
|
||||||
|
_logger?.LogDebug("Cookie '{CookieName}'='{CookieVal}'", CookieName, cookieVal);
|
||||||
|
if (!string.IsNullOrEmpty(cookieVal))
|
||||||
|
{
|
||||||
|
if (ShortToCulture.TryGetValue(cookieVal, out var mappedFromCookie))
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("Cookie short '{Cookie}' mapped to {Culture}", cookieVal, mappedFromCookie);
|
||||||
|
await SetCultureInternalAsync(mappedFromCookie, persistCookie: false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// <20><><EFBFBD><EFBFBD> cookie <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> culture<72><65><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD><EFBFBD>ʹ<EFBFBD><CAB9>
|
||||||
|
await SetCultureInternalAsync(MapToFullCulture(cookieVal), persistCookie: false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogDebug(ex, "<22><>ȡ Cookie ʧ<><CAA7>");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_jsRuntime != null && OperatingSystem.IsBrowser())
|
||||||
|
{
|
||||||
|
var browserLang = await _jsRuntime.InvokeAsync<string>("getBrowserLanguage");
|
||||||
|
_logger?.LogDebug("Browser language: {BrowserLang}", browserLang);
|
||||||
|
if (!string.IsNullOrEmpty(browserLang))
|
||||||
|
{
|
||||||
|
var mappedFromBrowser = MapToFullCulture(browserLang);
|
||||||
|
_logger?.LogDebug("Browser mapped to {Culture}", mappedFromBrowser);
|
||||||
|
await SetCultureInternalAsync(mappedFromBrowser, persistCookie: false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogDebug(ex, "<22><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD><CAA7>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD>ȷ<EFBFBD><C8B7><EFBFBD><EFBFBD><EFBFBD>ص<EFBFBD>ǰ culture
|
||||||
|
_logger?.LogDebug("InitializeAsync falling back to current culture {Culture}", _currentCulture);
|
||||||
|
await EnsureCultureLoadedAsync(_currentCulture);
|
||||||
|
await SetCultureInternalAsync(_currentCulture, persistCookie: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetCultureAsync(string cultureShortOrFull)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(cultureShortOrFull)) return;
|
||||||
|
if (ShortToCulture.TryGetValue(cultureShortOrFull, out var mapped))
|
||||||
|
{
|
||||||
|
await SetCultureInternalAsync(mapped, persistCookie: true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await SetCultureInternalAsync(MapToFullCulture(cultureShortOrFull), persistCookie: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task LoadCultureAsync(string culture) => EnsureCultureLoadedAsync(MapToFullCulture(culture));
|
||||||
|
|
||||||
|
private async Task SetCultureInternalAsync(string cultureFull, bool persistCookie)
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("SetCultureInternalAsync start: {Culture}, persist={Persist}", cultureFull, persistCookie);
|
||||||
|
await EnsureCultureLoadedAsync(cultureFull);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ci = new CultureInfo(cultureFull);
|
||||||
|
CultureInfo.DefaultThreadCurrentCulture = ci;
|
||||||
|
CultureInfo.DefaultThreadCurrentUICulture = ci;
|
||||||
|
_currentCulture = cultureFull;
|
||||||
|
_localizationService.SetLanguage(ci);
|
||||||
|
_logger?.LogDebug("Culture set to {Culture}", cultureFull);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogWarning(ex, "<22><><EFBFBD><EFBFBD> Culture ʧ<><CAA7>: {Culture}", cultureFull);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (persistCookie && _jsRuntime != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var shortKey = ShortToCulture.FirstOrDefault(kv => string.Equals(kv.Value, cultureFull, StringComparison.OrdinalIgnoreCase)).Key ?? cultureFull;
|
||||||
|
await _jsRuntime.InvokeVoidAsync("cookies.Write", CookieName, shortKey, DateTime.UtcNow.AddYears(1).ToString("o"));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogDebug(ex, "д Cookie ʧ<><CAA7>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_jsRuntime != null)
|
||||||
|
{
|
||||||
|
await _jsRuntime.InvokeVoidAsync("setHtmlLang", cultureFull);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
LanguageChanged?.Invoke(this, 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)
|
||||||
|
cultureFull = MapToFullCulture(cultureFull);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(cultureFull)) return;
|
||||||
|
if (_cache.ContainsKey(cultureFull))
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("EnsureCultureLoadedAsync: culture {Culture} already cached", cultureFull);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer HttpClient when running in browser (WASM)
|
||||||
|
if (_jsRuntime != null && OperatingSystem.IsBrowser())
|
||||||
|
{
|
||||||
|
_logger?.LogInformation("EnsureCultureLoadedAsync: running in browser, will attempt HttpClient for {Culture}", cultureFull);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var http = _sp.GetService(typeof(HttpClient)) as HttpClient;
|
||||||
|
if (http == null && _httpClientFactory != null)
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("HttpClient not found from service provider, using factory");
|
||||||
|
http = _httpClientFactory.CreateClient();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("HttpClient resolved from service provider: {HasClient}", http != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (http != null)
|
||||||
|
{
|
||||||
|
var url = $"/localization/{cultureFull}.json";
|
||||||
|
Uri? requestUri = null;
|
||||||
|
|
||||||
|
// If HttpClient has a BaseAddress, use it. Otherwise, if running in browser, build absolute URI from location.origin
|
||||||
|
if (http.BaseAddress != null)
|
||||||
|
{
|
||||||
|
requestUri = new Uri(http.BaseAddress, url);
|
||||||
|
}
|
||||||
|
else if (_jsRuntime != null && OperatingSystem.IsBrowser())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var origin = await _jsRuntime.InvokeAsync<string>("eval", "location.origin");
|
||||||
|
if (!string.IsNullOrEmpty(origin))
|
||||||
|
{
|
||||||
|
// ensure no double slashes
|
||||||
|
requestUri = new Uri(new Uri(origin), url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception jsEx)
|
||||||
|
{
|
||||||
|
_logger?.LogDebug(jsEx, "Failed to get location.origin from JS");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestUri != null)
|
||||||
|
{
|
||||||
|
_logger?.LogInformation("Downloading localization from {Url}", requestUri);
|
||||||
|
var txt = await http.GetStringAsync(requestUri);
|
||||||
|
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(txt) ?? new Dictionary<string, string>();
|
||||||
|
_cache[cultureFull] = dict;
|
||||||
|
_logger?.LogInformation("Loaded localization via HttpClient for {Culture}, entries: {Count}", cultureFull, dict.Count);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger?.LogWarning("HttpClient has no BaseAddress and JSRuntime unavailable to construct absolute URL; skipping HttpClient load for {Culture}", cultureFull);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger?.LogWarning("No HttpClient available to load localization for {Culture}", cultureFull);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogDebug(ex, "ͨ<><CDA8> HttpClient <20><><EFBFBD>ر<EFBFBD><D8B1>ػ<EFBFBD><D8BB>ļ<EFBFBD>ʧ<EFBFBD><CAA7>: {Culture}", cultureFull);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger?.LogDebug("EnsureCultureLoadedAsync trying filesystem for {Culture}", cultureFull);
|
||||||
|
// <20><><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȡ IWebHostEnvironment<6E><74>Server ʱ<><CAB1><EFBFBD>ã<EFBFBD>
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var envType = Type.GetType("Microsoft.AspNetCore.Hosting.IWebHostEnvironment, Microsoft.AspNetCore.Hosting.Abstractions")
|
||||||
|
?? Type.GetType("Microsoft.AspNetCore.Hosting.IWebHostEnvironment");
|
||||||
|
if (envType != null)
|
||||||
|
{
|
||||||
|
var env = _sp.GetService(envType);
|
||||||
|
if (env != null)
|
||||||
|
{
|
||||||
|
var webRootProp = envType.GetProperty("WebRootPath");
|
||||||
|
var contentRootProp = envType.GetProperty("ContentRootPath");
|
||||||
|
var webRoot = webRootProp?.GetValue(env) as string ?? Path.Combine(contentRootProp?.GetValue(env) as string ?? ".", "wwwroot");
|
||||||
|
var path = Path.Combine(webRoot ?? ".", "localization", cultureFull + ".json");
|
||||||
|
_logger?.LogDebug("Looking for localization file at {Path}", path);
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
var json = await File.ReadAllTextAsync(path);
|
||||||
|
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new Dictionary<string, string>();
|
||||||
|
_cache[cultureFull] = dict;
|
||||||
|
_logger?.LogInformation("Loaded localization from file for {Culture}, entries: {Count}", cultureFull, dict.Count);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("Localization file not found at {Path}", path);
|
||||||
|
// Fallback: check build output wwwroot under AppContext.BaseDirectory
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var alt = Path.Combine(AppContext.BaseDirectory ?? ".", "wwwroot", "localization", cultureFull + ".json");
|
||||||
|
_logger?.LogDebug("Looking for localization file at alternative path {AltPath}", alt);
|
||||||
|
if (File.Exists(alt))
|
||||||
|
{
|
||||||
|
var json2 = await File.ReadAllTextAsync(alt);
|
||||||
|
var dict2 = JsonSerializer.Deserialize<Dictionary<string, string>>(json2) ?? new Dictionary<string, string>();
|
||||||
|
_cache[cultureFull] = dict2;
|
||||||
|
_logger?.LogInformation("Loaded localization from alternative file path for {Culture}, entries: {Count}", cultureFull, dict2.Count);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("Localization file not found at alternative path {AltPath}", alt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception exAlt)
|
||||||
|
{
|
||||||
|
_logger?.LogDebug(exAlt, "Error while checking alternative localization path");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("IWebHostEnvironment not resolved from service provider");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger?.LogDebug("IWebHostEnvironment type not found via reflection");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.LogDebug(ex, "<22><><EFBFBD>ļ<EFBFBD>ϵͳ<CFB5><CDB3><EFBFBD>ر<EFBFBD><D8B1>ػ<EFBFBD><D8BB>ļ<EFBFBD>ʧ<EFBFBD><CAA7>: {Culture}", cultureFull);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger?.LogDebug("EnsureCultureLoadedAsync fallback to empty dict for {Culture}", cultureFull);
|
||||||
|
_cache[cultureFull] = new Dictionary<string, string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string MapToFullCulture(string culture)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(culture)) return culture;
|
||||||
|
// direct mapping
|
||||||
|
if (ShortToCulture.TryGetValue(culture, out var mapped)) return mapped;
|
||||||
|
// consider prefix, e.g. zh-CN -> zh
|
||||||
|
var prefix = culture.Split('-', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
|
||||||
|
if (!string.IsNullOrEmpty(prefix) && ShortToCulture.TryGetValue(prefix, out var mapped2)) return mapped2;
|
||||||
|
return culture;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <20><><EFBFBD><EFBFBD> ILocalizationProvider <20><> IStringLocalizer ʵ<>֣<EFBFBD>
|
||||||
|
/// ʹ<><CAB9> JSON <20>ļ<EFBFBD><C4BC>еļ<D0B5>ֵ<EFBFBD><D6B5>δ<EFBFBD>ҵ<EFBFBD><D2B5><EFBFBD><EFBFBD><EFBFBD> key <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
/// <20><><EFBFBD>Ƹ<EFBFBD>Ϊ JsonStringLocalizer <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ܵ<EFBFBD> StringLocalizer <20><>ͻ<EFBFBD><CDBB>
|
||||||
|
/// </summary>
|
||||||
|
public class JsonStringLocalizer<T> : IStringLocalizer<T>
|
||||||
|
{
|
||||||
|
private readonly ILocalizationProvider _provider;
|
||||||
|
|
||||||
|
public JsonStringLocalizer(ILocalizationProvider provider)
|
||||||
|
{
|
||||||
|
_provider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalizedString this[string name]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var value = _provider.GetString(name);
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
// Avoid synchronous blocking during server prerender. Start background load and return key.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ = _provider.LoadCultureAsync(_provider.CurrentCulture);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = value ?? name;
|
||||||
|
return new LocalizedString(name, result, resourceNotFound: result == name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalizedString this[string name, params object[] arguments]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var fmt = _provider.GetString(name);
|
||||||
|
if (fmt == null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ = _provider.LoadCultureAsync(_provider.CurrentCulture);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
var format = fmt ?? name;
|
||||||
|
var value = string.Format(format, arguments);
|
||||||
|
return new LocalizedString(name, value, resourceNotFound: format == name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
|
||||||
|
{
|
||||||
|
var list = new List<LocalizedString>();
|
||||||
|
var providerType = _provider.GetType();
|
||||||
|
var currentProp = providerType.GetProperty("CurrentCulture");
|
||||||
|
var culture = currentProp?.GetValue(_provider) as string ?? string.Empty;
|
||||||
|
var cacheField = providerType.GetField("_cache", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||||
|
if (!string.IsNullOrEmpty(culture) && cacheField?.GetValue(_provider) is Dictionary<string, Dictionary<string, string>> cache && cache.TryGetValue(culture, out var dict))
|
||||||
|
{
|
||||||
|
foreach (var kv in dict)
|
||||||
|
{
|
||||||
|
list.Add(new LocalizedString(kv.Key, kv.Value, resourceNotFound: false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IStringLocalizer WithCulture(CultureInfo culture)
|
||||||
|
{
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Atomx.Admin/Atomx.Admin.Client/wwwroot/js/common.js
Normal file
41
Atomx.Admin/Atomx.Admin.Client/wwwroot/js/common.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
window.CookieReader = {
|
||||||
|
Read: function (name) {
|
||||||
|
try {
|
||||||
|
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
|
||||||
|
if (match) return decodeURIComponent(match[2]);
|
||||||
|
return '';
|
||||||
|
} catch (e) {
|
||||||
|
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) { }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.getBrowserLanguage = function () {
|
||||||
|
try {
|
||||||
|
return (navigator.languages && navigator.languages[0]) || navigator.language || navigator.userLanguage || '';
|
||||||
|
} catch (e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.setHtmlLang = function (lang) {
|
||||||
|
try {
|
||||||
|
if (document && document.documentElement) document.documentElement.lang = lang || '';
|
||||||
|
} catch (e) { }
|
||||||
|
};
|
||||||
|
|
||||||
|
// simple cookies wrapper used earlier as cookies.Write
|
||||||
|
window.cookies = {
|
||||||
|
Write: function (name, value, expiresIso) {
|
||||||
|
return window.CookieReader.Write(name, value, expiresIso);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -43,4 +43,8 @@
|
|||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="wwwroot\js\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@using Atomx.Admin.Client.Services
|
@using Atomx.Admin.Client.Services
|
||||||
|
@inject Atomx.Admin.Client.Services.ILocalizationProvider LocalizationProvider
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@@ -7,7 +8,6 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
@* <link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]" /> *@
|
|
||||||
<link rel="stylesheet" href="@Assets["_content/AntDesign/css/ant-design-blazor.variable.css"] " />
|
<link rel="stylesheet" href="@Assets["_content/AntDesign/css/ant-design-blazor.variable.css"] " />
|
||||||
<link rel="stylesheet" href="@Assets["app.css"]" />
|
<link rel="stylesheet" href="@Assets["app.css"]" />
|
||||||
<link rel="stylesheet" href="@Assets["Atomx.Admin.styles.css"]" />
|
<link rel="stylesheet" href="@Assets["Atomx.Admin.styles.css"]" />
|
||||||
@@ -17,9 +17,35 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<div @key="LocalizationProvider.CurrentCulture">
|
||||||
<Routes @rendermode="InteractiveAuto" />
|
<Routes @rendermode="InteractiveAuto" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/js/common.js"></script>
|
||||||
<script src="_framework/blazor.web.js"></script>
|
<script src="_framework/blazor.web.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
if (LocalizationProvider != null)
|
||||||
|
{
|
||||||
|
LocalizationProvider.LanguageChanged += OnLanguageChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLanguageChanged(object? sender, string culture)
|
||||||
|
{
|
||||||
|
_ = InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (LocalizationProvider != null)
|
||||||
|
{
|
||||||
|
LocalizationProvider.LanguageChanged -= OnLanguageChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -60,9 +60,11 @@ builder.Services.AddHttpContextAccessor();
|
|||||||
// HttpClient <20><><EFBFBD><EFBFBD><EFBFBD>ݷ<EFBFBD><DDB7><EFBFBD>
|
// HttpClient <20><><EFBFBD><EFBFBD><EFBFBD>ݷ<EFBFBD><DDB7><EFBFBD>
|
||||||
builder.Services.AddHttpClientApiService(builder.Configuration["WebApi:ServerUrl"] ?? "http://localhost");
|
builder.Services.AddHttpClientApiService(builder.Configuration["WebApi:ServerUrl"] ?? "http://localhost");
|
||||||
|
|
||||||
|
// ע<><D7A2> LocalizationProvider <20><> Server ģʽ<C4A3><CABD>ʹ<EFBFBD><CAB9>
|
||||||
// ע<EFBFBD><EFBFBD>IStringLocalizer
|
// Use Scoped to avoid consuming scoped services from singleton
|
||||||
builder.Services.AddTransient(typeof(IStringLocalizer<>), typeof(StringLocalizer<>));
|
builder.Services.AddScoped<ILocalizationProvider, LocalizationProvider>();
|
||||||
|
builder.Services.AddScoped<ILocalizationService, LocalizationService>();
|
||||||
|
builder.Services.AddTransient(typeof(IStringLocalizer<>), typeof(JsonStringLocalizer<>));
|
||||||
|
|
||||||
// <20><><EFBFBD>ӱ<EFBFBD><D3B1>ػ<EFBFBD><D8BB><EFBFBD><EFBFBD><EFBFBD>
|
// <20><><EFBFBD>ӱ<EFBFBD><D3B1>ػ<EFBFBD><D8BB><EFBFBD><EFBFBD><EFBFBD>
|
||||||
builder.Services.AddLocalization();
|
builder.Services.AddLocalization();
|
||||||
@@ -149,6 +151,38 @@ builder.Services.Configure<MonitoringOptions>(builder.Configuration.GetSection("
|
|||||||
|
|
||||||
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>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Debug",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Information"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
{
|
{
|
||||||
"site.name": "Atomx Studio"
|
"site.name": "Atomx Studio",
|
||||||
|
"current.count": "Current count",
|
||||||
|
"click.me": "Click me",
|
||||||
|
"login.title": "Login",
|
||||||
|
"login.account.placeholder": "Account",
|
||||||
|
"login.password.placeholder": "Password",
|
||||||
|
"login.forgot": "Forgot password",
|
||||||
|
"login.register": "Register now",
|
||||||
|
"login.submit": "Sign in",
|
||||||
|
"login.setdev": "Set development account",
|
||||||
|
"copyright": "Copyright © 2025 Atomlust.com All rights reserved.",
|
||||||
|
"handler.server": "Server",
|
||||||
|
"handler.wasm": "Wasm",
|
||||||
|
"weather.title": "Weather",
|
||||||
|
"weather.summary": "A quick weather overview",
|
||||||
|
"weather.temperature": "Temperature",
|
||||||
|
"weather.refresh": "Refresh"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
{
|
{
|
||||||
"site.name": "Atomx 管理员"
|
"site.name": "Atomx 管理员",
|
||||||
|
"current.count": "当前计数",
|
||||||
|
"click.me": "点击我",
|
||||||
|
"login.title": "登录",
|
||||||
|
"login.account.placeholder": "登录账号",
|
||||||
|
"login.password.placeholder": "登录密码",
|
||||||
|
"login.forgot": "忘记密码",
|
||||||
|
"login.register": "马上注册",
|
||||||
|
"login.submit": "登录",
|
||||||
|
"login.setdev": "设置开发帐号",
|
||||||
|
"copyright": "Copyright © 2025-2025 Atomlust.com All rights reserved.",
|
||||||
|
"handler.server": "Server",
|
||||||
|
"handler.wasm": "Wasm",
|
||||||
|
"weather.title": "天气",
|
||||||
|
"weather.summary": "天气概览",
|
||||||
|
"weather.temperature": "温度",
|
||||||
|
"weather.refresh": "刷新"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user