using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using NewLife;
using NewLife.Log;
using NewLife.Remoting;
using NewLife.Remoting.Clients;
using NewLife.Remoting.Extensions;
using Xunit;
namespace XUnitTest.Common;
/// <summary>组件多è¯è¨€è¯è¡¨éªŒè¯</summary>
/// <remarks>
/// 本类用例会临时改写 <see cref="Localization.Culture"/>(进程级全局状æ€ï¼‰ï¼Œä¸Žå…¶å®ƒç”¨ä¾‹ä¸²è¡Œæ‰§è¡Œï¼Œ
/// é¿å…并行用例读到被临时改写的è¯è¨€ã€‚英文原文是è¯è¡¨é”®ï¼Œç¼ºè¯‘文时回退英文。
/// 组件è¯è¡¨ç”±æºç”Ÿæˆå™¨äº§å‡ºçš„æ¨¡å—åˆå§‹åŒ–器注册,首次访问程åºé›†æ—¶è§¦å‘ï¼›æ¤å¤„显å¼è§¦å‘扩展库的模å—åˆå§‹åŒ–,
/// é¿å…用例执行顺åºå½±å“注册时机。
/// </remarks>
[Collection("GlobalState")]
public class LocalizationTests
{
static LocalizationTests() => RuntimeHelpers.RunModuleConstructor(typeof(NewLife.Remoting.Extensions.ApiFilterAttribute).Module.ModuleHandle);
/// <summary>NewLife.Remoting è¯æ¡</summary>
private const String CoreKey = "Server address is not specified.";
/// <summary>NewLife.Remoting.Extensions è¯æ¡</summary>
private const String ExtKey = "Authentication failed";
#region 䏿–‡
[Fact(DisplayName = "多è¯è¨€ï¼šç®€ä½“䏿–‡æŒ‰è‹±æ–‡åŽŸæ–‡æŸ¥åˆ°ç»„ä»¶è¯‘æ–‡")]
public void Translate_zh_Hans()
{
var saved = Localization.Culture;
try
{
Localization.Culture = CultureInfo.GetCultureInfo("zh-CN");
Assert.Equal("未指定æœåŠ¡ç«¯åœ°å€ã€‚", Localization.Translate(CoreKey));
Assert.Equal("认è¯å¤±è´¥", Localization.Translate(ExtKey));
}
finally
{
Localization.Culture = saved;
}
}
[Fact(DisplayName = "多è¯è¨€ï¼šç¹ä½“䏿–‡ç”± zh-Hant 统一承接 zh-TW")]
public void Translate_zh_Hant()
{
var saved = Localization.Culture;
try
{
Localization.Culture = CultureInfo.GetCultureInfo("zh-TW");
Assert.Equal("未指定伺æœå™¨ä½å€ã€‚", Localization.Translate(CoreKey));
Assert.Equal("é©—è‰å¤±æ•—", Localization.Translate(ExtKey));
}
finally
{
Localization.Culture = saved;
}
}
#endregion
#region 其它è¯è¨€
[Fact(DisplayName = "多è¯è¨€ï¼šå¾·è¯ç‰éžä¸æ–‡è¯è¨€è¯è¡¨å·²æ³¨å†Œ")]
public void Translate_de()
{
var saved = Localization.Culture;
try
{
Localization.Culture = CultureInfo.GetCultureInfo("de-DE");
Assert.Equal("Serveradresse ist nicht angegeben.", Localization.Translate(CoreKey));
Assert.Equal("Authentifizierung fehlgeschlagen", Localization.Translate(ExtKey));
}
finally
{
Localization.Culture = saved;
}
}
/// <summary>与 locales.json ä¿æŒä¸€è‡´çš„ 31 ç§å†…ç½®è¯è¨€</summary>
public static readonly String[] Locales =
[
"ar", "bn", "cs", "da", "de", "el", "es", "fa", "fi", "fr", "he", "hi", "hu", "id", "it", "ja",
"ko", "ms", "nb", "nl", "pl", "pt", "ro", "ru", "sv", "th", "tr", "uk", "vi", "zh-Hans", "zh-Hant",
];
/// <summary>全部内置è¯è¨€çš„æµ‹è¯•æ•°æ®</summary>
public static IEnumerable<Object[]> LocaleData => Locales.Select(e => new Object[] { e });
[Theory(DisplayName = "多è¯è¨€ï¼šå…¨éƒ¨å†…ç½®è¯è¨€çš„è¯è¡¨å‡åœ¨è¿è¡Œæ—¶æ³¨å†Œ")]
[MemberData(nameof(LocaleData))]
public void Translate_all_locales(String name)
{
var saved = Localization.Culture;
try
{
Localization.Culture = CultureInfo.GetCultureInfo(name);
Assert.NotEqual(CoreKey, Localization.Translate(CoreKey));
Assert.NotEqual(ExtKey, Localization.Translate(ExtKey));
// 元数æ®è¯æ¡åŒæ ·è¦æ±‚å…¨è¯è¨€è¦†ç›–,ä¸å¾—å›žé€€è‹±æ–‡æºæ–‡
Assert.NotEqual(MainDescription, Localization.Translate(MainDescription));
Assert.NotEqual(ExtDescription, Localization.Translate(ExtDescription));
Assert.NotEqual("Load balancing", Localization.Translate("Load balancing"));
}
finally
{
Localization.Culture = saved;
}
}
#endregion
#region 回退
[Theory(DisplayName = "多è¯è¨€ï¼šæ— è¯è¡¨çš„è¯è¨€å›žé€€è‹±æ–‡åŽŸæ–‡")]
[InlineData("en-US")]
[InlineData("sw-KE")]
public void Translate_fallback(String name)
{
var saved = Localization.Culture;
try
{
Localization.Culture = CultureInfo.GetCultureInfo(name);
Assert.Equal(CoreKey, Localization.Translate(CoreKey));
Assert.Equal(ExtKey, Localization.Translate(ExtKey));
}
finally
{
Localization.Culture = saved;
}
}
[Fact(DisplayName = "多è¯è¨€ï¼šæœªçŸ¥é”®å›žé€€è‹±æ–‡åŽŸæ–‡")]
public void Translate_unknown_key() => Assert.Equal("Not a translated message.", Localization.Translate("Not a translated message."));
#endregion
#region 元数æ®
/// <summary>NewLife.Remoting 程åºé›†æè¿°ï¼ˆè¯è¡¨æºæ–‡ï¼Œå…许其它è¯è¨€å›žé€€è‹±æ–‡ï¼‰</summary>
private const String MainDescription = "Unified RPC, HTTP, WebSocket and SRMP communication for client-server and IoT applications.";
/// <summary>NewLife.Remoting.Extensions 程åºé›†æè¿°ï¼ˆè¯è¡¨æºæ–‡ï¼Œå…许其它è¯è¨€å›žé€€è‹±æ–‡ï¼‰</summary>
private const String ExtDescription = "Device access controllers, token services and session management extensions for NewLife.Remoting.";
[Fact(DisplayName = "多è¯è¨€ï¼šç¨‹åºé›†æ ‡é¢˜ä¸Žæè¿°ä¸ºè‹±æ–‡æºæ–‡ï¼Œä¸æ–‡æœ‰è¯‘æ–‡")]
public void Translate_assembly_metadata()
{
var main = typeof(ApiClient).Assembly;
var ext = typeof(ApiFilterAttribute).Assembly;
Assert.Equal("NewLife Remoting Library", main.GetCustomAttribute<AssemblyTitleAttribute>()?.Title);
Assert.Equal(MainDescription, main.GetCustomAttribute<AssemblyDescriptionAttribute>()?.Description);
Assert.Equal("NewLife Remoting Extensions", ext.GetCustomAttribute<AssemblyTitleAttribute>()?.Title);
Assert.Equal(ExtDescription, ext.GetCustomAttribute<AssemblyDescriptionAttribute>()?.Description);
var saved = Localization.Culture;
try
{
Localization.Culture = CultureInfo.GetCultureInfo("zh-CN");
Assert.Equal("NewLife统一远程通信框架", Localization.Translate("NewLife Remoting Library"));
Assert.Equal("æä¾›RPCã€HTTPã€WebSocket与SRMP统一通信能力", Localization.Translate(MainDescription));
Assert.Equal("远程通信æœåŠ¡æ‰©å±•", Localization.Translate("NewLife Remoting Extensions"));
Assert.Equal("æä¾›è®¾å¤‡æŽ¥å…¥æŽ§åˆ¶å™¨ã€ä»¤ç‰ŒæœåŠ¡ä¸Žä¼šè¯ç®¡ç†æ‰©å±•", Localization.Translate(ExtDescription));
// 全部è¯è¨€å…¨é‡è¦†ç›–:德è¯ç‰éžä¸æ–‡è¯è¨€åŒæ ·æœ‰è¯‘æ–‡
Localization.Culture = CultureInfo.GetCultureInfo("de-DE");
Assert.Equal("NewLife Remoting-Bibliothek", Localization.Translate("NewLife Remoting Library"));
Assert.Equal("Einheitliche RPC-, HTTP-, WebSocket- und SRMP-Kommunikation für Client-Server- und IoT-Anwendungen.", Localization.Translate(MainDescription));
// æœªå†…ç½®çš„åŒºåŸŸå›žé€€è‹±æ–‡æºæ–‡
Localization.Culture = CultureInfo.GetCultureInfo("sw-KE");
Assert.Equal("NewLife Remoting Library", Localization.Translate("NewLife Remoting Library"));
}
finally
{
Localization.Culture = saved;
}
}
[Fact(DisplayName = "多è¯è¨€ï¼šé›†ç¾¤å…ƒæ•°æ®æ ‡ç¾ä¸ºè‹±æ–‡æºæ–‡ï¼Œä¸æ–‡æœ‰è¯‘æ–‡")]
public void Translate_cluster_metadata()
{
Assert.Equal("Load balancing", typeof(ClientPoolCluster<String>).GetCustomAttribute<DisplayNameAttribute>()?.DisplayName);
Assert.Equal("Failover", typeof(ClientSingleCluster<String>).GetCustomAttribute<DisplayNameAttribute>()?.DisplayName);
var saved = Localization.Culture;
try
{
Localization.Culture = CultureInfo.GetCultureInfo("zh-CN");
Assert.Equal("è´Ÿè½½å‡è¡¡", Localization.Translate("Load balancing"));
Assert.Equal("故障转移", Localization.Translate("Failover"));
Localization.Culture = CultureInfo.GetCultureInfo("de-DE");
Assert.Equal("Lastverteilung", Localization.Translate("Load balancing"));
}
finally
{
Localization.Culture = saved;
}
}
#endregion
#region å ä½ç¬¦
[Fact(DisplayName = "多è¯è¨€ï¼šå¸¦å ä½ç¬¦çš„æ¨¡æ¿æŒ‰è¯‘æ–‡æ ¼å¼åŒ–")]
public void Translate_placeholders()
{
var saved = Localization.Culture;
try
{
Localization.Culture = CultureInfo.GetCultureInfo("zh-CN");
var text = Localization.Translate("Cannot find service named [{0}]!", "Api/Info");
Assert.Equal("æ— æ³•æ‰¾åˆ°å为[Api/Info]çš„æœåŠ¡ï¼", text);
}
finally
{
Localization.Culture = saved;
}
}
[Theory(DisplayName = "多è¯è¨€ï¼šå¸¦å ä½ç¬¦çš„æ¨¡æ¿åœ¨å…¨éƒ¨è¯è¨€ä¸å‡å¯æ ¼å¼åŒ–")]
[MemberData(nameof(LocaleData))]
public void Translate_placeholders_all_locales(String name)
{
var saved = Localization.Culture;
try
{
Localization.Culture = CultureInfo.GetCultureInfo(name);
var text = Localization.Translate("Cannot find service named [{0}]!", "Api/Info");
Assert.Contains("Api/Info", text);
Assert.DoesNotContain("{0}", text);
}
finally
{
Localization.Culture = saved;
}
}
#endregion
#region 异常
[Theory(DisplayName = "多è¯è¨€ï¼šå¼‚常消æ¯åœ¨å…¨éƒ¨è¯è¨€ä¸æŒ‰è¯è¡¨ç¿»è¯‘")]
[MemberData(nameof(LocaleData))]
public void Exception_translates_all_locales(String name)
{
var saved = Localization.Culture;
try
{
Localization.Culture = CultureInfo.GetCultureInfo(name);
var ex = new XException("Update succeeded!");
Assert.NotEqual("Update succeeded!", ex.Message);
}
finally
{
Localization.Culture = saved;
}
}
#endregion
#region 日志
[Fact(DisplayName = "多è¯è¨€ï¼šæ—¥å¿—拼接åç§°å‰ç¼€åŽä»æŒ‰æ¨¡æ¿ç¿»è¯‘")]
public void WriteLog_translates_template_after_name_prefix()
{
var saved = Localization.Culture;
var savedLog = Localization.LogCulture;
try
{
Localization.Culture = CultureInfo.GetCultureInfo("zh-CN");
Localization.LogCulture = CultureInfo.GetCultureInfo("zh-CN");
var log1 = new CaptureLog();
var host = new TestHost { Name = "Test", Log = log1 };
host.WriteLog("Slow call [{0}] took {1:n0}ms", "Api/Info", 1_234);
Assert.Equal("[Test]慢调用[Api/Info],耗时1,234ms", log1.Message);
var log2 = new CaptureLog();
var upgrade = new Upgrade { Name = "Test", Log = log2 };
upgrade.WriteLog("Update succeeded!");
Assert.Equal("[Test]æ›´æ–°æˆåŠŸï¼", log2.Message);
var log3 = new CaptureLog();
var client = new TestClient { Name = "Test", Log = log3 };
client.WriteLog("Logout: {0} {1}", "Api/Info", "old");
Assert.Equal("[Test]注销:Api/Info old", log3.Message);
var log4 = new CaptureLog();
var cluster = new ClientSingleCluster<String> { Name = "Test", Log = log4 };
cluster.WriteLog("Cluster connecting: {0}", "svc");
Assert.Equal("[Test]集群连接:svc", log4.Message);
var log5 = new CaptureLog();
var pool = new ClientPoolCluster<String> { Name = "Test", Log = log5 };
pool.WriteLog("Cluster balancing: {0}", "svc");
Assert.Equal("[Test]集群å‡è¡¡ï¼šsvc", log5.Message);
}
finally
{
Localization.Culture = saved;
Localization.LogCulture = savedLog;
}
}
[Theory(DisplayName = "多è¯è¨€ï¼šå…¨éƒ¨å†…ç½®è¯è¨€çš„æ—¥å¿—模æ¿å‡æŒ‰æ—¥å¿—è¯è¨€ç¿»è¯‘")]
[MemberData(nameof(LocaleData))]
public void WriteLog_translates_all_locales(String name)
{
var saved = Localization.Culture;
var savedLog = Localization.LogCulture;
try
{
var culture = CultureInfo.GetCultureInfo(name);
Localization.Culture = culture;
Localization.LogCulture = culture;
var log = new CaptureLog();
var host = new TestHost { Name = "Test", Log = log };
host.WriteLog("Update succeeded!");
Assert.NotNull(log.Message);
Assert.StartsWith("[Test]", log.Message);
Assert.DoesNotContain("Update succeeded!", log.Message);
}
finally
{
Localization.Culture = saved;
Localization.LogCulture = savedLog;
}
}
[Fact(DisplayName = "多è¯è¨€ï¼šè‡ªå®šä¹‰ ILog å®žçŽ°åŒæ ·å…ˆç¿»è¯‘å¹¶æ ¼å¼åŒ–冿‹¼æŽ¥å‰ç¼€")]
public void WriteLog_translates_for_custom_ilog()
{
var saved = Localization.Culture;
var savedLog = Localization.LogCulture;
try
{
Localization.Culture = CultureInfo.GetCultureInfo("zh-CN");
Localization.LogCulture = CultureInfo.GetCultureInfo("zh-CN");
var log = new RawLog();
var host = new TestHost { Name = "Test", Log = log };
host.WriteLog("Slow call [{0}] took {1:n0}ms", "Api/Info", 1_234);
Assert.Equal("[Test]慢调用[Api/Info],耗时1,234ms", Assert.Single(log.Messages));
}
finally
{
Localization.Culture = saved;
Localization.LogCulture = savedLog;
}
}
[Fact(DisplayName = "多è¯è¨€ï¼šæ—¥å¿—è¯è¨€ç‹¬ç«‹äºŽç•Œé¢è¯è¨€")]
public void WriteLog_uses_log_culture()
{
var saved = Localization.Culture;
var savedLog = Localization.LogCulture;
try
{
Localization.Culture = CultureInfo.GetCultureInfo("de-DE");
Localization.LogCulture = CultureInfo.GetCultureInfo("zh-CN");
var log = new CaptureLog();
var host = new TestHost { Name = "Test", Log = log };
host.WriteLog("Update succeeded!");
Assert.Equal("[Test]æ›´æ–°æˆåŠŸï¼", log.Message);
Assert.Equal("Serveradresse ist nicht angegeben.", Localization.Translate(CoreKey));
}
finally
{
Localization.Culture = saved;
Localization.LogCulture = savedLog;
}
}
private sealed class CaptureLog : Logger
{
public String? Message { get; private set; }
protected override void OnWrite(LogLevel level, String format, params Object?[] args) => Message = Format(format, args);
}
private sealed class RawLog : ILog
{
public List<String> Messages { get; } = [];
public Boolean Enable { get; set; } = true;
public LogLevel Level { get; set; } = LogLevel.All;
public void Write(LogLevel level, String format, params Object?[] args) => Messages.Add(args.Length == 0 ? format : $"{format}|{String.Join(",", args)}");
public void Debug(String format, params Object?[] args) => Write(LogLevel.Debug, format, args);
public void Info(String format, params Object?[] args) => Write(LogLevel.Info, format, args);
public void Warn(String format, params Object?[] args) => Write(LogLevel.Warn, format, args);
public void Error(String format, params Object?[] args) => Write(LogLevel.Error, format, args);
public void Fatal(String format, params Object?[] args) => Write(LogLevel.Fatal, format, args);
}
private sealed class TestHost : ApiHost { }
private sealed class TestClient : ClientBase { }
#endregion
#region 请求è¯è¨€
[Fact(DisplayName = "多è¯è¨€ï¼šAccept-Language 按 q æƒé‡é€‰æ‹©è¯è¨€")]
public void RequestCulture_parse_quality()
{
Assert.Equal("zh-CN", RequestCultureScope.ParseAcceptLanguage("fr-FR;q=0.5, zh-CN;q=0.9, en;q=0.8")!.Name);
Assert.Equal("de-DE", RequestCultureScope.ParseAcceptLanguage("de-DE")!.Name);
Assert.Null(RequestCultureScope.ParseAcceptLanguage(""));
Assert.Null(RequestCultureScope.ParseAcceptLanguage("invalid-!!"));
}
[Fact(DisplayName = "多è¯è¨€ï¼šæŒ‰ Accept-Language 切æ¢è¯·æ±‚è¯è¨€å¹¶åœ¨é‡Šæ”¾åŽè¿˜åŽŸ")]
public void RequestCulture_scope_switch_and_restore()
{
var saved = Localization.Culture;
var savedLog = Localization.LogCulture;
var savedUI = CultureInfo.CurrentUICulture;
try
{
Localization.Culture = null;
Localization.LogCulture = null;
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en-US");
using (RequestCultureScope.Create("fr-FR;q=0.5, zh-CN;q=0.9, en;q=0.8"))
{
Assert.Equal("zh-CN", CultureInfo.CurrentUICulture.Name);
Assert.Equal("未指定æœåŠ¡ç«¯åœ°å€ã€‚", Localization.Translate(CoreKey));
}
Assert.Equal("en-US", CultureInfo.CurrentUICulture.Name);
}
finally
{
Localization.Culture = saved;
Localization.LogCulture = savedLog;
CultureInfo.CurrentUICulture = savedUI;
}
}
[Fact(DisplayName = "多è¯è¨€ï¼šå…¨å±€å›ºå®š Culture 时忽略请求è¯è¨€")]
public void RequestCulture_scope_ignores_fixed_culture()
{
var saved = Localization.Culture;
var savedUI = CultureInfo.CurrentUICulture;
try
{
Localization.Culture = CultureInfo.GetCultureInfo("en-US");
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en-US");
using (RequestCultureScope.Create("zh-CN"))
{
Assert.Equal("en-US", CultureInfo.CurrentUICulture.Name);
Assert.Equal(CoreKey, Localization.Translate(CoreKey));
}
}
finally
{
Localization.Culture = saved;
CultureInfo.CurrentUICulture = savedUI;
}
}
[Fact(DisplayName = "多è¯è¨€ï¼šApiFilter 按请求头切æ¢è¯è¨€å¹¶åœ¨æ‰§è¡ŒåŽè¿˜åŽŸ")]
public void ApiFilter_applies_request_culture()
{
var saved = Localization.Culture;
var savedUI = CultureInfo.CurrentUICulture;
try
{
Localization.Culture = null;
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en-US");
var http = new DefaultHttpContext();
http.Request.Headers["Accept-Language"] = "de-DE";
var filter = new ApiFilterAttribute();
filter.OnActionExecuting(CreateExecutingContext(http));
try
{
Assert.Equal("de-DE", CultureInfo.CurrentUICulture.Name);
Assert.Equal("Serveradresse ist nicht angegeben.", Localization.Translate(CoreKey));
}
finally
{
filter.OnActionExecuted(CreateExecutedContext(http));
}
Assert.Equal("en-US", CultureInfo.CurrentUICulture.Name);
}
finally
{
Localization.Culture = saved;
CultureInfo.CurrentUICulture = savedUI;
}
}
[Fact(DisplayName = "多è¯è¨€ï¼šå®¿ä¸»å·²é…ç½® RequestLocalization æ—¶ ApiFilter ä¸è¦†ç›–宿主è¯è¨€")]
public void ApiFilter_defers_to_host_request_culture()
{
var saved = Localization.Culture;
var savedUI = CultureInfo.CurrentUICulture;
try
{
Localization.Culture = null;
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("fr-FR");
var http = new DefaultHttpContext();
http.Request.Headers["Accept-Language"] = "de-DE";
http.Features.Set<IRequestCultureFeature>(new RequestCultureFeature(new RequestCulture(CultureInfo.GetCultureInfo("fr-FR")), null));
var filter = new ApiFilterAttribute();
filter.OnActionExecuting(CreateExecutingContext(http));
try
{
Assert.Equal("fr-FR", CultureInfo.CurrentUICulture.Name);
Assert.Equal("Échec de l'authentification", Localization.Translate(ExtKey));
}
finally
{
filter.OnActionExecuted(CreateExecutedContext(http));
}
Assert.Equal("fr-FR", CultureInfo.CurrentUICulture.Name);
}
finally
{
Localization.Culture = saved;
CultureInfo.CurrentUICulture = savedUI;
}
}
private static ActionExecutingContext CreateExecutingContext(HttpContext http)
{
var actionContext = new ActionContext(http, new RouteData(), new ControllerActionDescriptor());
return new ActionExecutingContext(actionContext, [], new Dictionary<String, Object?>(), new Object());
}
private static ActionExecutedContext CreateExecutedContext(HttpContext http)
{
var actionContext = new ActionContext(http, new RouteData(), new ControllerActionDescriptor());
return new ActionExecutedContext(actionContext, [], new Object());
}
#endregion
}
|