实现多语言切换
This commit is contained in:
@@ -1,18 +1,87 @@
|
||||
@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 {
|
||||
|
||||
[Parameter]
|
||||
public string Locale { get; set; } = string.Empty;
|
||||
|
||||
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()
|
||||
{
|
||||
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
|
||||
@inject ILogger<Login> Logger
|
||||
@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)
|
||||
{
|
||||
@@ -20,29 +23,29 @@ else
|
||||
<Form @ref="form" Model="@login" OnFinish="LoginAsync">
|
||||
<FluentValidationValidator />
|
||||
<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>
|
||||
</AntDesign.Input>
|
||||
</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>
|
||||
</AntDesign.Input>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<a style="float: left;">
|
||||
忘记密码
|
||||
@L["login.forgot"]
|
||||
</a>
|
||||
<a style="float: right;">
|
||||
<NavLink href="/register">马上注册</NavLink>
|
||||
<NavLink href="/register">@L["login.register"]</NavLink>
|
||||
</a>
|
||||
</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>
|
||||
<a @onclick="setAccount">
|
||||
设置开发帐号
|
||||
@L["login.setdev"]
|
||||
</a>
|
||||
</FormItem>
|
||||
</Form>
|
||||
@@ -50,9 +53,31 @@ else
|
||||
</GridCol>
|
||||
</GridRow>
|
||||
<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>
|
||||
</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>
|
||||
}
|
||||
|
||||
@@ -86,6 +111,11 @@ else
|
||||
{
|
||||
handler = "Server";
|
||||
}
|
||||
|
||||
if (LocalizationProvider != null)
|
||||
{
|
||||
LocalizationProvider.LanguageChanged += OnLanguageChanged;
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
@@ -103,6 +133,25 @@ else
|
||||
dataLoaded = true;
|
||||
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()
|
||||
@@ -200,6 +249,15 @@ else
|
||||
login.Account = "admin";
|
||||
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 *@
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
@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)
|
||||
{
|
||||
@@ -16,7 +27,7 @@ else
|
||||
<thead>
|
||||
<tr>
|
||||
<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>Summary</th>
|
||||
</tr>
|
||||
@@ -36,10 +47,22 @@ else
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string Locale { get; set; } = string.Empty;
|
||||
private WeatherForecast[]? forecasts;
|
||||
|
||||
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
|
||||
await Task.Delay(500);
|
||||
|
||||
@@ -53,6 +76,10 @@ else
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
private void OnLanguageChanged(object? s, string c) => _ = InvokeAsync(StateHasChanged);
|
||||
|
||||
public void Dispose() { if (LocalizationProvider != null) LocalizationProvider.LanguageChanged -= OnLanguageChanged; }
|
||||
|
||||
private class WeatherForecast
|
||||
{
|
||||
public DateOnly Date { get; set; }
|
||||
@@ -60,4 +87,13 @@ else
|
||||
public string? Summary { get; set; }
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user