ASP.NET 8 – 多语言应用程序与单一 Resx 文件 – 第三部分 – 表单验证字符串
构建多语言 ASP.NET 8 MVC 应用程序的实用指南
1. 多语言表单验证错误字符串
在 ASP.NET MVC 应用程序中处理表单验证错误字符串的本地化,或所谓的“数据注释本地化”,是一项独立的工作。这也是本文的重点。
2. 本系列其他文章
本系列文章包括:
- ASP.NET 8 – 多语言应用程序(单 Resx 文件)
- ASP.NET 8 – 多语言应用程序(单 Resx 文件)– 第 2 部分 – 替代方法
- ASP.NET 8 – 多语言应用程序(单 Resx 文件)– 第 3 部分 – 表单验证字符串
- ASP.NET8 – 多语言应用程序与单一 Resx 文件 – 第四部分 – 资源管理器
3. 共享资源方法
默认情况下,ASP.NET Core 8 MVC 技术为每个控制器和视图预留了单独的资源文件 .resx。但大多数人不喜欢这种方式,因为应用程序中不同地方的大部分多语言字符串都是相同的,我们希望将它们集中在一起。文献 [1] 将这种方法称为“共享资源”方法。为了实现这一点,我们将创建一个标记类 SharedResources.cs 来 agrup(分组)所有资源。然后,在我们的应用程序中,我们将通过依赖注入 (DI) 调用该特定类/类型,而不是特定的控制器/视图。这是微软文档 [1] 中提到的一种小技巧,在 StackOverflow 文章 [6] 中曾引起困惑。我们计划在这里将其阐明。虽然 [1] 中已经解释了一切,但需要一些实际的例子,就像我们这里提供的例子一样。
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)
.AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) =>
factory.Create(typeof(SharedResource));
});
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 SharedResources03
{
/*
* 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: SharedResources03.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.3 创建语言资源文件
在“Resources”文件夹中,创建你的语言资源文件,并确保将它们命名为 SharedResources.xx.resx。
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 使用数据注释 – 字段验证
在您的模型类中,设置带有需要本地化为适当字符串的验证属性。
//LocalizationExampleViewModel.cs===============================================
namespace SharedResources03.Models.Home
{ public class LocalizationExampleViewModel
{
/* It is these field validation error messages
* that are focus of this example. We want to
* be able to present them in multiple languages
*/
//model
[Required(ErrorMessage = "The UserName field is required.")]
[Display(Name = "UserName")]
public string? UserName { get; set; }
[EmailAddress(ErrorMessage = "The Email field is not a valid email address.")]
[Display(Name = "Email")]
public string? Email { get; set; }
public bool IsSubmit { get; set; } = false;
}
}
对于控制器中的模型级验证,我们将使用经典的 IStringLocalizer
。
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly IStringLocalizer<SharedResource> _stringLocalizer;
public HomeController(ILogger<HomeController> logger,
IStringLocalizer<SharedResource> stringLocalizer)
{
_logger = logger;
_stringLocalizer = stringLocalizer;
}
//--------------------------
public IActionResult LocalizationExample(LocalizationExampleViewModel model)
{
if(model.IsSubmit)
{
if (!ModelState.IsValid)
{
ModelState.AddModelError
("", _stringLocalizer["Please correct all errors and submit again"]);
}
}
else
{
ModelState.Clear();
}
return View(model);
}
4.6 同步 ASP.NET CSS 错误类与 Bootstrap CSS 类
ASP.NET 会为带有错误的表单字段添加 CSS 类 .input-validation-error
。但是,Bootstrap 不知道如何处理该 CSS 类,因此需要将该类映射到 Bootstrap 可识别的 CSS 类,即 CSS 类 .is-invalid
。
这就是这里提供的 JavaScript 代码的目的。当然,我们会在 DOMContentLoaded
事件上进行挂载,并进行 CSS 类的映射。
这一切都是为了同步 ASP.NET CSS 错误类与 Bootstrap CSS 类,以便 Bootstrap 将错误输入元素的边框标记为红色。
最终结果是,表单控件上的红线标记了无效字段。
@* _ValidationClassesSyncBetweenAspNetAndBootstrap.cshtml===================== *@
@*
All this is to sync Asp.Net CSS error classes with Bootstrap CSS classes to
mark error input elements border to red by Bootstrap.
Asp.Net will add CSS class .input-validation-error to form a field with an error.
But, Bootstrap does not know what to do with that CSS class, so that class
needs to be mapped to CSS class that Bootstrap understands, and that is
CSS class .is-invalid.
That is the purpose of this JavaScript code that is here. Of course, we hook
to DOMContentLoaded event and do the mapping of CSS classes.
The final result is that the red line on the form control marking an invalid field.
*@
<script type="text/javascript">
window.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll("input.input-validation-error")
.forEach((elem) => { elem.classList.add("is-invalid"); }
);
});
</script>
4.7 包含字段和模型验证消息的示例视图
这是我们的示例视图
@* LocalizationExample.cshtml ====================================================*@
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.Extensions.Localization
@model LocalizationExampleViewModel
@{
<partial name="_ValidationClassesSyncBetweenAspNetAndBootstrap" />
<div style="width:500px ">
<fieldset class="border rounded-3 p-3 bg-info">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<form id="formlogin" novalidate>
<div class="form-group">
<label asp-for="UserName"></label>
<div>
<span asp-validation-for="UserName" class="text-danger"></span>
</div>
<input class="form-control" asp-for="UserName" />
</div>
<div class="form-group">
<label asp-for="Email"></label>
<div>
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<input class="form-control" asp-for="Email" />
</div>
<input type="hidden" name="IsSubmit" value="true">
<button type="submit" class="btn btn-primary mt-3 float-end"> Submit
</button>
</form>
</fieldset>
</div>
}
请注意,我们在 form
元素中使用了 novalidate
属性来抑制浏览器内置的、未本地化的验证弹窗。
因此,有三种可能的验证级别:
- 服务器端验证 – 在本示例中使用,并且是本地化的(多语言)
- 客户端验证 – 在 ASP.NET 中,可以通过使用 jquery.validate.unobtrusive.min.js 来启用,但我们在本示例中没有使用它。
- 浏览器集成验证 – 在本示例中通过使用
novalidate
属性禁用,因为它不是本地化的,并且始终是英文。
如果您不设置 novalidate
属性,浏览器将弹出其验证对话框,您将看到如下屏幕上的消息。对用户来说,看到多个不同的消息可能会令人困惑。
4.8 执行结果
执行结果如下所示
请注意,我在页脚添加了一些调试信息,以显示 **请求语言 Cookie** 的值,以查看应用程序是否按预期工作。
5. 完整代码
由于大多数人都喜欢可以直接复制粘贴的代码,所以这里是应用程序的完整代码。
//Program.cs===========================================================================
namespace SharedResources03
{
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)
.AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) =>
factory.Create(typeof(SharedResource));
});
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 SharedResources03
{
/*
* 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: SharedResources03.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
{
}
}
//ChangeLanguageViewModel.cs=====================================================
namespace SharedResources03.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 SharedResources03.Models.Home
{ public class LocalizationExampleViewModel
{
/* It is these field validation error messages
* that are focus of this example. We want to
* be able to present them in multiple languages
*/
//model
[Required(ErrorMessage = "The UserName field is required.")]
[Display(Name = "UserName")]
public string? UserName { get; set; }
[EmailAddress(ErrorMessage = "The Email field is not a valid email address.")]
[Display(Name = "Email")]
public string? Email { get; set; }
public bool IsSubmit { get; set; } = false;
}
}
//HomeController.cs================================================================
namespace SharedResources03.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly IStringLocalizer<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)
{
if(model.IsSubmit)
{
if (!ModelState.IsValid)
{
ModelState.AddModelError
("", _stringLocalizer["Please correct all errors and submit again"]);
}
}
else
{
ModelState.Clear();
}
return View(model);
}
public IActionResult Error()
{
return View(new ErrorViewModel
{ RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}
@* _ValidationClassesSyncBetweenAspNetAndBootstrap.cshtml===================== *@
@*
All this is to sync Asp.Net CSS error classes with Bootstrap CSS classes to
mark error input elements border to red by Bootstrap.
Asp.Net will add CSS class .input-validation-error to form a field with an error.
But, Bootstrap does not know what to do with that CSS class, so that class
needs to be mapped to CSS class that Bootstrap understands, and that is
CSS class .is-invalid.
That is the purpose of this JavaScript code that is here. Of course, we hook
to DOMContentLoaded event and do the mapping of CSS classes.
The final result is that the red line on the form control marking an invalid field.
*@
<script type="text/javascript">
window.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll("input.input-validation-error")
.forEach((elem) => { elem.classList.add("is-invalid"); }
);
});
</script>
@* 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
@{
<partial name="_ValidationClassesSyncBetweenAspNetAndBootstrap" />
<div style="width:500px ">
<fieldset class="border rounded-3 p-3 bg-info">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<form id="formlogin" novalidate>
<div class="form-group">
<label asp-for="UserName"></label>
<div>
<span asp-validation-for="UserName" class="text-danger"></span>
</div>
<input class="form-control" asp-for="UserName" />
</div>
<div class="form-group">
<label asp-for="Email"></label>
<div>
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<input class="form-control" asp-for="Email" />
</div>
<input type="hidden" name="IsSubmit" value="true">
<button type="submit" class="btn btn-primary mt-3 float-end">
Submit</button>
</form>
</fieldset>
</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月16日:初始版本