ASP.NET 8 – 多语言应用程序,使用单一 Resx 文件 - 第 4 部分 – 资源管理器
构建多语言 ASP.NET 8 MVC 应用程序的实用指南
1. 资源管理器在 ASP.NET 8 MVC 中仍然可用
对于喜欢传统方法的人来说,好消息是资源管理器在 ASP.NET 8 MVC 中仍然可用。您可以将其与 IStringLocalizer
同时使用,或者如果您喜欢,甚至可以将其作为唯一的本地化机制。
1.1 资源管理器的工作原理
因此,一种典型的方法是在代码中使用表达式,例如 Resources.SharedResource.Wellcome
。这实际上是一个求值为 string
的属性。字符串的求值是在运行时动态完成的,并且字符串是从 SharedResource resx 文件中选择的,具体取决于当前线程的区域性。
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] 将这种方法称为“共享资源”方法。为了实现这一点,我们将创建一个标记类 SharedResoureces.cs 来 agrup 所有资源。
4. 多语言应用程序步骤
4.1 配置本地化服务和中间件
本地化服务在 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);
});
}
private static void AddingMultiLanguageSupport(WebApplication? app)
{
app?.UseRequestLocalization();
}
4.2 创建标记类 SharedResources.cs
这只是一个用于 agrupir 共享资源的虚拟标记类。我们需要它是因为它的名称和类型。
似乎命名空间需要与应用程序的根命名空间相同,而根命名空间需要与程序集名称相同。我在更改命名空间时遇到了一些问题,它不起作用。如果对您不起作用,您可以尝试在 DI 指令中使用完整的类名,例如:IStringLocalizer<SharedResources01.SharedResource> StringLocalizer
“SharedResource
”这个名字没有什么魔法,你可以将其命名为“MyResources
”,然后在代码中更改所有对“MyResources
”的引用,一切仍然会正常工作。
似乎位置可以是任何文件夹,尽管一些文章 ([6] 声称它需要是项目根文件夹。在这个示例中我没有看到这样的问题。对我来说,看起来它应该是任何文件夹,只要保持您的命名空间整洁即可。
//SharedResource.cs===================================================
namespace SharedResources04
{
/*
* 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:
* IStringLocalizer<SharedResources04.SharedResource> StringLocalizer
*
* 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.3 创建语言资源文件
在“Resources”文件夹中,创建你的语言资源文件,并确保将它们命名为 SharedResources.xx.resx。
在 Visual Studio 资源编辑器中,您需要为所有资源文件设置访问修饰符为 Public。
4.4 选择语言/区域性
基于 [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
CookieOptions cookieOptions = new CookieOptions();
cookieOptions.Expires = DateTimeOffset.UtcNow.AddMonths(1);
cookieOptions.IsEssential = true;
myContext.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
cookieOptions
);
}
可以使用 Chrome DevTools 轻松查看 Cookie
我构建了一个小型应用程序来演示它,这是我可以更改语言的屏幕
请注意,我在页脚添加了一些调试信息,以显示 **请求语言 Cookie** 的值,以查看应用程序是否按预期工作。
4.5 在控制器中使用本地化服务
在这里,我们展示了 IStringLocalizer
和 **资源管理器** 这两种方法如何用于本地化控制器代码中的字符串
这是代码片段:
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly IStringLocalizer<SharedResource> _stringLocalizer;
/* Here is, of course, the Dependency Injection (DI) coming in and filling
* all the dependencies. The key thing is we are asking for a specific
* type=SharedResource.
* If it doesn't work for you, you can try to use full class name
* in your DI instruction, like this one:
* IStringLocalizer<SharedResources04.SharedResource> stringLocalizer
*/
public HomeController(ILogger<HomeController> logger,
IStringLocalizer<SharedResource> stringLocalizer)
{
_logger = logger;
_stringLocalizer = stringLocalizer;
}
//============================
public IActionResult LocalizationExample(LocalizationExampleViewModel model)
{
string text = "Thread CurrentUICulture is [" +
@Thread.CurrentThread.CurrentUICulture.ToString() + "] ; ";
text += "Thread CurrentCulture is [" + @Thread.CurrentThread.CurrentCulture.ToString() + "]";
model.ThreadCultureInController = text;
//here we test localization by Resource Manager
model.LocalizedInControllerByResourceManager1 = Resources.SharedResource.Wellcome;
model.LocalizedInControllerByResourceManager2 = Resources.SharedResource.Hello_World;
//here we test localization by IStringLocalizer
model.LocalizedInControllerByIStringLocalizer1 = _stringLocalizer["Wellcome"];
model.LocalizedInControllerByIStringLocalizer2 = _stringLocalizer["Hello World"];
return View(model);
}
4.6 在视图中使用本地化服务
在这里,我们展示了 IStringLocalizer
和 **资源管理器** 这两种方法如何用于本地化视图代码中的 string
。
这是代码片段:
@* LocalizationExample.cshtml ====================================================*@
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.Extensions.Localization
@using SharedResources04
@model LocalizationExampleViewModel
@* Here is of course the Dependency Injection (DI) coming in and filling
all the dependencies. The key thing is we are asking for a specific
type=SharedResource.
If it doesn't work for you, you can try to use full class name
in your DI instruction, like this one:
@inject IStringLocalizer<SharedResources01.SharedResource> StringLocalizer
*@
@inject IStringLocalizer<SharedResource> StringLocalizer
@{
<div style="width:600px">
<p class="text-success">
Controller Thread Culture: <br />
@Model.ThreadCultureInController
</p>
<p class="text-primary">
Localized In Controller By ResourceManager: <br />
@Model.LocalizedInControllerByResourceManager1
</p>
<p class="text-primary">
Localized In Controller By ResourceManager: <br />
@Model.LocalizedInControllerByResourceManager2
</p>
<p class="text-primary">
Localized In Controller By IStringLocalizer: <br />
@Model.LocalizedInControllerByIStringLocalizer1
</p>
<p class="text-primary">
Localized In Controller By IStringLocalizer: <br />
@Model.LocalizedInControllerByIStringLocalizer2
</p>
<p class="text-success">
@{
string text = "Thread CurrentUICulture is [" +
@Thread.CurrentThread.CurrentUICulture.ToString() + "] ; ";
text += "Thread CurrentCulture is [" +
@Thread.CurrentThread.CurrentCulture.ToString() + "]";
}
View Thread Culture: <br />
@text
</p>
<p class="text-primary">
Localized In View By ResourceManager: <br />
@SharedResources04.Resources.SharedResource.Wellcome
</p>
<p class="text-primary">
Localized In View By ResourceManager: <br />
@SharedResources04.Resources.SharedResource.Hello_World
</p>
<p class="text-primary">
Localized In View By IStringLocalizer: <br />
@StringLocalizer["Wellcome"]
</p>
<p class="text-primary">
Localized In View By IStringLocalizer: <br />
@StringLocalizer["Hello World"]
</p>
</div>
}
4.7 执行结果
执行结果如下所示
请注意,我在页脚添加了一些调试信息,以显示 **请求语言 Cookie** 的值,以查看应用程序是否按预期工作。
5. 完整代码
由于大多数人都喜欢可以直接复制粘贴的代码,所以这里是应用程序的完整代码。
//Program.cs===========================================================================
namespace SharedResources04
{
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);
});
}
private static void AddingMultiLanguageSupport(WebApplication? app)
{
app?.UseRequestLocalization();
}
}
}
//SharedResource.cs===================================================
namespace SharedResources04
{
/*
* 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:
* IStringLocalizer<SharedResources04.SharedResource> StringLocalizer
*
* 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
{
}
}
//HomeController.cs================================================================
namespace SharedResources04.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly IStringLocalizer<SharedResource> _stringLocalizer;
/* Here is, of course, the Dependency Injection (DI) coming in and filling
* all the dependencies. The key thing is we are asking for a specific
* type=SharedResource.
* If it doesn't work for you, you can try to use full class name
* in your DI instruction, like this one:
* IStringLocalizer<SharedResources04.SharedResource> stringLocalizer
*/
public HomeController(ILogger<HomeController> logger,
IStringLocalizer<SharedResource> stringLocalizer)
{
_logger = logger;
_stringLocalizer = stringLocalizer;
}
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
CookieOptions cookieOptions = new CookieOptions();
cookieOptions.Expires = DateTimeOffset.UtcNow.AddMonths(1);
cookieOptions.IsEssential = true;
myContext.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
cookieOptions
);
}
public IActionResult LocalizationExample(LocalizationExampleViewModel model)
{
string text = "Thread CurrentUICulture is [" +
@Thread.CurrentThread.CurrentUICulture.ToString() + "] ; ";
text += "Thread CurrentCulture is
[" + @Thread.CurrentThread.CurrentCulture.ToString() + "]";
model.ThreadCultureInController = text;
//here we test localization by Resource Manager
model.LocalizedInControllerByResourceManager1 =
Resources.SharedResource.Wellcome;
model.LocalizedInControllerByResourceManager2 =
Resources.SharedResource.Hello_World;
//here we test localization by IStringLocalizer
model.LocalizedInControllerByIStringLocalizer1 =
_stringLocalizer["Wellcome"];
model.LocalizedInControllerByIStringLocalizer2 =
_stringLocalizer["Hello World"];
return View(model);
}
public IActionResult Error()
{
return View(new ErrorViewModel
{ RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}
//ChangeLanguageViewModel.cs=====================================================
namespace SharedResources04.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 SharedResources04.Models.Home
{
public class LocalizationExampleViewModel
{
public string? LocalizedInControllerByResourceManager1 { get; set; }
public string? LocalizedInControllerByResourceManager2 { get; set; }
public string? LocalizedInControllerByIStringLocalizer1 { get; set; }
public string? LocalizedInControllerByIStringLocalizer2 { get; set; }
public string? ThreadCultureInController { 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
@using SharedResources04
@model LocalizationExampleViewModel
@* Here is of course the Dependency Injection (DI) coming in and filling
all the dependencies. The key thing is we are asking for a specific
type=SharedResource.
If it doesn't work for you, you can try to use full class name
in your DI instruction, like this one:
@inject IStringLocalizer<SharedResources04.SharedResource> StringLocalizer
*@
@inject IStringLocalizer<SharedResource> StringLocalizer
@{
<div style="width:600px">
<p class="text-success">
Controller Thread Culture: <br />
@Model.ThreadCultureInController
</p>
<p class="text-primary">
Localized In Controller By ResourceManager: <br />
@Model.LocalizedInControllerByResourceManager1
</p>
<p class="text-primary">
Localized In Controller By ResourceManager: <br />
@Model.LocalizedInControllerByResourceManager2
</p>
<p class="text-primary">
Localized In Controller By IStringLocalizer: <br />
@Model.LocalizedInControllerByIStringLocalizer1
</p>
<p class="text-primary">
Localized In Controller By IStringLocalizer: <br />
@Model.LocalizedInControllerByIStringLocalizer2
</p>
<p class="text-success">
@{
string text = "Thread CurrentUICulture is [" +
@Thread.CurrentThread.CurrentUICulture.ToString() + "] ; ";
text += "Thread CurrentCulture is [" +
@Thread.CurrentThread.CurrentCulture.ToString() + "]";
}
View Thread Culture: <br />
@text
</p>
<p class="text-primary">
Localized In View By ResourceManager: <br />
@SharedResources04.Resources.SharedResource.Wellcome
</p>
<p class="text-primary">
Localized In View By ResourceManager: <br />
@SharedResources04.Resources.SharedResource.Hello_World
</p>
<p class="text-primary">
Localized In View By IStringLocalizer: <br />
@StringLocalizer["Wellcome"]
</p>
<p class="text-primary">
Localized In View By IStringLocalizer: <br />
@StringLocalizer["Hello World"]
</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. 历史
- 2024 年 3 月 22 日:初始版本