545 lines
25 KiB
C#
545 lines
25 KiB
C#
using Microsoft.JSInterop;
|
||
using System.Collections.Concurrent;
|
||
using System.Globalization;
|
||
using System.Text.Json;
|
||
|
||
namespace Atomx.Admin.Client.Services
|
||
{
|
||
/// <summary>
|
||
/// 本类提供多语言 JSON 文件的加载、缓存与运行时切换功能。
|
||
/// 主要职责:
|
||
/// - 在 Server 环境(例如 prerender 或 Blazor Server)时尝试从 webroot 同步读取本地化 JSON 文件并缓存,
|
||
/// 以便在服务端渲染阶段立即可用。
|
||
/// - 在 WASM 环境时使用注入的 HttpClient 从 /localization/{culture}.json 下载并解析。
|
||
/// - 在切换语言时写入名为 `atomx.culture` 的 cookie(供后端读取)并尝试设置页面的 HTML lang 属性。
|
||
/// - 提供事件通知 LanguageChanged,供 UI 或其他服务订阅以响应语言变更。
|
||
///
|
||
/// 说明:为了兼容 Server 与 WASM,本 Provider 会尽量根据运行时环境选择最合适的资源加载方式。
|
||
/// </summary>
|
||
public interface ILocalizationProvider
|
||
{
|
||
string CurrentCulture { get; }
|
||
string? GetString(string key);
|
||
Task SetCultureAsync(string cultureShortOrFull);
|
||
Task InitializeAsync();
|
||
/// <summary>
|
||
/// 保证指定文化的资源已被加载(异步)。供外部在需要时触发加载。
|
||
/// </summary>
|
||
Task LoadCultureAsync(string culture);
|
||
event EventHandler<string>? LanguageChanged;
|
||
}
|
||
|
||
/// <summary>
|
||
/// LocalizationProvider 的实现:
|
||
/// - 维护一个静态缓存,避免重复下载/读取资源。
|
||
/// - 支持短码(如 zh / en)与完整 culture(如 zh-Hans / en-US)之间的映射。
|
||
/// - 在 Server 端支持同步加载以满足 prerender 场景。
|
||
/// </summary>
|
||
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;
|
||
|
||
// 缓存:culture -> translations。使用 ConcurrentDictionary 以线程安全地共享。
|
||
// 使用静态字段是为了使中间件/服务在同一进程内能够复用已加载的翻译,避免重复 I/O。
|
||
private static readonly ConcurrentDictionary<string, Dictionary<string, string>> _cache = new();
|
||
|
||
// 支持的短码映射,后续如需扩展可在此添加。
|
||
private static readonly Dictionary<string, string> ShortToCulture = new(StringComparer.OrdinalIgnoreCase)
|
||
{
|
||
{ "zh", "zh-Hans" },
|
||
{ "en", "en-US" }
|
||
};
|
||
|
||
// 默认文化(当未能从浏览器/URL/Cookie 中解析到文化时使用)
|
||
private string _currentCulture = "zh-Hans";
|
||
|
||
private const string CookieName = "atomx.culture";
|
||
|
||
/// <summary>
|
||
/// 构造函数:通过依赖注入获取必要服务。
|
||
/// 注意构造函数中不应执行耗时或 JS 相关的同步操作。
|
||
/// </summary>
|
||
public LocalizationProvider(IServiceProvider sp, ILogger<LocalizationProvider> logger, IHttpClientFactory? httpClientFactory, IJSRuntime? jsRuntime, ILocalizationService localizationService)
|
||
{
|
||
_sp = sp;
|
||
_httpClientFactory = httpClientFactory;
|
||
_jsRuntime = jsRuntime;
|
||
_logger = logger;
|
||
_localizationService = localizationService;
|
||
|
||
// 尝试根据当前线程 culture 初始化 _currentCulture(不抛出异常)
|
||
try
|
||
{
|
||
var threadUi = CultureInfo.DefaultThreadCurrentUICulture ?? CultureInfo.CurrentUICulture;
|
||
if (!string.IsNullOrEmpty(threadUi?.Name))
|
||
{
|
||
_currentCulture = MapToFullCulture(threadUi!.Name);
|
||
_logger.LogDebug("LocalizationProvider 构造检测到线程 UI 文化: {Culture}", _currentCulture);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogDebug(ex, "LocalizationProvider 构造读取线程文化失败");
|
||
}
|
||
|
||
// 如果在 Server 环境且未能使用 JSRuntime(意味着非浏览器),则尝试同步从文件系统读取本地化文件以支持 prerender
|
||
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("(Server 同步) 从路径加载本地化文件 {Path},Culture:{Culture},条目数:{Count}", path, _currentCulture, dict.Count);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogWarning(ex, "(Server 同步) 读取本地化文件失败: {Path}", path);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
_logger.LogDebug("本地化文件未在路径找到: {Path}", path);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogDebug(ex, "LocalizationProvider 构造中同步文件加载尝试失败");
|
||
}
|
||
}
|
||
|
||
public string CurrentCulture => _currentCulture;
|
||
|
||
public event EventHandler<string>? LanguageChanged;
|
||
|
||
/// <summary>
|
||
/// 从缓存中读取指定键的本地化字符串,未找到返回 null。
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化流程(浏览器端)
|
||
/// 优先级:URL 语言前缀 -> Cookie(atomx.culture) -> 浏览器语言 -> 默认
|
||
/// 若解析到短码(如 zh/en)则映射为完整文化(如 zh-Hans/en-US)。
|
||
/// </summary>
|
||
public async Task InitializeAsync()
|
||
{
|
||
_logger.LogDebug("LocalizationProvider.InitializeAsync 开始. 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("检测到 URL 首段: {Segment}", urlFirstSegment);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogDebug(ex, "读取 location.pathname 失败");
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(urlFirstSegment) && ShortToCulture.TryGetValue(urlFirstSegment, out var mapped))
|
||
{
|
||
_logger.LogDebug("URL 短码 '{Seg}' 映射为文化 '{Culture}'", urlFirstSegment, mapped);
|
||
await SetCultureInternalAsync(mapped, persistCookie: false);
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
if (_jsRuntime != null && OperatingSystem.IsBrowser())
|
||
{
|
||
var cookieVal = await _jsRuntime.InvokeAsync<string>("cookies.Read", CookieName);
|
||
_logger.LogDebug("读取 Cookie '{CookieName}'='{CookieVal}'", CookieName, cookieVal);
|
||
if (!string.IsNullOrEmpty(cookieVal))
|
||
{
|
||
if (ShortToCulture.TryGetValue(cookieVal, out var mappedFromCookie))
|
||
{
|
||
_logger.LogDebug("Cookie 短码 '{Cookie}' 映射为文化 {Culture}", cookieVal, mappedFromCookie);
|
||
await SetCultureInternalAsync(mappedFromCookie, persistCookie: false);
|
||
return;
|
||
}
|
||
else
|
||
{
|
||
// cookie 中可能已经是完整 culture,进行平滑归一化
|
||
await SetCultureInternalAsync(MapToFullCulture(cookieVal), persistCookie: false);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogDebug(ex, "读取 Cookie 失败");
|
||
}
|
||
|
||
try
|
||
{
|
||
if (_jsRuntime != null && OperatingSystem.IsBrowser())
|
||
{
|
||
var browserLang = await _jsRuntime.InvokeAsync<string>("getBrowserLanguage");
|
||
_logger.LogDebug("浏览器语言: {BrowserLang}", browserLang);
|
||
if (!string.IsNullOrEmpty(browserLang))
|
||
{
|
||
var mappedFromBrowser = MapToFullCulture(browserLang);
|
||
_logger.LogDebug("浏览器语言映射为 {Culture}", mappedFromBrowser);
|
||
await SetCultureInternalAsync(mappedFromBrowser, persistCookie: false);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogDebug(ex, "读取浏览器语言失败");
|
||
}
|
||
|
||
// 回退到当前默认文化并确保加载资源
|
||
_logger.LogDebug("InitializeAsync 回退使用当前文化 {Culture}", _currentCulture);
|
||
await EnsureCultureLoadedAsync(_currentCulture);
|
||
await SetCultureInternalAsync(_currentCulture, persistCookie: false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 对外异步设置文化(可传短码或完整 culture),并可选择是否持久化到 Cookie
|
||
/// </summary>
|
||
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));
|
||
|
||
/// <summary>
|
||
/// Server 端用于 prerender 时的同步设置:立即设置线程 Culture 并尝试从 webroot 同步加载本地化文件。
|
||
/// 该方法不会触发 JS 操作,适合在中间件或请求处理早期使用。
|
||
/// </summary>
|
||
public void SetCultureForServer(string cultureShortOrFull)
|
||
{
|
||
try
|
||
{
|
||
var cultureFull = MapToFullCulture(cultureShortOrFull);
|
||
if (string.IsNullOrEmpty(cultureFull)) return;
|
||
|
||
// 设置线程 culture,影响后续服务端处理(例如 IStringLocalizer)
|
||
try
|
||
{
|
||
var ci = new CultureInfo(cultureFull);
|
||
CultureInfo.DefaultThreadCurrentCulture = ci;
|
||
CultureInfo.DefaultThreadCurrentUICulture = ci;
|
||
_currentCulture = cultureFull;
|
||
}
|
||
catch { }
|
||
|
||
// 同步从 webroot 加载 JSON 本地化文件(若存在)
|
||
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");
|
||
if (File.Exists(path))
|
||
{
|
||
try
|
||
{
|
||
var json = File.ReadAllText(path);
|
||
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new Dictionary<string, string>();
|
||
_cache[cultureFull] = dict;
|
||
_logger.LogInformation("(Server 同步) 已为 {Culture} 从路径加载本地化文件,条目数: {Count}", cultureFull, dict.Count);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogWarning(ex, "(Server 同步) 读取本地化文件失败: {Path}", path);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogDebug(ex, "SetCultureForServer 加载文件时发生错误: {Culture}", cultureFull);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogDebug(ex, "SetCultureForServer 执行过程中发生异常");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 内部设置文化并在需要时持久化 Cookie、更新 localization service 并触发事件。
|
||
/// </summary>
|
||
private async Task SetCultureInternalAsync(string cultureFull, bool persistCookie)
|
||
{
|
||
//_logger.LogDebug("设置内部文化异步开始: {Culture}, 持久化={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}", cultureFull);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogWarning(ex, "设置 Culture 失败: {Culture}", cultureFull);
|
||
}
|
||
|
||
if (persistCookie && _jsRuntime != null)
|
||
{
|
||
try
|
||
{
|
||
var shortKey = ShortToCulture.FirstOrDefault(kv => string.Equals(kv.Value, cultureFull, StringComparison.OrdinalIgnoreCase)).Key ?? cultureFull;
|
||
// 将 shortKey 写入 cookie,供后端请求读取(例如 Server 模式下的中间件)
|
||
await _jsRuntime.InvokeVoidAsync("cookies.Write", CookieName, shortKey, DateTime.UtcNow.AddYears(1).ToString("o"));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogDebug(ex, "写入 Cookie 失败");
|
||
}
|
||
}
|
||
|
||
try
|
||
{
|
||
if (_jsRuntime != null)
|
||
{
|
||
// 尝试设置 HTML 的 lang 属性,改善无障碍/SEO
|
||
await _jsRuntime.InvokeVoidAsync("setHtmlLang", cultureFull);
|
||
}
|
||
}
|
||
catch { }
|
||
|
||
// 通知订阅者
|
||
LanguageChanged?.Invoke(this, cultureFull);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 确保指定文化的 JSON 文件已被加载到缓存。加载顺序:WASM HttpClient -> 文件系统 -> 空字典占位。
|
||
/// </summary>
|
||
private async Task EnsureCultureLoadedAsync(string cultureFull)
|
||
{
|
||
// 归一化短码(例如 zh -> zh-Hans)
|
||
cultureFull = MapToFullCulture(cultureFull);
|
||
|
||
if (string.IsNullOrEmpty(cultureFull)) return;
|
||
if (_cache.ContainsKey(cultureFull))
|
||
{
|
||
_logger.LogDebug("EnsureCultureLoadedAsync: 文化 {Culture} 已缓存", cultureFull);
|
||
return;
|
||
}
|
||
|
||
// 如果在浏览器(WASM),优先使用 HttpClient 下载本地化 JSON
|
||
if (_jsRuntime != null && OperatingSystem.IsBrowser())
|
||
{
|
||
_logger.LogInformation("EnsureCultureLoadedAsync: 在浏览器环境,将尝试通过 HttpClient 下载 {Culture}", cultureFull);
|
||
try
|
||
{
|
||
var http = _sp.GetService(typeof(HttpClient)) as HttpClient;
|
||
if (http == null && _httpClientFactory != null)
|
||
{
|
||
_logger.LogDebug("未从 ServiceProvider 解析到 HttpClient,使用 IHttpClientFactory 创建");
|
||
http = _httpClientFactory.CreateClient();
|
||
}
|
||
else
|
||
{
|
||
_logger.LogDebug("从 ServiceProvider 解析 HttpClient: {HasClient}", http != null);
|
||
}
|
||
|
||
if (http != null)
|
||
{
|
||
var url = $"/localization/{cultureFull}.json";
|
||
Uri? requestUri = null;
|
||
|
||
// 当 HttpClient 配置了 BaseAddress 时使用;否则通过 JS 获取 location.origin 构建绝对 URI
|
||
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))
|
||
{
|
||
requestUri = new Uri(new Uri(origin), url);
|
||
}
|
||
}
|
||
catch (Exception jsEx)
|
||
{
|
||
_logger.LogDebug(jsEx, "从 JS 获取 location.origin 失败");
|
||
}
|
||
}
|
||
|
||
if (requestUri != null)
|
||
{
|
||
_logger.LogInformation("从 {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("通过 HttpClient 为 {Culture} 加载到本地化数据,条目数: {Count}", cultureFull, dict.Count);
|
||
return;
|
||
}
|
||
else
|
||
{
|
||
_logger.LogWarning("HttpClient 无法构建请求 URL,跳过通过 HttpClient 加载 {Culture}", cultureFull);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
_logger.LogWarning("未找到可用的 HttpClient 以加载 {Culture}", cultureFull);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogDebug(ex, "通过 HttpClient 加载本地化文件失败: {Culture}", cultureFull);
|
||
}
|
||
}
|
||
|
||
_logger.LogDebug("EnsureCultureLoadedAsync: 尝试文件系统加载 {Culture}", cultureFull);
|
||
// 回退:尝试通过 IWebHostEnvironment 从文件系统读取(Server 时可用)
|
||
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("查找本地化文件路径: {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("从文件为 {Culture} 加载本地化,条目数: {Count}", cultureFull, dict.Count);
|
||
return;
|
||
}
|
||
else
|
||
{
|
||
_logger.LogDebug("未在路径找到本地化文件: {Path}", path);
|
||
// 备用路径:构建于运行时输出目录的 wwwroot
|
||
try
|
||
{
|
||
var alt = Path.Combine(AppContext.BaseDirectory ?? ".", "wwwroot", "localization", cultureFull + ".json");
|
||
_logger.LogDebug("查找备用路径: {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("从备用路径为 {Culture} 加载到本地化文件,条目数: {Count}", cultureFull, dict2.Count);
|
||
return;
|
||
}
|
||
else
|
||
{
|
||
_logger.LogDebug("备用路径未找到本地化文件: {AltPath}", alt);
|
||
}
|
||
}
|
||
catch (Exception exAlt)
|
||
{
|
||
_logger.LogDebug(exAlt, "检查备用本地化路径时出错");
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
_logger.LogDebug("无法从 ServiceProvider 获取 IWebHostEnvironment 实例");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
_logger.LogDebug("通过反射未能找到 IWebHostEnvironment 类型");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogDebug(ex, "通过文件系统加载本地化文件失败: {Culture}", cultureFull);
|
||
}
|
||
|
||
_logger.LogDebug("EnsureCultureLoadedAsync: 回退为 {Culture} 的空字典占位", cultureFull);
|
||
_cache[cultureFull] = new Dictionary<string, string>();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将短码或带区域的字符串映射为内部使用的完整 culture(例如 zh -> zh-Hans)
|
||
/// </summary>
|
||
private string MapToFullCulture(string culture)
|
||
{
|
||
if (string.IsNullOrEmpty(culture)) return culture;
|
||
// 直接映射
|
||
if (ShortToCulture.TryGetValue(culture, out var mapped)) return mapped;
|
||
// 考虑前缀,例如 zh-CN -> zh
|
||
var prefix = culture.Split('-', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
|
||
if (!string.IsNullOrEmpty(prefix) && ShortToCulture.TryGetValue(prefix, out var mapped2)) return mapped2;
|
||
return culture;
|
||
}
|
||
}
|
||
|
||
|
||
}
|