chore
This commit is contained in:
@@ -1,61 +0,0 @@
|
||||
@inject LanguageProvider LanguageProvider
|
||||
@inject IStringLocalizer<LanguageSelector> Localizer
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
@GetLanguageDisplayName(LanguageProvider.CurrentLanguage)
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
@foreach (var language in LanguageProvider.SupportedLanguages)
|
||||
{
|
||||
<li>
|
||||
<button class="dropdown-item @(language == LanguageProvider.CurrentLanguage ? "active" : "")"
|
||||
@onclick="() => ChangeLanguage(language)"
|
||||
type="button">
|
||||
@GetLanguageDisplayName(language)
|
||||
@if (language == LanguageProvider.CurrentLanguage)
|
||||
{
|
||||
<span class="badge bg-primary">✓</span>
|
||||
}
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
// 订阅变更以便在语言切换时即时更新该组件
|
||||
LanguageProvider.OnLanguageChanged += OnLanguageChanged;
|
||||
}
|
||||
|
||||
private void OnLanguageChanged()
|
||||
{
|
||||
// 在 UI 线程上下文中触发 StateHasChanged
|
||||
_ = InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
LanguageProvider.OnLanguageChanged -= OnLanguageChanged;
|
||||
}
|
||||
|
||||
private string GetLanguageDisplayName(string languageCode)
|
||||
{
|
||||
return languageCode switch
|
||||
{
|
||||
"zh-Hans" => "简体中文",
|
||||
"en-US" => "English (US)",
|
||||
_ => languageCode
|
||||
};
|
||||
}
|
||||
|
||||
private async Task ChangeLanguage(string languageCode)
|
||||
{
|
||||
await LanguageProvider.ChangeLanguageAsync(languageCode);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
@page "/account/login"
|
||||
@page "/{locale}/account/login"
|
||||
@using System.Text.Json
|
||||
@layout EmptyLayout
|
||||
@inject ILogger<Login> Logger
|
||||
@inject IJSRuntime JS
|
||||
@inject IStringLocalizer<Login> Localizer
|
||||
|
||||
<PageTitle>登录</PageTitle>
|
||||
|
||||
@@ -13,10 +13,6 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<LanguageSelector></LanguageSelector>
|
||||
<p>
|
||||
@LanguageProvider.CurrentLanguage 网站名称 @Localizer["site.name"]
|
||||
</p>
|
||||
<Flex Style="height:100vh" Justify="FlexJustify.Center" Align="FlexAlign.Center" Direction="FlexDirection.Vertical">
|
||||
<GridRow Justify="RowJustify.Center" Class="">
|
||||
<GridCol>
|
||||
@@ -63,6 +59,9 @@ else
|
||||
@code {
|
||||
string handler = "Server";
|
||||
|
||||
[Parameter]
|
||||
public string Locale { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
[SupplyParameterFromQuery(Name = "ReturnUrl")]
|
||||
public string? ReturnUrl { get; set; }
|
||||
|
||||
@@ -25,17 +25,12 @@ builder.Services.AddScoped<IconsExtension>();
|
||||
// <20><> HttpClient <20><><EFBFBD>ڼ<EFBFBD><DABC><EFBFBD> wwwroot/Localization/{culture}.json <20>Ⱦ<EFBFBD>̬<EFBFBD><CCAC><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>
|
||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||
|
||||
// ע<>᱾<EFBFBD>ػ<EFBFBD><D8BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʹ<EFBFBD><CAB9><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>վ<EFBFBD><D5BE><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ¼<C4BF><C2BC> "Localization" <20><>ע<EFBFBD><D7A2><EFBFBD><EFBFBD><EFBFBD>ڼ<EFBFBD><DABC>ؾ<EFBFBD>̬<EFBFBD>ļ<EFBFBD><C4BC><EFBFBD> HttpClient<6E><74>
|
||||
builder.Services.AddScoped<IStringLocalizerFactory>(sp =>
|
||||
new JsonStringLocalizerFactory("localization", sp.GetRequiredService<HttpClient>()));
|
||||
|
||||
// ע<><D7A2>IStringLocalizer
|
||||
builder.Services.AddTransient(typeof(IStringLocalizer<>), typeof(StringLocalizer<>));
|
||||
// <20><><EFBFBD>ӱ<EFBFBD><D3B1>ػ<EFBFBD><D8BB><EFBFBD><EFBFBD><EFBFBD>
|
||||
builder.Services.AddLocalization();
|
||||
|
||||
// ע<><D7A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ṩ<EFBFBD><E1B9A9>
|
||||
builder.Services.AddScoped<LanguageProvider>();
|
||||
|
||||
// ע<><D7A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Զ<EFBFBD><D4B6><EFBFBD><EFBFBD><EFBFBD> token & ˢ<>µ<EFBFBD> DelegatingHandler
|
||||
builder.Services.AddScoped<AuthHeaderHandler>();
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
using Microsoft.Extensions.Localization;
|
||||
using System.Globalization;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Atomx.Admin.Client.Services
|
||||
{
|
||||
public class JsonStringLocalizer : IStringLocalizer
|
||||
{
|
||||
private readonly string _resourcesPath;
|
||||
private readonly Dictionary<string, Dictionary<string, string>> _resourcesCache = new();
|
||||
private readonly object _lock = new();
|
||||
private readonly HttpClient? _httpClient;
|
||||
|
||||
// resourcesPath 应为相对于站点根的“目录”名称,例如 "Localization"
|
||||
// 在 Blazor WebAssembly 场景下,会使用注入的 HttpClient 从 wwwroot/Localization/{culture}.json 获取资源
|
||||
public JsonStringLocalizer(string resourcesPath, HttpClient? httpClient = null)
|
||||
{
|
||||
_resourcesPath = (resourcesPath ?? "localization").Trim('/'); // 规范化
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public LocalizedString this[string name]
|
||||
{
|
||||
get
|
||||
{
|
||||
var value = GetString(name);
|
||||
return new LocalizedString(name, value ?? name, resourceNotFound: value == null);
|
||||
}
|
||||
}
|
||||
|
||||
public LocalizedString this[string name, params object[] arguments]
|
||||
{
|
||||
get
|
||||
{
|
||||
var format = GetString(name);
|
||||
var value = string.Format(format ?? name, arguments);
|
||||
return new LocalizedString(name, value, resourceNotFound: format == null);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
|
||||
{
|
||||
var culture = CultureInfo.CurrentUICulture.Name;
|
||||
var resources = LoadResources(culture);
|
||||
return resources.Select(r => new LocalizedString(r.Key, r.Value, false));
|
||||
}
|
||||
|
||||
private string? GetString(string name)
|
||||
{
|
||||
var culture = CultureInfo.CurrentUICulture.Name;
|
||||
var resources = LoadResources(culture);
|
||||
|
||||
// 尝试当前文化
|
||||
if (resources.TryGetValue(name, out var value))
|
||||
return value;
|
||||
|
||||
// 如果还找不到,尝试英文作为后备
|
||||
if (culture != "en-US")
|
||||
{
|
||||
var enResources = LoadResources("en-US");
|
||||
if (enResources.TryGetValue(name, out var enValue))
|
||||
return enValue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Dictionary<string, string> LoadResources(string culture)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_resourcesCache.TryGetValue(culture, out var cachedResources))
|
||||
return cachedResources;
|
||||
|
||||
var resources = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var fileName = $"{culture}.json";
|
||||
|
||||
// 在浏览器(WASM)环境下,通过 HttpClient 从静态资源目录获取
|
||||
if (OperatingSystem.IsBrowser() && _httpClient != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 构造相对 URL,例如 "Localization/zh-Hans.json"
|
||||
var relativeUrl = $"{_resourcesPath}/{fileName}";
|
||||
var json = _httpClient.GetStringAsync(relativeUrl).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
var jsonResources = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||
if (jsonResources != null)
|
||||
{
|
||||
foreach (var item in jsonResources)
|
||||
{
|
||||
resources[item.Key] = item.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error loading localization file {fileName} via HttpClient: {ex.Message}");
|
||||
}
|
||||
|
||||
_resourcesCache[culture] = resources;
|
||||
return resources;
|
||||
}
|
||||
|
||||
// 非浏览器(例如 Server 或在 prerender 阶段)尝试从文件系统读取。
|
||||
// 尝试几种可能的路径:基路径为 AppContext.BaseDirectory 或当前工作目录,或直接使用传入的路径。
|
||||
try
|
||||
{
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(AppContext.BaseDirectory, _resourcesPath, fileName),
|
||||
Path.Combine(Directory.GetCurrentDirectory(), _resourcesPath, fileName),
|
||||
Path.Combine(_resourcesPath, fileName),
|
||||
};
|
||||
|
||||
var filePath = candidates.FirstOrDefault(File.Exists);
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
var json = File.ReadAllText(filePath);
|
||||
var jsonResources = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||
if (jsonResources != null)
|
||||
{
|
||||
foreach (var item in jsonResources)
|
||||
{
|
||||
resources[item.Key] = item.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error loading localization file {fileName} from disk: {ex.Message}");
|
||||
}
|
||||
|
||||
_resourcesCache[culture] = resources;
|
||||
return resources;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
namespace Atomx.Admin.Client.Services
|
||||
{
|
||||
public class JsonStringLocalizerFactory : IStringLocalizerFactory
|
||||
{
|
||||
private readonly string _resourcesPath;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public JsonStringLocalizerFactory(string resourcesPath, HttpClient httpClient)
|
||||
{
|
||||
_resourcesPath = (resourcesPath ?? "localization").Trim('/');
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public IStringLocalizer Create(Type resourceSource)
|
||||
{
|
||||
return new JsonStringLocalizer(_resourcesPath, _httpClient);
|
||||
}
|
||||
|
||||
public IStringLocalizer Create(string baseName, string location)
|
||||
{
|
||||
return new JsonStringLocalizer(_resourcesPath, _httpClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Atomx.Admin.Client.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// 语言提供者服务
|
||||
/// </summary>
|
||||
public class LanguageProvider
|
||||
{
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
private readonly NavigationManager _navigationManager;
|
||||
private string _currentLanguage = "zh-Hans";
|
||||
|
||||
public event Action? OnLanguageChanged;
|
||||
|
||||
public LanguageProvider(IJSRuntime jsRuntime, NavigationManager navigationManager)
|
||||
{
|
||||
_jsRuntime = jsRuntime;
|
||||
_navigationManager = navigationManager;
|
||||
}
|
||||
|
||||
public string CurrentLanguage
|
||||
{
|
||||
get => _currentLanguage;
|
||||
private set
|
||||
{
|
||||
if (_currentLanguage != value)
|
||||
{
|
||||
_currentLanguage = value;
|
||||
OnLanguageChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<string> SupportedLanguages { get; } = new()
|
||||
{
|
||||
"zh-Hans", // 简体中文
|
||||
"en-US" // 英文(美国)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 初始化语言
|
||||
/// </summary>
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
// 尝试从本地存储获取保存的语言
|
||||
Console.WriteLine("尝试从本地存储获取保存的语言 Initializing LanguageProvider...");
|
||||
try
|
||||
{
|
||||
|
||||
var savedLanguage = await _jsRuntime.InvokeAsync<string>("localStorage.getItem", "preferred-language");
|
||||
if (!string.IsNullOrEmpty(savedLanguage) && SupportedLanguages.Contains(savedLanguage))
|
||||
{
|
||||
CurrentLanguage = savedLanguage;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 从浏览器获取语言
|
||||
var browserLanguage = await _jsRuntime.InvokeAsync<string>("getBrowserLanguage");
|
||||
CurrentLanguage = GetSupportedLanguage(browserLanguage);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// JS互操作可能不可用(在预渲染时)
|
||||
CurrentLanguage = "zh-Hans";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换语言
|
||||
/// </summary>
|
||||
public async Task ChangeLanguageAsync(string languageCode)
|
||||
{
|
||||
Console.WriteLine("切换语言 ChangeLanguageAsync to " + languageCode);
|
||||
if (SupportedLanguages.Contains(languageCode) && CurrentLanguage != languageCode)
|
||||
{
|
||||
CurrentLanguage = languageCode;
|
||||
|
||||
// 保存到本地存储
|
||||
try
|
||||
{
|
||||
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", "preferred-language", languageCode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略错误
|
||||
}
|
||||
|
||||
// 通知语言已更改
|
||||
OnLanguageChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取支持的语言
|
||||
/// </summary>
|
||||
private string GetSupportedLanguage(string browserLanguage)
|
||||
{
|
||||
if (string.IsNullOrEmpty(browserLanguage))
|
||||
return "zh-Hans";
|
||||
|
||||
// 检查完全匹配
|
||||
if (SupportedLanguages.Contains(browserLanguage))
|
||||
return browserLanguage;
|
||||
|
||||
// 检查中性语言匹配
|
||||
var neutralLanguage = browserLanguage.Split('-')[0];
|
||||
foreach (var supported in SupportedLanguages)
|
||||
{
|
||||
if (supported.StartsWith(neutralLanguage))
|
||||
return supported;
|
||||
}
|
||||
|
||||
return "zh-Hans"; // 默认语言
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace Atomx.Admin.Client.Services
|
||||
{
|
||||
public interface ILocalizationService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前文化环境
|
||||
/// </summary>d
|
||||
CultureInfo CurrentCulture { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 当语言发生改变时触发的事件。
|
||||
/// </summary>
|
||||
event EventHandler<CultureInfo> LanguageChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 当语言发生改变时触发的事件。调用 <see cref="InteractiveStringLocalizer"/> 来更改语言环境。
|
||||
/// </summary>
|
||||
/// <param name="culture"></param>
|
||||
void SetLanguage(CultureInfo culture);
|
||||
}
|
||||
|
||||
|
||||
public class LocalizationService : ILocalizationService
|
||||
{
|
||||
private CultureInfo? _currentCulture;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前文化环境
|
||||
/// </summary>
|
||||
public CultureInfo CurrentCulture => _currentCulture ?? CultureInfo.CurrentCulture;
|
||||
|
||||
public event EventHandler<CultureInfo> LanguageChanged = default!;
|
||||
|
||||
public void SetLanguage(CultureInfo culture)
|
||||
{
|
||||
if (!culture.Equals(CultureInfo.CurrentCulture))
|
||||
{
|
||||
CultureInfo.CurrentCulture = culture;
|
||||
}
|
||||
|
||||
if (_currentCulture == null || !_currentCulture.Equals(culture))
|
||||
{
|
||||
_currentCulture = culture;
|
||||
CultureInfo.DefaultThreadCurrentCulture = culture;
|
||||
CultureInfo.DefaultThreadCurrentUICulture = culture;
|
||||
|
||||
LanguageChanged?.Invoke(this, culture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using Atomx.Admin.Client.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Atomx.Admin.Client.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// 继承此基类的组件会自动订阅 LanguageProvider 的语言变更事件并在变更时重新渲染。
|
||||
/// </summary>
|
||||
public abstract class LocalizedComponentBase : ComponentBase, IDisposable
|
||||
{
|
||||
[Inject]
|
||||
protected LanguageProvider LanguageProvider { get; set; } = null!;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
LanguageProvider.OnLanguageChanged += LanguageChangedHandler;
|
||||
}
|
||||
|
||||
private void LanguageChangedHandler()
|
||||
{
|
||||
// 在组件上下文中安全调用 StateHasChanged
|
||||
_ = InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
LanguageProvider.OnLanguageChanged -= LanguageChangedHandler;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,5 +38,4 @@
|
||||
@inject NavigationManager Navigation
|
||||
@inject HttpService HttpService
|
||||
@inject MessageService MessageService
|
||||
@inject ModalService ModalService
|
||||
@inject LanguageProvider LanguageProvider
|
||||
@inject ModalService ModalService
|
||||
Reference in New Issue
Block a user