目录
1. 上一篇文章解决方案的变体
在本文中,我们将展示上一篇文章中关于如何解决只有一个Resx语言字符串文件的问题的解决方案的变体。我们之所以展示这个变体,是因为它在互联网上是一种流行的方法(参见[7]、[8]、[9]),尽管基本工作原则与上一篇文章相同。这种方法是帮助程序/包装对象的一种用法,以实现相同的结果。
我个人更喜欢上一篇文章中的直接方法,但这种方法在互联网上非常流行,因此由开发人员根据他/她的喜好进行选择。
2. 本系列文章
本系列中的文章包括:
- ASP.NET 8——使用单个Resx文件的多语言应用程序
- ASP.NET 8——使用单个Resx文件的多语言应用程序——第2部分——替代方法
- ASP.NET 8——使用单个Resx文件的多语言应用程序——第3部分——表单验证字符串
- ASP.NET 8——具有单个Resx文件的多语言应用程序——第4部分——资源管理器
3. 共享资源方法
默认情况下,ASP.NET Core 8 MVC技术为每个控制器和视图设想单独的资源文件.resx。但是大多数人不喜欢它,因为大多数多语言字符串在应用程序的不同位置都是相同的,我们希望它都在同一个地方。文献[1]将这种方法称为“共享资源”方法。为了实现它,我们将创建一个标记类SharedResources.cs来对所有资源进行分组。
然后,在我们的应用程序中,我们使用工厂函数创建一个专注于该类/类型的StringLocalizer服务,并将其包装到名为SharedStringLocalizer的帮助程序对象中。
然后,在我们的应用程序中,我们将使用依赖注入(DI)将该包装对象/服务注入到我们需要本地化服务的方法中。
与本系列上一篇文章的解决方案的主要区别在于,我们不是使用DI直接IStringLocalizer<SharedResource>注入,而是将其包装到helper对象SharedStringLocalizer中,然后注入该helper对象。它如何工作的基本原理是相同的。
4. 多语种申请步骤
4.1 创建标记类SharedResources.cs
这只是一个用于对共享资源进行分组的虚拟标记类。我们需要它的名称和类型。
似乎命名空间需要与应用根命名空间相同,而应用根命名空间需要与程序集名称相同。我在更改命名空间时遇到了一些问题,它不起作用。
SharedResource名称中没有魔法,您可以命名它为MyResources并将代码中的所有引用更改为MyResources,并且所有引用仍然有效。
该位置似乎可以是任何文件夹,尽管某些文章([6])声称它必须是根项目文件夹。在这个例子中,我没有看到这样的问题。对我来说,看起来它可以是任何文件夹,只需保持命名空间整洁即可。
//SharedResource.cs===================================================
namespace SharedResources02
{
/*
* This is just a dummy marker class to group shared resources
* We need it for its name and type
*
* It seems the namespace needs to be the same as app root namespace
* which needs to be the same as the assembly name.
* I had some problems when changing the namespace, it would not work.
* If it doesn't work for you, you can try to use full class name
* in your DI instruction, like this one: SharedResources02.SharedResource
*
* There is no magic in the name "SharedResource", you can
* name it "MyResources" and change all references in the code
* to "MyResources" and all will still work
*
* Location seems can be any folder, although some
* articles claim it needs to be the root project folder.
* I do not see such problems in this example.
* To me, looks it can be any folder, just keep your
* namespace tidy.
*/
public class SharedResource
{
}
}
4.2 创建包装器帮助程序类
我们将创建包装器帮助程序类/服务,我们将使用DI将其注入到我们的代码中。
//ISharedStringLocalizer.cs=================================
namespace SharedResources02
{
//we create this interface to use it for DI dependency setting
public interface ISharedStringLocalizer
{
public LocalizedString this[string key]
{
get;
}
LocalizedString GetLocalizedString(string key);
}
}
//SharedStringLocalizer.cs==================================================
namespace SharedResources02
{
//we create this helper/wrapper class/service
//that we are going to pass around in DI
public class SharedStringLocalizer : ISharedStringLocalizer
{
//here is object that is doing the real work
//it is almost the same as IStringLocalizer<SharedResource>
private readonly IStringLocalizer localizer;
public SharedStringLocalizer(IStringLocalizerFactory factory)
{
var type = typeof(SharedResource);
var assemblyName = new AssemblyName(
type.GetTypeInfo().Assembly.FullName ?? String.Empty);
this.localizer = factory.Create("SharedResource",
assemblyName?.Name ?? String.Empty);
}
//in our methods, we just pass work to internal object
public LocalizedString this[string key] => this.localizer[key];
public LocalizedString GetLocalizedString(string key)
{
return this.GetLocalizedString(key);
}
}
}
//ISharedHtmlLocalizer.cs===============================================
namespace SharedResources02
{
//we create this interface to use it for DI dependency setting
public interface ISharedHtmlLocalizer
{
public LocalizedHtmlString this[string key]
{
get;
}
LocalizedHtmlString GetLocalizedString(string key);
}
}
//SharedHtmlLocalizer.cs==================================================
namespace SharedResources02
{
//we create this helper/wrapper class/service
//that we are going to pass around in DI
public class SharedHtmlLocalizer: ISharedHtmlLocalizer
{
//here is object that is doing the real work
//it is almost the same as IHtmlLocalizer<SharedResource>
private readonly IHtmlLocalizer localizer;
public SharedHtmlLocalizer(IHtmlLocalizerFactory factory)
{
var type = typeof(SharedResource);
var assemblyName = new AssemblyName(
type.GetTypeInfo().Assembly.FullName ?? String.Empty);
this.localizer = factory.Create("SharedResource",
assemblyName?.Name ?? String.Empty);
}
//in our methods, we just pass work to internal object
public LocalizedHtmlString this[string key] => this.localizer[key];
public LocalizedHtmlString GetLocalizedString(string key)
{
return this.GetLocalizedString(key);
}
}
}
4.3 创建语言资源文件
在“资源”文件夹中,创建语言资源文件,并确保将其命名为SharedResources.xx.resx。
4.4 配置本地化服务和中间件
本地化服务配置Program.cs:
private static void AddingMultiLanguageSupportServices(WebApplicationBuilder? builder)
{
if (builder == null) { throw new Exception("builder==null"); };
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
builder.Services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[] { "en", "fr", "de", "it" };
options.SetDefaultCulture(supportedCultures[0])
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
});
builder.Services.AddSingleton<ISharedStringLocalizer, SharedStringLocalizer>();
builder.Services.AddSingleton<ISharedHtmlLocalizer, SharedHtmlLocalizer>();
}
private static void AddingMultiLanguageSupport(WebApplication? app)
{
app?.UseRequestLocalization();
}
4.5 选择语言/文化
基于[5],本地化服务有三个默认提供程序:
- QueryStringRequestCultureProvider
- CookieRequestCultureProvider
- AcceptLanguageHeaderRequestCultureProvider
由于大多数应用通常会提供一种机制来设置区域性,用于使用ASP.NET Core区域性Cookie设置区域性,因此在示例中,我们将仅关注该方法。
这是设置.AspNetCore.Culture cookie的代码:
private void ChangeLanguage_SetCookie(HttpContext myContext, string? culture)
{
if(culture == null) { throw new Exception("culture == null"); };
//this code sets .AspNetCore.Culture cookie
myContext.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
new CookieOptions { Expires = DateTimeOffset.UtcNow.AddMonths(1) }
);
}
使用Chrome DevTools可以很容易地看到Cookie:
我构建了一个小型应用程序来演示它,这是我更改语言的屏幕:
请注意,我在页脚中添加了一些调试信息,以显示请求语言cookie的值,以查看应用程序是否按预期工作。
4.6 在控制器中使用本地化服务
当然,在控制器中,依赖注入(DI)进入并填充所有依赖关系。因此,这里将注入SharedStringLocalizer和SharedHtmlLocalizer服务。下面是代码片段:
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly ISharedStringLocalizer _stringLocalizer;
private readonly ISharedHtmlLocalizer _htmlLocalizer;
/* Here is, of course, the Dependency Injection (DI) coming in and filling
* all the dependencies.
* So, here, services SharedStringLocalizer and SharedHtmlLocalizer
* will be injected
*/
public HomeController(ILogger<HomeController> logger,
ISharedStringLocalizer stringLocalizer,
ISharedHtmlLocalizer htmlLocalizer)
{
_logger = logger;
_stringLocalizer = stringLocalizer;
_htmlLocalizer = htmlLocalizer;
}
public IActionResult LocalizationExample(LocalizationExampleViewModel model)
{
//so, here we use ISharedStringLocalizer
model.IStringLocalizerInController = _stringLocalizer["Wellcome"];
//so, here we use ISharedHtmlLocalizer
model.IHtmlLocalizerInController = _htmlLocalizer["Wellcome"];
return View(model);
}
4.7 在视图中使用本地化服务
当然,在视图中,依赖注入(DI)进入并填充所有依赖项。因此,这里将注入SharedStringLocalizer和SharedHtmlLocalizer服务。下面是代码片段:
@* LocalizationExample.cshtml ====================================================*@
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.Extensions.Localization
@model LocalizationExampleViewModel
@* Here is, of course, the Dependency Injection (DI) coming in and filling
all the dependencies.
So, here services SharedStringLocalizer and SharedHtmlLocalizer
will be injected
*@
@inject ISharedStringLocalizer StringLocalizer
@inject ISharedHtmlLocalizer HtmlLocalizer
@{
<div style="width:600px">
<p class="bg-info">
ISharedStringLocalizer Localized in Controller:
@Model.IStringLocalizerInController
</p>
<p class="bg-info">
@{
string? text1 = StringLocalizer["Wellcome"];
}
ISharedStringLocalizer Localized in View: @text1
</p>
<p class="bg-info">
ISharedHtmlLocalizer Localized in Controller:
@Model.IHtmlLocalizerInController
</p>
<p class="bg-info">
@{
string? text2 = "Wellcome";
}
ISharedHtmlLocalizer Localized in View: @HtmlLocalizer[@text2]
</p>
</div>
}
4.8 执行结果
执行结果如下所示:
请注意,我在页脚中添加了一些调试信息,以显示请求语言cookie 的值,以查看应用程序是否按预期工作。
5. 完整代码
由于大多数人都喜欢可以复制粘贴的代码,因此这是应用程序的完整代码。
//SharedResource.cs===================================================
namespace SharedResources02
{
/*
* This is just a dummy marker class to group shared resources
* We need it for its name and type
*
* It seems the namespace needs to be the same as app root namespace
* which needs to be the same as the assembly name.
* I had some problems when changing the namespace, it would not work.
* If it doesn't work for you, you can try to use full class name
* in your DI instruction, like this one: SharedResources02.SharedResource
*
* There is no magic in the name "SharedResource", you can
* name it "MyResources" and change all references in the code
* to "MyResources" and all will still work
*
* Location seems can be any folder, although some
* articles claim it needs to be the root project folder.
* I do not see such problems in this example.
* To me, looks it can be any folder, just keep your
* namespace tidy.
*/
public class SharedResource
{
}
}
//ISharedStringLocalizer.cs=================================
namespace SharedResources02
{
//we create this interface to use it for DI dependency setting
public interface ISharedStringLocalizer
{
public LocalizedString this[string key]
{
get;
}
LocalizedString GetLocalizedString(string key);
}
}
//SharedStringLocalizer.cs==================================================
namespace SharedResources02
{
//we create this helper/wrapper class/service
//that we are going to pass around in DI
public class SharedStringLocalizer : ISharedStringLocalizer
{
//here is object that is doing the real work
//it is almost the same as IStringLocalizer<SharedResource>
private readonly IStringLocalizer localizer;
public SharedStringLocalizer(IStringLocalizerFactory factory)
{
var type = typeof(SharedResource);
var assemblyName = new AssemblyName(
type.GetTypeInfo().Assembly.FullName ?? String.Empty);
this.localizer = factory.Create("SharedResource",
assemblyName?.Name ?? String.Empty);
}
//in our methods, we just pass work to internal object
public LocalizedString this[string key] => this.localizer[key];
public LocalizedString GetLocalizedString(string key)
{
return this.GetLocalizedString(key);
}
}
}
//ISharedHtmlLocalizer.cs===============================================
namespace SharedResources02
{
//we create this interface to use it for DI dependency setting
public interface ISharedHtmlLocalizer
{
public LocalizedHtmlString this[string key]
{
get;
}
LocalizedHtmlString GetLocalizedString(string key);
}
}
//SharedHtmlLocalizer.cs==================================================
namespace SharedResources02
{
//we create this helper/wrapper class/service
//that we are going to pass around in DI
public class SharedHtmlLocalizer: ISharedHtmlLocalizer
{
//here is object that is doing the real work
//it is almost the same as IHtmlLocalizer<SharedResource>
private readonly IHtmlLocalizer localizer;
public SharedHtmlLocalizer(IHtmlLocalizerFactory factory)
{
var type = typeof(SharedResource);
var assemblyName = new AssemblyName(
type.GetTypeInfo().Assembly.FullName ?? String.Empty);
this.localizer = factory.Create("SharedResource",
assemblyName?.Name ?? String.Empty);
}
//in our methods, we just pass work to internal object
public LocalizedHtmlString this[string key] => this.localizer[key];
public LocalizedHtmlString GetLocalizedString(string key)
{
return this.GetLocalizedString(key);
}
}
}
//Program.cs===========================================================================
namespace SharedResources02
{
public class Program
{
public static void Main(string[] args)
{
//=====Middleware and Services=============================================
var builder = WebApplication.CreateBuilder(args);
//adding multi-language support
AddingMultiLanguageSupportServices(builder);
// Add services to the container.
builder.Services.AddControllersWithViews();
//====App===================================================================
var app = builder.Build();
//adding multi-language support
AddingMultiLanguageSupport(app);
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=ChangeLanguage}/{id?}");
app.Run();
}
private static void AddingMultiLanguageSupportServices
(WebApplicationBuilder? builder)
{
if (builder == null) { throw new Exception("builder==null"); };
builder.Services.AddLocalization
(options => options.ResourcesPath = "Resources");
builder.Services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[] { "en", "fr", "de", "it" };
options.SetDefaultCulture(supportedCultures[0])
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
});
builder.Services.AddSingleton<ISharedStringLocalizer, SharedStringLocalizer>();
builder.Services.AddSingleton<ISharedHtmlLocalizer, SharedHtmlLocalizer>();
}
private static void AddingMultiLanguageSupport(WebApplication? app)
{
app?.UseRequestLocalization();
}
}
}
//HomeController.cs================================================================
namespace SharedResources02.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly ISharedStringLocalizer _stringLocalizer;
private readonly ISharedHtmlLocalizer _htmlLocalizer;
/* Here is, of course, the Dependency Injection (DI) coming in and filling
* all the dependencies.
* So, here services SharedStringLocalizer and SharedHtmlLocalizer
* will be injected
*/
public HomeController(ILogger<HomeController> logger,
ISharedStringLocalizer stringLocalizer,
ISharedHtmlLocalizer htmlLocalizer)
{
_logger = logger;
_stringLocalizer = stringLocalizer;
_htmlLocalizer = htmlLocalizer;
}
public IActionResult ChangeLanguage(ChangeLanguageViewModel model)
{
if (model.IsSubmit)
{
HttpContext myContext = this.HttpContext;
ChangeLanguage_SetCookie(myContext, model.SelectedLanguage);
//doing funny redirect to get new Request Cookie
//for presentation
return LocalRedirect("/Home/ChangeLanguage");
}
//prepare presentation
ChangeLanguage_PreparePresentation(model);
return View(model);
}
private void ChangeLanguage_PreparePresentation(ChangeLanguageViewModel model)
{
model.ListOfLanguages = new List<SelectListItem>
{
new SelectListItem
{
Text = "English",
Value = "en"
},
new SelectListItem
{
Text = "German",
Value = "de",
},
new SelectListItem
{
Text = "French",
Value = "fr"
},
new SelectListItem
{
Text = "Italian",
Value = "it"
}
};
}
private void ChangeLanguage_SetCookie(HttpContext myContext, string? culture)
{
if(culture == null) { throw new Exception("culture == null"); };
//this code sets .AspNetCore.Culture cookie
myContext.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
new CookieOptions { Expires = DateTimeOffset.UtcNow.AddMonths(1) }
);
}
public IActionResult LocalizationExample(LocalizationExampleViewModel model)
{
//so, here we use ISharedStringLocalizer
model.IStringLocalizerInController = _stringLocalizer["Wellcome"];
//so, here we use ISharedHtmlLocalizer
model.IHtmlLocalizerInController = _htmlLocalizer["Wellcome"];
return View(model);
}
public IActionResult Error()
{
return View(new ErrorViewModel
{ RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}
//ChangeLanguageViewModel.cs=====================================================
namespace SharedResources02.Models.Home
{
public class ChangeLanguageViewModel
{
//model
public string? SelectedLanguage { get; set; } = "en";
public bool IsSubmit { get; set; } = false;
//view model
public List<SelectListItem>? ListOfLanguages { get; set; }
}
}
//LocalizationExampleViewModel.cs===============================================
namespace SharedResources02.Models.Home
{
public class LocalizationExampleViewModel
{
public string? IStringLocalizerInController { get; set; }
public LocalizedHtmlString? IHtmlLocalizerInController { get; set; }
}
}
@* ChangeLanguage.cshtml ===================================================*@
@model ChangeLanguageViewModel
@{
<div style="width:500px">
<p class="bg-info">
<partial name="_Debug.AspNetCore.CultureCookie" /><br />
</p>
<form id="form1">
<fieldset class="border rounded-3 p-3">
<legend class="float-none w-auto px-3">Change Language</legend>
<div class="form-group">
<label asp-for="SelectedLanguage">Select Language</label>
<select class="form-select" asp-for="SelectedLanguage"
asp-items="@Model.ListOfLanguages">
</select>
<input type="hidden" name="IsSubmit" value="true">
<button type="submit" form="form1"
class="btn btn-primary mt-3 float-end"
asp-area="" asp-controller="Home"
asp-action="ChangeLanguage">
Submit
</button>
</div>
</fieldset>
</form>
</div>
}
@* LocalizationExample.cshtml ====================================================*@
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.Extensions.Localization
@model LocalizationExampleViewModel
@* Here is, of course, the Dependency Injection (DI) coming in and filling
all the dependencies.
So, here services SharedStringLocalizer and SharedHtmlLocalizer
will be injected
*@
@inject ISharedStringLocalizer StringLocalizer
@inject ISharedHtmlLocalizer HtmlLocalizer
@{
<div style="width:600px">
<p class="bg-info">
ISharedStringLocalizer Localized in Controller:
@Model.IStringLocalizerInController
</p>
<p class="bg-info">
@{
string? text1 = StringLocalizer["Wellcome"];
}
ISharedStringLocalizer Localized in View: @text1
</p>
<p class="bg-info">
ISharedHtmlLocalizer Localized in Controller:
@Model.IHtmlLocalizerInController
</p>
<p class="bg-info">
@{
string? text2 = "Wellcome";
}
ISharedHtmlLocalizer Localized in View: @HtmlLocalizer[@text2]
</p>
</div>
}
6. 参考资料
- [1] 使ASP.NET Core应用的内容可本地化
- [2] 在ASP.NET Core应用中提供语言和文化的本地化资源
- [3] 实施策略,在本地化的ASP.NET Core应用中为每个请求选择语言/区域性
- [4] ASP.NET Core中的全球化和本地化
- [5] 排查ASP.NET Core本地化问题
- [6] 在SharedResources的帮助下ASP.NET Core本地化
- [7] 使用ASP.NET Core MVC添加多种语言
- [8] ASP.NET Core本地化:一个RESX来统治它们
- [9] 在ASP.NET Core 3.1中使用单个资源文件进行视图本地化
https://www.codeproject.com/Articles/5378997/ASP-NET-8-Multilingual-Application-with-Single-R-2