最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

.NET??i18n?原理、實(shí)現(xiàn)一個(gè)?i18n?框架的過程詳解

 更新時(shí)間:2026年04月08日 08:49:49   作者:癡者工良  
本文主要介紹了如何實(shí)現(xiàn)一個(gè)多語言框架,包括從JSON文件讀取多語言資源、使用ASP.NET Core的多語言功能、實(shí)現(xiàn)IRequestCultureProvider接口、設(shè)計(jì)并實(shí)現(xiàn)多語言框架的抽象接口、編寫單元測試和性能測試,以及基于Redis的動(dòng)態(tài)多語言實(shí)現(xiàn),感興趣的朋友跟隨小編一起看看吧

隨著業(yè)務(wù)的國際化,為了滿足不同客戶群體的需要,軟件產(chǎn)品需要支持多種語言,根據(jù)用戶選擇的語言呈現(xiàn)不同語言的界面。ASP.NET Core 或 ABP 等框架都提供了多語言解決方案,盡管配置方法各不相同,但都是通過鍵值對(duì)的方式使用的,開發(fā)者需要為每個(gè) key 提供對(duì)應(yīng)語言的值,框架會(huì)根據(jù)請(qǐng)求上下文自動(dòng)區(qū)配 key 在對(duì)應(yīng)語言下的值。

在本章中,筆者將基于 ASP.NET Core,簡述如何實(shí)現(xiàn)一個(gè) i18n 多語言框架,Maomi.I18n 支持在控制臺(tái)、ASP.NET Core、WPF 等項(xiàng)目下使用,支持自定義多語言資源。

什么是 i18n?

i18n 是 "internationalization"(國際化)的縮寫。之所以寫成 i18n,是因?yàn)槭鬃帜?nbsp;i 和末字母 n 之間有 18 個(gè)字母。

簡單理解:i18n = 國際化,指的是讓應(yīng)用程序能夠適應(yīng)不同語言和地區(qū)的技術(shù)。

類似的縮寫還有:L10n (localization,本地化,L 和 n 之間有 10 個(gè)字母)

i18n vs L10n 的區(qū)別

概念英文含義示例
國際化i18n開發(fā)時(shí)讓程序支持多語言的能力使用資源文件、支持 Unicode、處理不同日期格式
本地化L10n針對(duì)特定語言/地區(qū)進(jìn)行適配翻譯文本、使用本地貨幣符號(hào)、適配當(dāng)?shù)厝掌诟袷?/td>

類比理解

  • i18n = 手機(jī)充電口設(shè)計(jì)成 Type-C(通用接口)
  • L10n = 給手機(jī)配一個(gè)當(dāng)?shù)匾?guī)格的充電頭

體驗(yàn) Maomi.I18n

Maomi.I18n 是本章完成學(xué)習(xí)后的代碼成果, 在編寫多語言框架之前,先來學(xué)習(xí) Maomi.I18n 的用法。

控制臺(tái)示例

如果要使用 json 文件存儲(chǔ)語言資源包,那么需要根據(jù)項(xiàng)目名稱創(chuàng)建目錄,接下來,我們創(chuàng)建示例項(xiàng)目演示這個(gè)過程。
創(chuàng)建一個(gè) Demo5.Lib 項(xiàng)目,引入 Maomi.I18n 框架。

在 Demo5.Lib 和 Console 兩個(gè)項(xiàng)目中都添加 i18n 目錄和對(duì)應(yīng)的多語言資源文件。

在 i18n/Demo5.Lib 目錄中創(chuàng)建 en-US.json、zh-CN.json 文件。

兩個(gè)文件的內(nèi)容都設(shè)置為:

{
  "test": "lib"
}

右鍵修改 en-US.json、zh-CN.json 屬性,設(shè)置生成操作為內(nèi)容、始終復(fù)制到輸出目錄。

然后創(chuàng)建一個(gè) Demo5.Console 項(xiàng)目,引用 Demo5.Lib。

在 i18n/Demo5.Console 目錄中創(chuàng)建 en-US.json、zh-CN.json 文件。

兩個(gè)文件都設(shè)置內(nèi)容為:

{
  "test": "console"
}

最后得到的目錄結(jié)構(gòu)如下:

├─Demo5.Console
│  │  Demo5.Console.csproj
│  │  Program.cs
│  │
│  ├─i18n
│  │  └─Demo5.Console
│  │          en-US.json
│  │          zh-CN.json
│
├─Demo5.Lib
│  │  Demo5.Lib.csproj
│  │  Extensions.cs
│  │
│  ├─i18n
│  │  └─Demo5.Lib
│  │          en-US.json
│  │          zh-CN.json

在 Demo5.Console 的 Program 中使用 IStringLocalizer<T> 來獲取 key 在不同語言下的值。

var ioc = new ServiceCollection();
ioc.AddI18n("zh-CN");
ioc.AddI18nResource(options =>
{
	options.ParseDirectory("i18n");
});
ioc.AddLib();
var services = ioc.BuildServiceProvider();
// 手動(dòng)設(shè)置當(dāng)前請(qǐng)求語言
using (var c = new I18nScope("en-US"))
{
	var l1 = services.GetRequiredService<IStringLocalizer<Program>>();
	var l2 = services.GetRequiredService<IStringLocalizer<Test>>();
	var s1 = l1["test"];
	var s2 = l2["test"];
	Console.WriteLine(s1);
	Console.WriteLine(s2);
}

編譯 Demo5.Console ,打開 bin/Debug/net8.0 目錄,在 i18n 目錄下可以看到如下文件結(jié)構(gòu):

.
├── Demo5.Console
│   ├── en-US.json
│   └── zh-CN.json
└── Demo5.Lib
    ├── en-US.json
    └── zh-CN.json

Maomi.I18n 的原理很簡單,每個(gè)項(xiàng)目都設(shè)置多語言文件,編譯后所有文件都會(huì)合并到 i18n 目錄中統(tǒng)一管理和加載,每個(gè)目錄都與項(xiàng)目名稱一致,便于區(qū)分。使用 IStringLocalizer<T> 讀取 key 時(shí),會(huì)自動(dòng)從 T 類型所在的項(xiàng)目名稱目錄下加載 json 文件。

單個(gè)項(xiàng)目管理

在上一個(gè)例子中,每個(gè)項(xiàng)目都有自己的多語言資源文件,當(dāng)然我們?cè)谡麄€(gè)解決方案中共用一個(gè)語言文件,不需要按照目錄劃分。

收到導(dǎo)入多語言資源文件:

ioc.AddI18nResource(options =>
{
	options.AddJsonFile("zh-CN", "i18n/zh-CN.json");
	options.AddJsonFile("en-US", "i18n/en-US.json");
});

或者自動(dòng)掃描目錄,并自動(dòng)使用 json 文件名稱當(dāng)作多語言名稱:

ioc.AddI18nResource(options =>
{
	options.AddJsonDirectory("i18n");
});

使用時(shí)直接注入 IStringLocalizer ,不需要注入 IStringLocalizer<T>

// 手動(dòng)設(shè)置當(dāng)前請(qǐng)求語言
using (var c = new I18nScope("en-US"))
{
	var l1 = services.GetRequiredService<IStringLocalizer>();
	var l2 = services.GetRequiredService<IStringLocalizer>();
	var s1 = l1["test"];
	var s2 = l2["test"];
	Console.WriteLine(s1);
	Console.WriteLine(s2);
}

如何設(shè)置當(dāng)前語言

設(shè)置當(dāng)前上下文語言,只需要設(shè)置當(dāng)前文化即可:

CultureInfo.CurrentCulture = new CultureInfo("zh-CN");

另一種方法是跟當(dāng)前容器上下文有個(gè),開發(fā)者可以自由定義如何解析當(dāng)前程序多語言上下文。

public class MyI18nContext : I18nContext
{
    public MyI18nContext()
    {
        base.Culture = ...
    }
    public void Set(CultureInfo cultureInfo)
    {
        base.Culture = cultureInfo;
    }
}
builder.Services.AddScoped<I18nContext, MyI18nContext>();

Web 示例

創(chuàng)建一個(gè) Api 項(xiàng)目,名為 Demo5.Api ,引入 Maomi.I18n.AspNetCore

在項(xiàng)目中新建一個(gè) i18n/Demo5.Api 目錄,然后創(chuàng)建兩個(gè) json 文件。

zh-CN.json 文件內(nèi)容:

{
  "購物車": {
    "商品名稱": "商品名稱",
    "加入時(shí)間": "加入時(shí)間",
    "清理失效商品": "清理失效商品"
  },
  "會(huì)員等級(jí)": {
    "用戶名": "用戶名",
    "積分": "積分:{0}",
    "等級(jí)": "等級(jí)"
  }
}

en-US.json 文件內(nèi)容:

{
  "購物車": {
    "商品名稱": "Product name",
    "加入時(shí)間": "Join date",
    "清理失效商品": "Cleaning up failures"
  },
  "會(huì)員等級(jí)": {
    "用戶名": "Username",
    "積分": "Member points:{0}",
    "等級(jí)": "Level"
  }
}

Maomi.I18n 框架會(huì)掃描程序集目錄的 json 文件,然后解析 json 文件以鍵值對(duì)的形式存儲(chǔ)到內(nèi)存中,Key 的形式與第三章中提到的 IConfiguration 的 Key 一致,在第四章中也提到了如何解析 json 文件,比如要取得商品名稱的值,可以使用 ["購物車:商品名稱"] 這樣的形式獲取嵌套層次下的值,而且還可以使用字符串插值,如 "積分": "Member points:{0}"。

使用 Maomi.I18n ,只需要兩步,注入 i18n 服務(wù)和導(dǎo)入 i18n 語言資源。

// 添加 i18n 多語言支持
builder.Services.AddI18nAspNetCore(defaultLanguage: "zh-CN");
// 設(shè)置多語言來源-json
builder.Services.AddI18nResource(option =>
{
    var basePath = "i18n";
    option.AddJson(basePath);
});

接著,添加 i18n 中間件,中間件會(huì)將用戶請(qǐng)求時(shí)的上下文中解析出對(duì)應(yīng)的語言。

var app = builder.Build();
app.UseI18n();	// <- 放到中間件靠前的位置

然后添加控制器或直接編寫中間件進(jìn)行測試,只需要注入 IStringLocalizer 服務(wù)即可。

app.UseRouting();
app.Use(async (HttpContext context, RequestDelegate next) =>
{
	var localizer = context.RequestServices.GetRequiredService<IStringLocalizer<Program>>();
	await context.Response.WriteAsync(localizer["購物車:商品名稱"]);
	return;
});

啟動(dòng)程序,打開地址 http://localhost:5177/test?culture=en-US&ui-culture=en-US 可以觀察到輸出為 Product name。

攜帶請(qǐng)求語言信息

Maomi.I18n 本質(zhì)是基于 ASP.NET Core 的多語言接口進(jìn)行擴(kuò)展的,所以 Maomi.I18n 并不需要做太多解析語言的工作,而是依靠 ASP.NET Core 自帶的多語言功能將客戶端請(qǐng)求時(shí)解析出要使用的語言,以及相關(guān)的上下文信息。

ASP.NET Core 中可以使用 app.UseRequestLocalization(); 引入 RequestLocalizationMiddleware 中間件提供一些多語言的處理。RequestLocalizationMiddleware 中間件會(huì)自動(dòng)調(diào)用 IRequestCultureProvider 檢索請(qǐng)求所用語言,然后我們可以通過 context.Features.Get<IRequestCultureFeature>(); 來獲取到對(duì)應(yīng)的語言,簡化我們?cè)O(shè)計(jì)多語言框架的代碼。

ASP.NET Core 定義了一個(gè) IRequestCultureProvider 接口,用于解析客戶端請(qǐng)求時(shí)攜帶的的區(qū)域性信息,從當(dāng)前請(qǐng)求中解析出所用的語言,ASP.NET Core 本身有三個(gè)類型實(shí)現(xiàn)了該接口,所以相當(dāng)于自帶了三種獲取當(dāng)前請(qǐng)求語言的方式,下面我們來了解這三種方式是如何通過請(qǐng)求上下文解析語言標(biāo)識(shí)。

第一種是 URL 路由參數(shù),可以通過 QueryStringRequestCultureProvider 類型解析出來,需要 url 中攜帶兩個(gè)參數(shù) cultureui-culture,其格式示例如下:

?culture=en-US&ui-culture=en-US

第二種是 Cookie,提供器是 CookieRequestCultureProvider,cookie 中需要添加名為 .AspNetCore.Culture 的 cookie,其格式示例如下:

c=en-US|uic=en-US

示例:

.AspNetCore.Culture=c=en-US|uic=en-US

第三種是通過 Header 設(shè)置,也是最常用的設(shè)置方法,提供器是 AcceptLanguageHeaderRequestCultureProvider,其格式示例如下:

Accept-Language: zh-CN,zh;q=0.9

當(dāng)然,開發(fā)者可以根據(jù)需求,修改這三者的配置,以便使用其他請(qǐng)求位置或不同的參數(shù)名稱解析出當(dāng)前請(qǐng)求的文化名稱。

new QueryStringRequestCultureProvider()
{
	QueryStringKey = "lan",
	UIQueryStringKey = "ui"
}

由于 ASP.NET Core 會(huì)自動(dòng)解析出請(qǐng)求語言,因此我們只需要從 IRequestCultureFeature 服務(wù)中取得語言信息即可,不需要自行解析。

var requestCultureFeature = context.Features.Get<IRequestCultureFeature>();
var requestCulture = requestCultureFeature?.RequestCulture;

當(dāng)客戶端請(qǐng)求時(shí),ASP.NET Core 會(huì)自動(dòng)從 RequestLocalizationOptions 中取出 IRequestCultureProvider 服務(wù)列表,然后逐個(gè)調(diào)用,直到能夠確定用戶請(qǐng)求的語言信息。 ASP.NET Core 默認(rèn)會(huì)按順序執(zhí)行 QueryStringRequestCultureProvider、CookieRequestCultureProvider、AcceptLanguageHeaderRequestCultureProvider 三個(gè)提供器,如果前者解析找不到對(duì)應(yīng)的參數(shù),則會(huì)使用下一個(gè) IRequestCultureProvider 解析,如果默認(rèn)三個(gè)提供器都解析不出來,則會(huì)調(diào)用用戶自定義的服務(wù),如果能夠獲得結(jié)果,則不會(huì)再調(diào)用其它的提供器。當(dāng)然也可以自行修改以上組件的順序,但是這里不再贅述。

要實(shí)現(xiàn)一個(gè)IRequestCultureProvider 很簡單,比如我們要求在 url 中使用 c、uic 兩個(gè)參數(shù)攜帶多語言信息,其示例代碼如下:

// 自定義請(qǐng)求語言提供器
	// 或直接繼承 RequestCultureProvider
	public class I18nRequestCultureProvider : IRequestCultureProvider
	{
		private readonly string _defaultLanguage;
		public I18nRequestCultureProvider(string defaultLanguage)
		{
			_defaultLanguage = defaultLanguage;
		}
		private const string RouteValueKey = "c";
		private const string UIRouteValueKey = "uic";
		public override Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext  httpContext)
		{
			var request = httpContext.Request;
			if (!request.RouteValues.Any())
			{
				return NullProviderCultureResult;
			}
			string? queryCulture = null;
			string? queryUICulture = null;
			// 從路由中解析
			if (!string.IsNullOrWhiteSpace(RouteValueKey))
			{
				queryCulture = request.RouteValues[RouteValueKey]?.ToString();
			}
            // 其他過程省略
			var providerResultCulture = new ProviderCultureResult(queryCulture, queryUICulture);
			return Task.FromResult<ProviderCultureResult?>(providerResultCulture);
		}
	}

開發(fā)者需要根據(jù) HttpContext 中的請(qǐng)求參數(shù)解析出當(dāng)前請(qǐng)求使用的語言,如果解析不出來,則應(yīng)該返回 NullProviderCultureResult,框架會(huì)繼續(xù)使用下一個(gè)IRequestCultureProvider 解析請(qǐng)求語言。如果找到了請(qǐng)求語言,則需要返回 ProviderCultureResult。

要注意的是,IRequestCultureProvider 接口的服務(wù)是不能通過容器注入的,而是在 RequestLocalizationOptions 中配置。

			services.Configure<RequestLocalizationOptions>(options =>
			{
				// 默認(rèn)自帶了三個(gè)請(qǐng)求語言提供器,會(huì)先從這些提供器識(shí)別要使用的語言。
				// QueryStringRequestCultureProvider
				// CookieRequestCultureProvider
				// AcceptLanguageHeaderRequestCultureProvider
				// 自定義請(qǐng)求請(qǐng)求語言提供器
				options.RequestCultureProviders.Add(new I18nRequestCultureProvider(defaultLanguage));
			});

如果你想調(diào)整提供器的順序,只需要修改 options.RequestCultureProviders 中的 IRequestCultureProvider 集合即可。

實(shí)現(xiàn) i18n 框架

在本節(jié)中,將會(huì)介紹如何設(shè)計(jì)和編寫一個(gè) i18n 框架,框架的全部代碼如下所示。

部分文件說明如下:

// 當(dāng)前程序多語言上下文
I18nContext.cs
// 多語言資源接口定義
I18nResource.cs
// i18n 語言資源工廠
I18nResourceFactory.cs
// 設(shè)置當(dāng)前語言作用域
I18nScope.cs
// 服務(wù)注入擴(kuò)展
I18nExtensions.cs
// 從 json 文件讀取多語言資源擴(kuò)展
JsonResourceExtensions.cs
// 實(shí)現(xiàn) I18nResourceFactory
InternalI18nResourceFactory.cs
// 自定義請(qǐng)求語言解析
I18nRequestCultureProvider.cs
// 實(shí)現(xiàn) IStringLocalizer 接口
I18nStringLocalizer.cs
// 實(shí)現(xiàn) IStringLocalizer<T> 接口
I18nStringLocalizer`.cs
// 實(shí)現(xiàn) I18nResource,通過 json 文件導(dǎo)入語言資源
JsonResource.cs
// 解析 json 的幫助類
ReadJsonHelper.cs

抽象接口

設(shè)計(jì)多語言框架,首先將框架劃分為三個(gè)角色,即使用者、框架自身、多語言提供者,使用者通過抽象接口獲取 key 對(duì)應(yīng)語言的值,多語言提供者通過抽象接口提供多語言鍵值對(duì)數(shù)據(jù)。所以,抽象接口主要是面向使用者和多語言提供者設(shè)計(jì),框架自身則是為使用者和提供者架設(shè)一個(gè)橋梁,此外還需要定義一些上下文類、模型類,以便傳遞信息。

首先思考以何種方式保存多語言資源數(shù)據(jù),比如說嵌入程序集、在項(xiàng)目中攜帶 json 文件、存儲(chǔ)在 redis 中等,i18n 框架不需要關(guān)心多語言存儲(chǔ)在哪里,只需要通過接口加載出來即可。

定義一個(gè) I18nResource 接口,i18n 框架通過該接口加載多語言數(shù)據(jù)。

/// <summary>
/// i18n 語言資源.
/// </summary>
/// <remarks>每個(gè) I18nResource 對(duì)應(yīng)一種語言的一個(gè)資源文件.</remarks>
public interface I18nResource
{
    /// <summary>
    /// 該資源提供的語言.
    /// </summary>
    CultureInfo SupportedCulture { get; }
    /// <summary>
    /// 獲取具有給定名稱的字符串資源.
    /// </summary>
    /// <param name="culture">語言名字.</param>
    /// <param name="name">字符串名稱.</param>
    /// <returns><see cref="LocalizedString"/>.</returns>
    LocalizedString Get(string culture, string name);
    /// <summary>
    /// 獲取具有給定名稱的字符串資源.
    /// </summary>
    /// <param name="culture">語言名字.</param>
    /// <param name="name">字符串名稱.</param>
    /// <param name="arguments">字符串插值參數(shù).</param>
    /// <returns><see cref="LocalizedString"/>.</returns>
    LocalizedString Get(string culture, string name, params object[] arguments);
    /// <summary>
    /// 從 i18n 資源文件中獲取所有字符串.
    /// </summary>
    /// <param name="includeParentCultures"></param>
    /// <returns><see cref="LocalizedString"/>.</returns>
    public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures);
}
/// <summary>
/// i18n 語言資源.
/// </summary>
/// <remarks>每個(gè) I18nResource 對(duì)應(yīng)一種語言的一個(gè)資源文件.</remarks>
/// <typeparam name="T">類型.</typeparam>
public interface I18nResource<T> : I18nResource
{
}

然后創(chuàng)建多語言管理工廠,管理 I18nResource 列表,支持從容器中取出多語言資源文件。

/// <summary>
/// I18n 資源工廠.
/// </summary>
public interface I18nResourceFactory
{
    /// <summary>
    /// 當(dāng)前支持的語言.
    /// </summary>
    IList<CultureInfo> SupportedCultures { get; }
    /// <summary>
    /// 所有資源提供器.
    /// </summary>
    IList<I18nResource> Resources { get; }
    /// <summary>
    /// 在容器中的資源服務(wù).
    /// </summary>
    IList<Type> ServiceResources { get; }
    /// <summary>
    /// 添加 i18n 語言資源,該類型將會(huì)被從容器中取出.
    /// </summary>
    /// <param name="resourceType">i18n 語言資源.</param>
    /// <returns><see cref="I18nResourceFactory"/>.</returns>
    I18nResourceFactory AddServiceType(Type resourceType);
    /// <summary>
    /// 添加 i18n 語言資源.
    /// </summary>
    /// <param name="resource">i18n 語言資源.</param>
    /// <returns><see cref="I18nResourceFactory"/>.</returns>
    I18nResourceFactory Add(I18nResource resource);
    /// <summary>
    /// 添加 i18n 語言資源.
    /// </summary>
    /// <typeparam name="T">類型.</typeparam>
    /// <param name="resource">i18n 語言資源.</param>
    /// <returns><see cref="I18nResourceFactory"/>.</returns>
    I18nResourceFactory Add<T>(I18nResource<T> resource);
}

接下來是設(shè)計(jì)使用者接口的抽象。

ASP.NET Core 通過 IRequestCultureProvider 檢索出來的當(dāng)前請(qǐng)求語言,為了簡化解析語言標(biāo)識(shí)的代碼,定義一個(gè)I18nContext 類型,用來存儲(chǔ)從請(qǐng)求上下文中解析處理的多語言標(biāo)識(shí),i18n 框架中下游服務(wù)可以通過 I18nContext 獲取當(dāng)前請(qǐng)求語言。

// 記錄當(dāng)前請(qǐng)求的 i18n 信息
public class I18nContext
{
	// 當(dāng)前用戶請(qǐng)求的語言
	public CultureInfo Culture { get; internal set; } = CultureInfo.CurrentCulture;
}

IStringLocalizer、IStringLocalizer<T> 是 ASP.NET Core 中多語言服務(wù)的接口,使用者可以從這兩個(gè)接口中查詢多語言字符串,我們實(shí)現(xiàn)兩個(gè)對(duì)應(yīng)的服務(wù),從 I18nResource 集合中查找出對(duì)應(yīng)字符串的值。

/// <summary>
/// 表示提供本地化字符串的服務(wù).
/// </summary>
public class I18nStringLocalizer : IStringLocalizer
{
    private readonly IServiceProvider _serviceProvider;
    private readonly I18nContext _context;
    private readonly I18nResourceFactory _resourceFactory;
    /// <summary>
    /// Initializes a new instance of the <see cref="I18nStringLocalizer"/> class.
    /// </summary>
    /// <param name="context"></param>
    /// <param name="resourceFactory"></param>
    /// <param name="serviceProvider"></param>
    public I18nStringLocalizer(I18nContext context, I18nResourceFactory resourceFactory, IServiceProvider serviceProvider)
    {
        _context = context;
        _resourceFactory = resourceFactory;
        _serviceProvider = serviceProvider;
    }
    /// <inheritdoc/>
    public LocalizedString this[string name] => Find(name);
    /// <inheritdoc/>
    public LocalizedString this[string name, params object[] arguments] => Find(name, arguments);
    /// <inheritdoc/>
    public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
    {
        foreach (var serviceType in _resourceFactory.ServiceResources)
        {
            var resource = _serviceProvider.GetRequiredService(serviceType) as I18nResource;
            if (resource == null)
            {
                continue;
            }
            foreach (var item in resource.GetAllStrings(includeParentCultures))
            {
                yield return item;
            }
        }
        foreach (var resource in _resourceFactory.Resources)
        {
            foreach (var item in resource.GetAllStrings(includeParentCultures))
            {
                yield return item;
            }
        }
    }
    private LocalizedString Find(string name)
    {
        foreach (var serviceType in _resourceFactory.ServiceResources)
        {
            var resource = _serviceProvider.GetRequiredService(serviceType) as I18nResource;
            if (resource == null)
            {
                continue;
            }
            if (_context.Culture.Name != resource.SupportedCulture.Name)
            {
                continue;
            }
            var result = resource.Get(_context.Culture.Name, name);
            if (result == null || result.ResourceNotFound)
            {
                continue;
            }
            return result;
        }
        foreach (var resource in _resourceFactory.Resources)
        {
            if (_context.Culture.Name != resource.SupportedCulture.Name)
            {
                continue;
            }
            var result = resource.Get(_context.Culture.Name, name);
            if (result == null || result.ResourceNotFound)
            {
                continue;
            }
            return result;
        }
        // 所有的資源都查找不到時(shí),使用默認(rèn)值
        return new LocalizedString(name, name);
    }
    private LocalizedString Find(string name, params object[] arguments)
    {
        foreach (var serviceType in _resourceFactory.ServiceResources)
        {
            var resource = _serviceProvider.GetRequiredService(serviceType) as I18nResource;
            if (resource == null)
            {
                continue;
            }
            if (_context.Culture.Name != resource.SupportedCulture.Name)
            {
                continue;
            }
            var result = resource.Get(_context.Culture.Name, name, arguments);
            if (result == null || result.ResourceNotFound)
            {
                continue;
            }
            return result;
        }
        foreach (var resource in _resourceFactory.Resources)
        {
            if (_context.Culture.Name != resource.SupportedCulture.Name)
            {
                continue;
            }
            var result = resource.Get(_context.Culture.Name, name, arguments);
            if (result == null || result.ResourceNotFound)
            {
                continue;
            }
            return result;
        }
        // 所有的資源都查找不到時(shí),使用默認(rèn)值
        return new LocalizedString(name, string.Format(name, arguments));
    }
}
/// <summary>
/// 表示提供本地化字符串的服務(wù).
/// </summary>
/// <typeparam name="T">類型.</typeparam>
public class I18nStringLocalizer<T> : IStringLocalizer<T>
{
    private readonly IServiceProvider _serviceProvider;
    private readonly I18nContext _context;
    private readonly I18nResourceFactory _resourceFactory;
    /// <summary>
    /// Initializes a new instance of the <see cref="I18nStringLocalizer{T}"/> class.
    /// </summary>
    /// <param name="context"></param>
    /// <param name="resourceFactory"></param>
    /// <param name="serviceProvider"></param>
    public I18nStringLocalizer(I18nContext context, I18nResourceFactory resourceFactory, IServiceProvider serviceProvider)
    {
        _context = context;
        _resourceFactory = resourceFactory;
        _serviceProvider = serviceProvider;
    }
    /// <inheritdoc/>
    public LocalizedString this[string name] => Find(name);
    /// <inheritdoc/>
    public LocalizedString this[string name, params object[] arguments] => Find(name, arguments);
    /// <inheritdoc/>
    public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
    {
        foreach (var serviceType in _resourceFactory.ServiceResources)
        {
            var resource = _serviceProvider.GetRequiredService(serviceType) as I18nResource;
            if (resource == null)
            {
                continue;
            }
            foreach (var item in resource.GetAllStrings(includeParentCultures))
            {
                yield return item;
            }
        }
        foreach (var resource in _resourceFactory.Resources)
        {
            foreach (var item in resource.GetAllStrings(includeParentCultures))
            {
                yield return item;
            }
        }
    }
    private LocalizedString Find(string name)
    {
        var resourceType = typeof(I18nResource<T>);
        foreach (var serviceType in _resourceFactory.ServiceResources)
        {
            if (!serviceType.IsGenericType && serviceType.GenericTypeArguments[0].Assembly != typeof(T).Assembly)
            {
                continue;
            }
            var resource = _serviceProvider.GetRequiredService(serviceType) as I18nResource;
            if (resource == null)
            {
                continue;
            }
            if (_context.Culture.Name != resource.SupportedCulture.Name)
            {
                continue;
            }
            var result = resource.Get(_context.Culture.Name, name);
            if (result == null || result.ResourceNotFound)
            {
                continue;
            }
            return result;
        }
        foreach (var resource in _resourceFactory.Resources)
        {
            if (_context.Culture.Name != resource.SupportedCulture.Name)
            {
                continue;
            }
            // I18nResource<T>
            if (!resource.GetType().IsGenericType || resource.GetType().GenericTypeArguments[0].Assembly != typeof(T).Assembly)
            {
                continue;
            }
            var result = resource.Get(_context.Culture.Name, name);
            if (result == null || result.ResourceNotFound)
            {
                continue;
            }
            return result;
        }
        // 所有的資源都查找不到時(shí),使用默認(rèn)值
        return new LocalizedString(name, name);
    }
    private LocalizedString Find(string name, params object[] arguments)
    {
        var resourceType = typeof(I18nResource<T>);
        foreach (var serviceType in _resourceFactory.ServiceResources)
        {
            if (!serviceType.IsGenericType && serviceType.GenericTypeArguments[0].Assembly != typeof(T).Assembly)
            {
                continue;
            }
            var resource = _serviceProvider.GetRequiredService(serviceType) as I18nResource;
            if (resource == null)
            {
                continue;
            }
            if (_context.Culture.Name != resource.SupportedCulture.Name)
            {
                continue;
            }
            var result = resource.Get(_context.Culture.Name, name, arguments);
            if (result == null || result.ResourceNotFound)
            {
                continue;
            }
            return result;
        }
        foreach (var resource in _resourceFactory.Resources)
        {
            if (_context.Culture.Name != resource.SupportedCulture.Name)
            {
                continue;
            }
            // I18nResource<T>
            if (!resource.GetType().IsGenericType || resource.GetType().GenericTypeArguments[0].Assembly != typeof(T).Assembly)
            {
                continue;
            }
            var result = resource.Get(_context.Culture.Name, name, arguments);
            if (result == null || result.ResourceNotFound)
            {
                continue;
            }
            return result;
        }
        // 所有的資源都查找不到時(shí),使用默認(rèn)值
        return new LocalizedString(name, string.Format(name, arguments));
    }
}

CultureInfoScope 的作用很簡單,在其作用域之內(nèi)修改 CultureInfo.CurrentCulture 的值。

/// <summary>
/// i18n 作用域.
/// </summary>
public class I18nScope : IDisposable
{
    private readonly CultureInfo _defaultCultureInfo;
    /// <summary>
    /// Initializes a new instance of the <see cref="I18nScope"/> class.
    /// </summary>
    /// <param name="language"></param>
    public I18nScope(string language)
    {
        _defaultCultureInfo = CultureInfo.CurrentCulture;
        CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture(language);
    }
    /// <inheritdoc/>
    public void Dispose()
    {
        CultureInfo.CurrentCulture = _defaultCultureInfo;
    }
}

至此,我們已經(jīng)設(shè)計(jì)好 i18n 框架的抽象了,接下來我們會(huì)進(jìn)一步實(shí)現(xiàn) i18n 框架。

實(shí)現(xiàn)從 json 讀取語言資源

Maomi.I18n 本身實(shí)現(xiàn)了一個(gè)從 json 文件讀取多語言資源包的 I18nResource 服務(wù),而無論從哪里讀取的多語言資源,大部分都可以以 kv 的形式存儲(chǔ)到內(nèi)存中,因此,講解開發(fā)者如何實(shí)現(xiàn)一個(gè) DictionaryResource 服務(wù)存儲(chǔ)從不同地方讀取到的多語言資源內(nèi)容。

為了保證每個(gè)項(xiàng)目都可以攜帶自己的語言信息,我們可以要求項(xiàng)目下面創(chuàng)建 i18n 目錄,然后創(chuàng)建與當(dāng)前項(xiàng)目同名的子目錄,在子目錄下存儲(chǔ)自己的語言文件。

這樣做的好處時(shí),當(dāng)編譯項(xiàng)目時(shí),主項(xiàng)目下的 i18n 會(huì)收集到所有項(xiàng)目的語言文件,而且不會(huì)發(fā)生沖突。而且當(dāng)我們使用 nuget 打包項(xiàng)目時(shí),nuget 包還會(huì)攜帶這些文件,使用這個(gè)拉取 nuget 包后也可以使用到這些多語言文件。

/// <summary>
/// 字典存儲(chǔ)多語言文件資源.
/// </summary>
public class DictionaryResource : I18nResource
{
    /// <inheritdoc/>
    public CultureInfo SupportedCulture => _cultureInfo;
    private readonly CultureInfo _cultureInfo;
    private readonly IReadOnlyDictionary<string, LocalizedString> _kvs;
    /// <summary>
    /// Initializes a new instance of the <see cref="DictionaryResource"/> class.
    /// </summary>
    /// <param name="cultureInfo"></param>
    /// <param name="kvs"></param>
    public DictionaryResource(CultureInfo cultureInfo, IReadOnlyDictionary<string, object> kvs)
    {
        _cultureInfo = cultureInfo;
        _kvs = kvs.ToDictionary(x => x.Key, x => new LocalizedString(x.Key, x.Value.ToString()!));
    }
    /// <inheritdoc/>
    public virtual LocalizedString Get(string culture, string name)
    {
        if (culture != _cultureInfo.Name)
        {
            return new LocalizedString(name, name, resourceNotFound: true);
        }
        var value = _kvs.GetValueOrDefault(name);
        if (value == null)
        {
            return new LocalizedString(name, name, resourceNotFound: true);
        }
        return value;
    }
    /// <inheritdoc/>
    public virtual LocalizedString Get(string culture, string name, params object[] arguments)
    {
        if (culture != _cultureInfo.Name)
        {
            return new LocalizedString(name, name, resourceNotFound: true);
        }
        var value = _kvs.GetValueOrDefault(name);
        if (value == null)
        {
            return new LocalizedString(name, name, resourceNotFound: true);
        }
        return new LocalizedString(name, string.Format(value, arguments));
    }
    /// <inheritdoc/>
    public virtual IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
    {
        return _kvs.Values;
    }
}
/// <summary>
/// 字典存儲(chǔ)多語言文件資源.
/// </summary>
/// <typeparam name="TResource">類型.</typeparam>
public class DictionaryResource<TResource> : DictionaryResource, I18nResource<TResource>
{
    private readonly Assembly _assembly;
    /// <summary>
    /// Initializes a new instance of the <see cref="DictionaryResource{TResource}"/> class.
    /// </summary>
    /// <param name="cultureInfo"></param>
    /// <param name="kvs"></param>
    /// <param name="assembly"></param>
    public DictionaryResource(CultureInfo cultureInfo, IReadOnlyDictionary<string, object> kvs, Assembly assembly)
        : base(cultureInfo, kvs)
    {
        _assembly = assembly;
    }
    /// <inheritdoc/>
    public override LocalizedString Get(string culture, string name)
    {
        if (typeof(TResource).Assembly != _assembly)
        {
            return new LocalizedString(name, name, resourceNotFound: true);
        }
        return base.Get(culture, name);
    }
    /// <inheritdoc/>
    public override LocalizedString Get(string culture, string name, params object[] arguments)
    {
        if (typeof(TResource).Assembly != _assembly)
        {
            return new LocalizedString(name, name, resourceNotFound: true);
        }
        return base.Get(culture, name, arguments);
    }
}

編寫擴(kuò)展方法注入 json 語言資源,該擴(kuò)展方法只會(huì)加載與 T 類型所在程序集的相同命名目錄下的 json 文件。

/// <summary>
/// Json 多語言文件資源.
/// </summary>
public static class JsonResourceExtensions
{
    /// <summary>
    /// 掃描目錄下的所有子目錄,自動(dòng)區(qū)配對(duì)應(yīng)的項(xiàng)目/程序集下,json 文件名稱會(huì)被動(dòng)作語言名稱.
    /// </summary>
    /// <param name="resourceFactory"></param>
    /// <param name="basePath"></param>
    /// <returns><see cref="I18nResourceFactory"/>.</returns>
    public static I18nResourceFactory ParseDirectory(
        this I18nResourceFactory resourceFactory,
        string basePath)
    {
        var basePathDirectoryInfo = new DirectoryInfo(basePath);
        Queue<DirectoryInfo> directoryInfos = new Queue<DirectoryInfo>();
        var assemblies = AppDomain.CurrentDomain.GetAssemblies();
        var basePathFullName = basePathDirectoryInfo.FullName;
        directoryInfos.Enqueue(basePathDirectoryInfo);
        while (directoryInfos.Count > 0)
        {
            var curDirectory = directoryInfos.Dequeue();
            var lanDir = curDirectory.GetDirectories();
            foreach (var lan in lanDir)
            {
                directoryInfos.Enqueue(lan);
            }
            var files = curDirectory.GetFiles().Where(x => x.Name.EndsWith(".json")).ToArray();
            if (files.Length == 0)
            {
                continue;
            }
            // 移除路徑的前部分
            var curPath = curDirectory.FullName[basePathFullName.Length..].Trim('/', '\\');
            var assembly = assemblies.FirstOrDefault(x => string.Equals(curPath, x.GetName().Name, StringComparison.CurrentCultureIgnoreCase));
            if (assembly == null)
            {
                continue;
            }
            foreach (var file in files)
            {
                var language = Path.GetFileNameWithoutExtension(file.Name);
                var text = File.ReadAllText(file.FullName);
                var dic = ReadJsonHelper.Read(new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(text)), new JsonReaderOptions { AllowTrailingCommas = true });
                DictionaryResource jsonResource = (Activator.CreateInstance(
                    typeof(DictionaryResource<>).MakeGenericType(assembly.GetTypes()[0]),
                    new object[] { new CultureInfo(language), dic, assembly }) as DictionaryResource)!;
                resourceFactory.Add(jsonResource);
            }
        }
        return resourceFactory;
    }
    /// <summary>
    /// 添加 json 文件資源,json 文件名稱會(huì)被當(dāng)作語言名稱.
    /// </summary>
    /// <param name="resourceFactory"></param>
    /// <param name="basePath">基礎(chǔ)路徑.</param>
    /// <returns><see cref="I18nResourceFactory"/>.</returns>
    public static I18nResourceFactory AddJsonDirectory(
        this I18nResourceFactory resourceFactory,
        string basePath)
    {
        var rootDir = new DirectoryInfo(basePath);
        var files = rootDir.GetFiles().Where(x => x.Name.EndsWith(".json"));
        foreach (var file in files)
        {
            var language = Path.GetFileNameWithoutExtension(file.Name);
            var text = File.ReadAllText(file.FullName);
            var dic = ReadJsonHelper.Read(new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(text)), new JsonReaderOptions { AllowTrailingCommas = true });
            DictionaryResource jsonResource = new DictionaryResource(new CultureInfo(language), dic);
            resourceFactory.Add(jsonResource);
        }
        return resourceFactory;
    }
    /// <summary>
    /// 添加 json 文件資源,將目錄下的所有 json 文件都?xì)w類到此程序集下,json 文件名稱會(huì)被當(dāng)作語言名稱.
    /// </summary>
    /// <typeparam name="T">類型.</typeparam>
    /// <param name="resourceFactory"></param>
    /// <param name="basePath">基礎(chǔ)路徑.</param>
    /// <returns><see cref="I18nResourceFactory"/>.</returns>
    public static I18nResourceFactory AddJsonDirectory<T>(
        this I18nResourceFactory resourceFactory,
        string basePath)
        where T : class
    {
        var rootDir = new DirectoryInfo(basePath);
        var files = rootDir.GetFiles().Where(x => x.Name.EndsWith(".json"));
        foreach (var file in files)
        {
            var language = Path.GetFileNameWithoutExtension(file.Name);
            var text = File.ReadAllText(file.FullName);
            var dic = ReadJsonHelper.Read(new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(text)), new JsonReaderOptions { AllowTrailingCommas = true });
            DictionaryResource<T> jsonResource = new DictionaryResource<T>(new CultureInfo(language), dic, typeof(T).Assembly);
            resourceFactory.Add(jsonResource);
        }
        return resourceFactory;
    }
    /// <summary>
    /// 添加 json 文件資源.
    /// </summary>
    /// <param name="resourceFactory"></param>
    /// <param name="language">語言.</param>
    /// <param name="jsonFile">json 文件路徑.</param>
    /// <returns><see cref="I18nResourceFactory"/>.</returns>
    public static I18nResourceFactory AddJsonFile(this I18nResourceFactory resourceFactory, string language, string jsonFile)
    {
        string s = File.ReadAllText(jsonFile);
        Dictionary<string, object> kvs = ReadJsonHelper.Read(new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(s)), new JsonReaderOptions
        {
            AllowTrailingCommas = true
        });
        DictionaryResource resource = new DictionaryResource(new CultureInfo(language), kvs);
        resourceFactory.Add(resource);
        return resourceFactory;
    }
    /// <summary>
    /// 添加 json 文件資源.
    /// </summary>
    /// <typeparam name="T">類型.</typeparam>
    /// <param name="resourceFactory"></param>
    /// <param name="language">語言.</param>
    /// <param name="jsonFile">json 文件路徑.</param>
    /// <returns><see cref="I18nResourceFactory"/>.</returns>
    public static I18nResourceFactory AddJsonFile<T>(this I18nResourceFactory resourceFactory, string language, string jsonFile)
        where T : class
    {
        string s = File.ReadAllText(jsonFile);
        Dictionary<string, object> kvs = ReadJsonHelper.Read(new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(s)), new JsonReaderOptions
        {
            AllowTrailingCommas = true
        });
        DictionaryResource<T> resource = new DictionaryResource<T>(new CultureInfo(language), kvs, typeof(T).Assembly);
        resourceFactory.Add(resource);
        return resourceFactory;
    }
}

中間件準(zhǔn)備完畢之后,我們開始寫管理 I18nResourceFactory 接口的實(shí)現(xiàn),以便管理好各種語言資源服務(wù)。

/// <summary>
/// i18n 語言資源管理器.
/// </summary>
public class InternalI18nResourceFactory : I18nResourceFactory
{
    private readonly List<CultureInfo> _supportedCultures;
    private readonly List<I18nResource> _resources;
    private readonly List<Type> _serviceResources;
    /// <summary>
    /// Initializes a new instance of the <see cref="InternalI18nResourceFactory"/> class.
    /// </summary>
    public InternalI18nResourceFactory()
    {
        _supportedCultures = new();
        _resources = new();
        _serviceResources = new();
    }
    /// <inheritdoc/>
    public IList<CultureInfo> SupportedCultures => _supportedCultures;
    /// <inheritdoc/>
    public IList<I18nResource> Resources => _resources;
    /// <inheritdoc/>
    public IList<Type> ServiceResources => _serviceResources;
    /// <inheritdoc/>
    public I18nResourceFactory Add(I18nResource resource)
    {
        _supportedCultures.Add(resource.SupportedCulture);
        _resources.Add(resource);
        return this;
    }
    /// <inheritdoc/>
    public I18nResourceFactory Add<T>(I18nResource<T> resource)
    {
        _supportedCultures.Add(resource.SupportedCulture);
        _resources.Add(resource);
        return this;
    }
    /// <inheritdoc/>
    public I18nResourceFactory AddServiceType(Type resourceType)
    {
        _serviceResources.Add(resourceType);
        return this;
    }
}

實(shí)現(xiàn) IStringLocalizerFactory 接口,根據(jù)泛型類型創(chuàng)建 IStringLocalizer 對(duì)象。

/// <summary>
/// 表示創(chuàng)建<see cref="IStringLocalizer"/> 實(shí)例的工廠.
/// </summary>
public class I18nStringLocalizerFactory : IStringLocalizerFactory
{
    private readonly I18nResourceFactory _i18nResourceFactory;
    private readonly IServiceProvider _serviceProvider;
    /// <summary>
    /// Initializes a new instance of the <see cref="I18nStringLocalizerFactory"/> class.
    /// </summary>
    /// <param name="i18nResourceFactory"></param>
    /// <param name="serviceProvider"></param>
    public I18nStringLocalizerFactory(I18nResourceFactory i18nResourceFactory, IServiceProvider serviceProvider)
    {
        _i18nResourceFactory = i18nResourceFactory;
        _serviceProvider = serviceProvider;
    }
    /// <inheritdoc/>
    public IStringLocalizer Create(Type resourceSource)
    {
        var type = typeof(I18nStringLocalizer<>).MakeGenericType(resourceSource);
        return (_serviceProvider.GetRequiredService(type) as IStringLocalizer)!;
    }
    /// <inheritdoc/>
    public IStringLocalizer Create(string baseName, string location)
    {
        return _serviceProvider.GetRequiredService<IStringLocalizer>();
    }
}

最后在 I18nExtensions 中添加 AddI18n 擴(kuò)展方法注入相關(guān)的服務(wù)。

/// <summary>
/// i18n 擴(kuò)展.
/// </summary>
public static class I18nExtensions
{
    /// <summary>
    /// 添加 i18n 支持服務(wù).
    /// </summary>
    /// <param name="services"></param>
    /// <param name="defaultLanguage">默認(rèn)語言.</param>
    public static void AddI18n(this IServiceCollection services, string defaultLanguage = "zh-CN")
    {
        InternalI18nResourceFactory resourceFactory = new InternalI18nResourceFactory();
        // i18n 上下文
        services.AddScoped<I18nContext, DefaultI18nContext>();
        // 注入 i18n 服務(wù)
        services.AddSingleton<I18nResourceFactory>(s => resourceFactory);
        services.AddScoped<IStringLocalizerFactory, I18nStringLocalizerFactory>();
        services.AddScoped<IStringLocalizer, I18nStringLocalizer>();
        services.TryAddEnumerable(new ServiceDescriptor(typeof(IStringLocalizer<>), typeof(I18nStringLocalizer<>), ServiceLifetime.Scoped));
    }
    /// <summary>
    /// 添加 i18n 資源.
    /// </summary>
    /// <param name="services"></param>
    /// <param name="resourceFactory"></param>
    public static void AddI18nResource(this IServiceCollection services, Action<I18nResourceFactory> resourceFactory)
    {
        var service = services.BuildServiceProvider().GetRequiredService<I18nResourceFactory>();
        resourceFactory.Invoke(service);
    }
}

對(duì)于 ASP.NET Core 應(yīng)用,則需要額外添加一些擴(kuò)展函數(shù):

/// <summary>
/// i18n 擴(kuò)展.
/// </summary>
public static class I18nExtensions
{
    /// <summary>
    /// 添加 i18n 支持服務(wù).
    /// </summary>
    /// <param name="services"></param>
    /// <param name="defaultLanguage">默認(rèn)語言.</param>
    public static void AddI18nAspNetCore(this IServiceCollection services, string defaultLanguage = "zh-CN")
    {
        services.AddI18n(defaultLanguage);
        var resourceFactory = services.BuildServiceProvider().GetRequiredService<I18nResourceFactory>();
        // ASP.NET Core 自帶的
        services.AddLocalization();
        // 配置 ASP.NET Core 的本地化服務(wù)
        services.Configure<RequestLocalizationOptions>(options =>
        {
            options.ApplyCurrentCultureToResponseHeaders = true;
            options.DefaultRequestCulture = new RequestCulture(culture: defaultLanguage, uiCulture: defaultLanguage);
            options.SupportedCultures = resourceFactory.SupportedCultures;
            options.SupportedUICultures = resourceFactory.SupportedCultures;
            // 默認(rèn)自帶了三個(gè)請(qǐng)求語言提供器,會(huì)先從這些提供器識(shí)別要使用的語言。
            // QueryStringRequestCultureProvider
            // CookieRequestCultureProvider
            // AcceptLanguageHeaderRequestCultureProvider
            // 自定義請(qǐng)求請(qǐng)求語言提供器
            options.RequestCultureProviders.Add(new InternalRequestCultureProvider(options));
        });
        // i18n 上下文
        services.AddScoped<I18nContext, HttpI18nContext>();
    }
    /// <summary>
    /// i18n 中間件.
    /// </summary>
    /// <param name="app"></param>
    public static void UseI18n(this IApplicationBuilder app)
    {
        var options = app.ApplicationServices.GetRequiredService<IOptions<RequestLocalizationOptions>>();
        app.UseRequestLocalization(options.Value);
    }
}
/// <summary>
/// 模型驗(yàn)證使用多語言.
/// </summary>
public static partial class DataAnnotationsExtensions
{
    /// <summary>
    /// 為 API 模型驗(yàn)證注入 i18n 服務(wù).
    /// </summary>
    /// <param name="builder"></param>
    /// <returns><see cref="IMvcBuilder"/>.</returns>
    public static IMvcBuilder AddI18nDataAnnotation(this IMvcBuilder builder)
    {
        builder
            .AddDataAnnotationsLocalization(options =>
            {
                options.DataAnnotationLocalizerProvider = (modelType, stringLocalizerFactory) =>
                stringLocalizerFactory.Create(modelType);
            });
        return builder;
    }
}

在本章中,講解了如何編寫一個(gè)全球化語言的類庫,其實(shí)現(xiàn)比較簡單,也許并不滿足業(yè)務(wù)系統(tǒng)的需求,那么你可以根據(jù)本章的內(nèi)容,結(jié)合業(yè)務(wù)系統(tǒng)的需求,實(shí)現(xiàn)一個(gè)更好用的 i18n 類庫。

單元測試

編寫單元測試和性能測試是開發(fā)者需要掌握的技能之一,在本書中的第四章介紹了性能測試的編寫示例,在本章中繼續(xù)介紹單元測試的編寫方法,讀者還可以從從 Maomi 倉庫源碼中了解看到更多的單元測試示例。

創(chuàng)建一個(gè) xUnit 單元測試項(xiàng)目,項(xiàng)目文件結(jié)構(gòu)如下:

添加對(duì) Maomi.I18n 、Microsoft.AspNetCore.Mvc.Testing 兩個(gè)類庫的引用。

在 I18nTest 中創(chuàng)建一個(gè)測試方法:

[Fact]
public async Task I18n_Request()
{    
}

首先構(gòu)建一個(gè)用于測試的 Web Host,并且注入相關(guān)的服務(wù),用來模擬啟動(dòng) Web 服務(wù)。

using var host = await new HostBuilder()
	.ConfigureWebHost(webBuilder =>
	{
		webBuilder
			.UseTestServer()
			.ConfigureServices(services =>
			{
				services.AddControllers();
				services.AddI18n(defaultLanguage: "zh-CN");
				services.AddI18nResource(option =>
				{
					var basePath = "i18n";
					option.AddJson(basePath);
				});
			})
			.Configure(app =>
			{
				app.UseI18n();
				app.UseRouting();
				app.Use(async (HttpContext context, RequestDelegate next) =>
				{
					var localizer = context.RequestServices.GetRequiredService<IStringLocalizer<Program>>();
					await context.Response.WriteAsync(localizer["購物車:商品名稱"]);
					return;
				});
			});
	})
	.StartAsync();

由于該 Host 不會(huì)真的啟動(dòng)一個(gè) Web 服務(wù),因此無法直接發(fā)起 HttpClient 請(qǐng)求進(jìn)行測試,需要從 Host 中創(chuàng)建一個(gè) HttpClient 對(duì)象:

var httpClient = host.GetTestClient();

下面給出分別測試路由、Cookie、Accept-Language 標(biāo)頭的三種解析語言標(biāo)識(shí)測試 i18n 框架的示例代碼:

httpClient.DefaultRequestHeaders.AcceptLanguage.Clear();
var response = await httpClient.GetStringAsync("/test?culture=en-US&ui-culture=en-US");
Assert.Equal("Product name", response);
response = await httpClient.GetStringAsync("/test?culture=zh-CN&ui-culture=zh-CN");
Assert.Equal("商品名稱", response);
httpClient.DefaultRequestHeaders.Add("Cookie", ".AspNetCore.Culture=c=en-US|uic=en-US");
response = await httpClient.GetStringAsync("/test");
Assert.Equal("Product name", response);
httpClient.DefaultRequestHeaders.Add("Cookie", ".AspNetCore.Culture=c=zh-CN|uic=zh-CN");
response = await httpClient.GetStringAsync("/test");
Assert.Equal("商品名稱", response);
httpClient.DefaultRequestHeaders.Remove("Cookie");
httpClient.DefaultRequestHeaders.AcceptLanguage.Clear();
httpClient.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("zh-CN"));
httpClient.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("zh", 0.9));
response = await httpClient.GetStringAsync("/test");
Assert.Equal("商品名稱", response);
httpClient.DefaultRequestHeaders.AcceptLanguage.Clear();
httpClient.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("en-US"));
httpClient.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("en", 0.9));
response = await httpClient.GetStringAsync("/test");
Assert.Equal("Product name", response);
httpClient.DefaultRequestHeaders.AcceptLanguage.Clear();
httpClient.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("sv"));
httpClient.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("en-US", 0.9));
response = await httpClient.GetStringAsync("/test");
Assert.Equal("Product name", response);

基于 Redis 的動(dòng)態(tài)多語言

本節(jié)示例代碼在 Maomi.I18n.Redis 中,該擴(kuò)展庫通過實(shí)現(xiàn) I18nResource 接口,從 Redis 中加載多語言數(shù)據(jù)緩存到本地客戶端中,當(dāng) Redis 中的數(shù)據(jù)變化時(shí),客戶端自動(dòng)拉取最新的值。由于數(shù)據(jù)會(huì)被緩存到本地,所以該擴(kuò)展庫可以實(shí)時(shí)更新多語言數(shù)據(jù),同時(shí)保持高性能。

新建一個(gè) Maomi.I18n.Redis 項(xiàng)目,引用 Maomi.I18n 庫 和 FreeRedis 庫,創(chuàng)建 RedisI18nResource 類實(shí)現(xiàn) I18nResource 接口,從 Redis 中讀取值。

// i18n redis 資源
public class RedisI18nResource : I18nResource
{
	private readonly RedisClient _redisClient;
	private readonly string _pathPrefix;
	internal RedisI18nResource(RedisClient redisClient, string pathPrefix, TimeSpan expired, int capacity = 10)
	{
		_redisClient = redisClient;
		_pathPrefix = pathPrefix;
		// Redis client-side 模式
		redisClient.UseClientSideCaching(new ClientSideCachingOptions
		{
			Capacity = capacity,
			KeyFilter = key => key.StartsWith(pathPrefix),
			CheckExpired = (key, dt) => DateTime.Now.Subtract(dt) > expired
		});
		// FreeRedis 的 client-side 模式,使用 Hash 類型時(shí),
		// 第一次需要先 HGetAll() ,框架將緩存拉取到本地
		GetAllStrings(default);
	}
	public IReadOnlyList<CultureInfo> SupportedCultures => _redisClient
		.Keys(_pathPrefix)
		.Select(x => new CultureInfo(x.Remove(0, _pathPrefix.Length + 1))).ToList();
	public IReadOnlyList<CultureInfo> SupportedUICultures => _redisClient
		.Keys(_pathPrefix)
		.Select(x => new CultureInfo(x.Remove(0, _pathPrefix.Length + 1))).ToList();
	public LocalizedString Get(string culture, string name)
	{
		var key = $"{_pathPrefix}:{culture}";
		var value = _redisClient.HGet<string>(key, name);
		if (string.IsNullOrEmpty(value)) return new LocalizedString(name, name, resourceNotFound: true);
		return new LocalizedString(name, value);
	}
	public LocalizedString Get(string culture, string name, params object[] arguments)
	{
		var key = $"{_pathPrefix}:{culture}";
		var value = _redisClient.HGet<string>(key, name);
		if (string.IsNullOrEmpty(value)) return new LocalizedString(name, name, resourceNotFound: true);
		var v = string.Format(value, arguments);
		return new LocalizedString(name, v);
	}
	public LocalizedString Get<T>(string culture, string name)
	{
		var key = $"{_pathPrefix}:{culture}";
		var value = _redisClient.HGet<string>(key, name);
		if (string.IsNullOrEmpty(value)) return new LocalizedString(name, name, resourceNotFound: true);
		return new LocalizedString(name, value);
	}
	public LocalizedString Get<T>(string culture, string name, params object[] arguments)
	{
		var key = $"{_pathPrefix}:{culture}";
		var value = _redisClient.HGet<string>(key, name);
		if (string.IsNullOrEmpty(value)) return new LocalizedString(name, name, resourceNotFound: true);
		var v = string.Format(value, arguments);
		return new LocalizedString(name, v);
	}
	public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
	{
		var keys = _redisClient.Keys(_pathPrefix);
		foreach (var key in keys)
		{
			var vs = _redisClient.HGetAll<string>(key);
			foreach (var item in vs)
			{
				yield return new LocalizedString(item.Key, item.Value);
			}
		}
	}
}

然后編寫一個(gè)擴(kuò)展類,利用 FreeRedis 中的 API,將 Redis 中的緩存拉取到本地內(nèi)存中,并且 Redis 中的緩存變化時(shí),F(xiàn)reeRedis 會(huì)自動(dòng)拉取到本地?;谶@個(gè)特性,我們雖然使用了 Redis 存儲(chǔ)多語言,但是每次讀取時(shí)實(shí)際上都是在本地內(nèi)存讀取的,因此具有極高的性能和速度,也避免了網(wǎng)絡(luò)開銷。

public static class Extensions
{
	// 添加 i18n redis 資源
	public static I18nResourceFactory AddRedis(this I18nResourceFactory resourceFactory,
		RedisClient.DatabaseHook redis,
		string pathPrefix,
		TimeSpan expired,
		int capacity = 10
		)
	{
		redis.UseClientSideCaching(new ClientSideCachingOptions
		{
			Capacity = capacity,
			KeyFilter = key => key.StartsWith(pathPrefix),
			CheckExpired = (key, dt) => DateTime.Now.Subtract(dt) > expired
		});
		var keys = redis.Keys(pathPrefix);
		resourceFactory.Add(new RedisI18nResource(redis, pathPrefix));
		return resourceFactory;
	}
}

Demo6.Redis 項(xiàng)目示范了該擴(kuò)展的使用方法,關(guān)鍵部分代碼示例如下:

WRedisClient cli = new RedisClient("127.0.0.1:6379,defaultDatabase=0");
builder.Services.AddI18n(defaultLanguage: "zh-CN");
builder.Services.AddI18nResource(option =>
{
	option.AddRedis(cli, "language", TimeSpan.FromMinutes(100), 10);
	option.AddJson<Program>("i18n");
});

在 Redis 中創(chuàng)建 Hash 類型的 key,并設(shè)置一些鍵值對(duì),然后在客戶端中讀取出來。

nuget 打包嵌入 json

在企業(yè)內(nèi)部開發(fā)時(shí),可能要將項(xiàng)目打包為 nuget 提供給其他開發(fā)者使用,所以多語言資源文件也需要打包到 nuget 包中。

創(chuàng)建 Demo5.Nuget 類庫項(xiàng)目,其目錄結(jié)構(gòu)如下:

修改 .csproj 文件,將相關(guān)屬性修改為如下所示配置:

<ItemGroup>
		<Content Include="i18n\Demo5.Nuget\en-US.json" Pack="true">
			<PackageCopyToOutput>true</PackageCopyToOutput>
			<CopyToOutputDirectory>Always</CopyToOutputDirectory>
			<PackagePath>contentFiles\any\any\i18n\Demo5.Nuget\en-US.json</PackagePath>
		</Content>
		<Content Include="i18n\Demo5.Nuget\zh-CN.json" Pack="true">
			<PackageCopyToOutput>true</PackageCopyToOutput>
			<CopyToOutputDirectory>Always</CopyToOutputDirectory>
			<PackagePath>contentFiles\any\any\i18n\Demo5.Nuget\zh-CN.json</PackagePath>
		</Content>
	</ItemGroup>

而在 Web 項(xiàng)目中,由于編譯器已經(jīng)自動(dòng)設(shè)置了 EnableDefaultContentItems 屬性,自動(dòng)設(shè)置給 web.config、 .json 、 .cshtml 文件設(shè)置 <Content></Content> 屬性,所以自定義配置 Content 屬性時(shí)會(huì)沖突,我們需要在 <PropertyGroup> </PropertyGroup> 屬性中關(guān)閉此配置。

<EnableDefaultContentItems>false</EnableDefaultContentItems>

使用者引入 nuget 包后,可以看到項(xiàng)目中出現(xiàn)了對(duì)應(yīng)的文件。

到此這篇關(guān)于.NET 高級(jí)開發(fā) | i18n 原理、實(shí)現(xiàn)一個(gè) i18n 框架的文章就介紹到這了,更多相關(guān).NET 高級(jí)開發(fā) | i18n 原理、實(shí)現(xiàn)一個(gè) i18n 框架內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論

双流县| 桦川县| 唐海县| 娱乐| 大足县| 象山县| 东安县| 咸宁市| 博白县| 遵义市| 子长县| 铜川市| 三河市| 伽师县| 泉州市| 北安市| 牟定县| 滨州市| 绥阳县| 钟山县| 柘城县| 瑞金市| 抚顺县| 沽源县| 双流县| 确山县| 柏乡县| 娄底市| 佳木斯市| 五家渠市| 馆陶县| 黔江区| 昭平县| 保德县| 开封市| 永济市| 宁国市| 新营市| 石首市| 通渭县| 平湖市|