입력 유효성 검사 중에 표시되는 기본 메시지에 대한 다국어 지원

페이지 생성 날짜 :

환경

ASP.NET 코어
  • 5.0 MVC

처음에

이 팁이 있는 기본 입력 유효성 검사 메시지의 다국어 지원은 완료되지 않을 수 있습니다. 누락 된 것들을 처리하십시오.

또한 세션 상태 또는 캐시 상태에 따라 언어를 변경해도 이전 언어의 텍스트에 나타날 수 있습니다.

기본 입력 유효성 검사 메시지를 변경하는 방법에는 여러 가지가 있으며, 각 메시지를 결합할 수 있습니다. 그것은 그들 중 하나가 아니므로 안정적으로 변경하려면 모든 패턴에 대응해야합니다.

전제

이 팁은 다음 팁을 이해하는 것으로 작성되었습니다.

또한 새 프로젝트를 만드는 경우 위의 팁을 기반으로 다음 파일과 코드를 추가해야 합니다.

  • 공유리소스.resx(+en, es) 파일을 만듭니다. (이 팁의 메시지만 번역되므로 내용이 비어 있을 수 있습니다.)
  • 공유 리소스.cs 파일 만들기
  • 시작에 지역화 코드 추가.ConfigureServices
  • 시작에 지역화 코드 추가.구성
  • 추가된 UserViewModel(이번에는 여러 언어로 기본 메시지를 명시적으로 키키하지 않습니다).
  • 사용자가 만든 화면 작업 및 보기 추가(Create.cshtml)

코드 시작

위의 가정에 따라 각 코드는 다음과 같습니다.

스타트업.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Globalization;

namespace LocalizationDefaultValidation
{
  public class Startup
  {
    // 省略

    public void ConfigureServices(IServiceCollection services)
    {
      services.AddControllersWithViews();

      services.AddMvc()
        // ローカライズに必要。Resx ファイルのフォルダパスを指定
        .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix, opts => { opts.ResourcesPath = "Resources"; })
        // DataAnnotations のローカライズに必要
        .AddDataAnnotationsLocalization(options =>
        {
          // DataAnnotation を使ったときのメッセージは SharedResource に集約する
          options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(SharedResource));
        });
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
      if (env.IsDevelopment())
      {
        app.UseDeveloperExceptionPage();
      }
      else
      {
        app.UseExceptionHandler("/Home/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
      }

      // 標準の機能で切り替えたい言語を定義します。
      var supportedCultures = new[]
      {
        new CultureInfo("ja"),
        new CultureInfo("en"),
        new CultureInfo("es"),
      };

      // 標準の言語切り替え機能を有効にします。対応しているのは「クエリ文字列」「Cookie」「Accept-Language HTTP ヘッダー」です。
      app.UseRequestLocalization(new RequestLocalizationOptions
      {
        DefaultRequestCulture = new RequestCulture("ja"),
        SupportedCultures = supportedCultures,
        SupportedUICultures = supportedCultures
      });

      app.UseHttpsRedirection();
      app.UseStaticFiles();

      // 省略
    }
  }
}
홈 컨트롤러.cs
using LocalizationDefaultValidation.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;

namespace LocalizationDefaultValidation.Controllers
{
  public class HomeController : Controller
  {
    // 省略

    public IActionResult Create()
    {
      SetDayOfWeeksViewData();
      return View();
    }

    [HttpPost]
    public IActionResult Create(UserViewModel model)
    {
      SetDayOfWeeksViewData();

      // エラーなら差し戻し
      if (ModelState.IsValid == false) return View(model);

      // 正常に登録できた場合は Index に戻る
      return RedirectToAction(nameof(Index));
    }

    /// <summary>曜日の一覧を ViewData に設定します。</summary>
    private void SetDayOfWeeksViewData()
      => ViewData["DayOfWeeks"] = ((DayOfWeek[])Enum.GetValues(typeof(DayOfWeek))).Select(x => new SelectListItem(x.ToString(), ((int)x).ToString()));
  }
}
인덱스.cshtml
@{
  ViewData["Title"] = "Home Page";
}

<div class="text-center">
  <h1 class="display-4">Welcome</h1>
  <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

<p>ユーザー作成画面に遷移します。リンクごとに選択した言語で表示できます。</p>
<ul>
  <li><a asp-action="Create">Create</a></li>
  <li><a asp-action="Create" asp-route-culture="ja">Create (ja)</a></li>
  <li><a asp-action="Create" asp-route-culture="en">Create (en)</a></li>
  <li><a asp-action="Create" asp-route-culture="es">Create (es)</a></li>
</ul>
만들기.cshtml
@model LocalizationDefaultValidation.Models.UserViewModel
@using System.Globalization;
@{
  ViewData["Title"] = "Create";
}

<h1>Create</h1>

<h4>UserViewModel</h4>
<hr />
<div class="row">
  <div class="col-md-4">
    <form asp-action="Create" enctype="multipart/form-data" >
      <div asp-validation-summary="ModelOnly" class="text-danger"></div>
      <div class="form-group">
        <label asp-for="ID" class="control-label"></label>
        <input asp-for="ID" class="form-control" />
        <span asp-validation-for="ID" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="Name" class="control-label"></label>
        <input asp-for="Name" class="form-control" />
        <span asp-validation-for="Name" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="Password" class="control-label"></label>
        <input asp-for="Password" class="form-control" />
        <span asp-validation-for="Password" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="ConfirmPassword" class="control-label"></label>
        <input asp-for="ConfirmPassword" class="form-control" />
        <span asp-validation-for="ConfirmPassword" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="Email" class="control-label"></label>
        <input asp-for="Email" class="form-control" />
        <span asp-validation-for="Email" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="Age" class="control-label"></label>
        <input asp-for="Age" class="form-control" />
        <span asp-validation-for="Age" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="Gender"></label>
        <div>
          <label><input type="radio" asp-for="Gender" value="@(GenderType.None)" />@(GenderType.None)</label>
          <label><input type="radio" asp-for="Gender" value="@(GenderType.Man)" />@(GenderType.Man)</label>
          <label><input type="radio" asp-for="Gender" value="@(GenderType.Woman)" />@(GenderType.Woman)</label>
        </div>
        <span asp-validation-for="Gender" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="Birthday" class="control-label"></label>
        <input asp-for="Birthday" class="form-control" />
        <span asp-validation-for="Birthday" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="Phone" class="control-label"></label>
        <input asp-for="Phone" class="form-control" />
        <span asp-validation-for="Phone" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="PostalCode" class="control-label"></label>
        <input asp-for="PostalCode" class="form-control" />
        <span asp-validation-for="PostalCode" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="CreditCard" class="control-label"></label>
        <input asp-for="CreditCard" class="form-control" />
        <span asp-validation-for="CreditCard" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="Money" class="control-label"></label>
        <input asp-for="Money" class="form-control" />
        <span asp-validation-for="Money" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="StartDateTime" class="control-label"></label>
        <input asp-for="StartDateTime" class="form-control" />
        <span asp-validation-for="StartDateTime" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="WakeUpTime" class="control-label"></label>
        <input asp-for="WakeUpTime" class="form-control" />
        <span asp-validation-for="WakeUpTime" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="Homepage" class="control-label"></label>
        <input asp-for="Homepage" class="form-control" />
        <span asp-validation-for="Homepage" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="MyImage" class="control-label"></label>
        <input asp-for="MyImage" class="form-control" />
        <span asp-validation-for="MyImage" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="MyColor" class="control-label"></label>
        <input asp-for="MyColor" class="form-control" type="color" />
        <span asp-validation-for="MyColor" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="WorkingDays" class="control-label"></label>
        <select asp-for="WorkingDays" class="form-control" asp-items="@((IEnumerable<SelectListItem>)ViewData["DayOfWeeks"])" multiple></select>
        <span asp-validation-for="WorkingDays" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="VacationDay" class="control-label"></label>
        <select asp-for="VacationDay" class="form-control" asp-items="@((IEnumerable<SelectListItem>)ViewData["DayOfWeeks"])" multiple></select>
        <span asp-validation-for="VacationDay" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="Comment" class="control-label"></label>
        <textarea asp-for="Comment" class="form-control"></textarea>
        <span asp-validation-for="Comment" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="FileName" class="control-label"></label>
        <input asp-for="FileName" class="form-control" />
        <span asp-validation-for="FileName" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="UploadFile" class="control-label"></label>
        <input asp-for="UploadFile" type="file" multiple />
        <span asp-validation-for="UploadFile" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="Month" class="control-label"></label>
        <input asp-for="Month" class="form-control" type="month" />
        <span asp-validation-for="Month" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="Search" class="control-label"></label>
        <input asp-for="Search" class="form-control" type="search" />
        <span asp-validation-for="Search" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="Range" class="control-label"></label>
        <input asp-for="Range" class="form-control" type="range" min="10" max="100" />
        <span asp-validation-for="Range" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="Week" class="control-label"></label>
        <input asp-for="Week" class="form-control" type="week" />
        <span asp-validation-for="Week" class="text-danger"></span>
      </div>
      <div class="form-group form-check">
        <label class="form-check-label">
          <input class="form-check-input" asp-for="IsAccepted" /> @Html.DisplayNameFor(model => model.IsAccepted)
        </label>
      </div>
      <div class="form-group">
        <input type="submit" value="Create" class="btn btn-primary" asp-route-culture="@CultureInfo.CurrentCulture.Name" />
      </div>
    </form>
  </div>
</div>

<div>
  <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
  @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
사용자뷰모델.cs
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace LocalizationDefaultValidation.Models
{
  public class UserViewModel
  {
    [Required]
    [Display(Name = "ID (半角英数字)")]
    [StringLength(20)]
    [RegularExpression(@"^[0-9a-zA-Z]*$")]
    public string ID { get; set; }

    [StringLength(50)]
    public string Name { get; set; }

    [StringLength(50, MinimumLength = 8)]
    [DataType(DataType.Password)]
    [RegularExpression(@"^[0-9a-zA-Z]*$")]
    public string Password { get; set; }

    [StringLength(50, MinimumLength = 8)]
    [DataType(DataType.Password)]
    [Compare(nameof(Password))]
    public string ConfirmPassword { get; set; }

    [EmailAddress]
    [DataType(DataType.EmailAddress)] // スキャフォールディングするときはコメントアウトする
    public string Email { get; set; }

    //[Int]
    [Range(0, 150)]
    public int Age { get; set; }

    [Required]
    [EnumDataType(typeof(GenderType))]
    public GenderType Gender { get; set; }

    [DataType(DataType.Date)]
    public DateTime Birthday { get; set; }

    [Phone()]
    [DataType(DataType.PhoneNumber)] // スキャフォールディングするときはコメントアウトする
    public string Phone { get; set; }

    [DataType(DataType.PostalCode)]
    public string PostalCode { get; set; }

    [CreditCard()]
    [DataType(DataType.CreditCard)] // スキャフォールディングするときはコメントアウトする
    public string CreditCard { get; set; }

    [DataType(DataType.Currency)]
    public decimal Money { get; set; }

    [DataType(DataType.DateTime)]
    public DateTime StartDateTime { get; set; }

    [DataType(DataType.Time)]
    public TimeSpan WakeUpTime { get; set; }

    [Url]
    [DataType(DataType.Url)] // スキャフォールディングするときはコメントアウトする
    public string Homepage { get; set; }

    [Url]
    [DataType(DataType.ImageUrl)] // スキャフォールディングするときはコメントアウトする
    public string MyImage { get; set; }

    public string MyColor { get; set; }

    [MaxLength(5)]
    public DayOfWeek[] WorkingDays { get; set; }

    [MinLength(3)]
    public DayOfWeek[] VacationDay { get; set; }

    [StringLength(200)]
    [DataType(DataType.MultilineText)]
    public string Comment { get; set; }

    [Display(Name = "ファイル名 (.png)")]
    [FileExtensions(Extensions = "png")]
    public string FileName { get; set; }

    [DataType(DataType.Upload)]
    public List<IFormFile> UploadFile { get; set; }

    public DateTime Month { get; set; }

    public string Search { get; set; }

    [Range(10, 100)]
    public int Range { get; set; }

    public string Week { get; set; }

    [Required]
    public bool IsAccepted { get; set; }
  }

  public enum GenderType
  {
    None,
    Man,
    Woman,
    Other,
  }
}
공유리소스.cs
namespace LocalizationDefaultValidation
{
  // クラス名は作成した .resx のファイル名と同じにする必要がある
  public class SharedResource { }
}

모델 바인딩 메시지

모델 속성 유형을 int 뷰에 설정하거나 DateTime 바인딩하고 형식이 지정되지 않은 등록을 하면 '값'이 잘못되었습니다."와 같은 메시지가 표시됩니다. 이러한 메시지는 모델에서 빈 int 문자열을 바인딩할 수 없을 때 나타납니다. 바인딩할 수 없는 타이밍이기 때문에 다른 값 유효성 검사 및 서버 측 처리 전에만 발생합니다.

이러한 메시지는 DefaultModelBindingMessageProvider 로 정의됩니다.

Startup.cs 시작부터 정의된 메서드의 services.AddControllersWithViews 인수에 추가한 다음 Action 를 추가합니다. 인수에서 전달된 것을 설정하여 다국어가 될 수 options ModelBindingMessageProvider 있습니다.

설정할 수 있는 메시지유형은 11가지가 있으므로 먼저 다음과 같이 메시지를 정의해야 합니다. SharedResource.resx 키 이름은 선택 사항입니다. 주석 열의 기본 영어 메시지는(주석을 포함할 필요가 없습니다)입니다. 각 언어를 번역할 필요가 없으므로 자체 번역을 수행해야 합니다(샘플 코드에는 번역된 SharedResource.resx도 포함됩니다).

이름 주석
ModelBinding_AttemptedValueIsInvalid '{0}'는 {1} 잘못된 값입니다. '{0} 값'은 {1} 유효하지 않습니다.
ModelBinding_MissingBindRequiredValue {0} 대한 값은 지정되지 않습니다. '{0}' 매개 변수 또는 속성에 대한 값은 제공되지 않았습니다.
ModelBinding_MissingKeyOrValue 필수. 값이 필요합니다.
ModelBinding_MissingRequestBodyRequiredValue 요청에는 본문이 있어야 합니다. 비어 있는 요청 본문이 필요합니다.
ModelBinding_NonPropertyAttemptedValueIsInvalid '{0}'는 유효하지 않습니다. '{0} 값'이 잘못되었습니다.
ModelBinding_NonPropertyUnknownValueIsInvalid 값이 잘못되었습니다. 제공된 값은 유효하지 않습니다.
ModelBinding_NonPropertyValueMustBeANumber 번호를 지정해야 합니다. 필드는 숫자여야 합니다.
ModelBinding_UnknownValueIsInvalid {0} 값은 유효하지 않습니다. 제공된 값은 {0} 유효하지 않습니다.
ModelBinding_ValueIsInvalid '{0}'는 유효하지 않습니다. '{0}' 값이 잘못되었습니다.
ModelBinding_ValueMustBeANumber {0} 숫자여야 합니다. 필드 {0} 숫자여야 합니다.
ModelBinding_ValueMustNotBeNull 필요한 입력. '{0}' 값이 잘못되었습니다.

다음과 같이 시작 .cs 수정합니다.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Localization;
using System;
using System.Globalization;
using System.Reflection;

namespace LocalizationDefaultValidation
{
  public class Startup
  {
    // 省略 (初期コード)

    /// <summary>
    /// 検証メッセージローカライズで使用。
    /// </summary>
    private IServiceProvider ServiceProvider { get; set; }

    private IStringLocalizer _localizer = null;
    /// <summary>
    /// 検証メッセージローカライズで使用。
    /// </summary>
    private IStringLocalizer Localizer
      => _localizer ?? (_localizer = ServiceProvider.GetService<IStringLocalizerFactory>()
               .Create(nameof(SharedResource), new AssemblyName(typeof(SharedResource).Assembly.FullName).Name));

    // このメソッドはランタイムによって呼び出されます。 このメソッドを使用して、コンテナーにサービスを追加します。
    public void ConfigureServices(IServiceCollection services)
    {
      services.AddControllersWithViews(options =>
      {
        // 検証メッセージのローカライズで使用
        // モデルバインディング失敗時のエラーメッセージをカスタマイズ
        // サーバ側でモデルに値を格納する際に発生する可能性がある
        
        // メッセージの {0} や {1} を置換するための関数を定義
        static string f1(string f, string a1) => string.Format(f, a1);
        static string f2(string f, string a1, string a2) => string.Format(f, a1, a2);

        // 各メソッドを読んでメッセージを置き換えます
        var mp = options.ModelBindingMessageProvider;
        mp.SetAttemptedValueIsInvalidAccessor((x, y) => f2(Localizer["ModelBinding_AttemptedValueIsInvalid"], x, y));
        mp.SetMissingBindRequiredValueAccessor((x) => f1(Localizer["ModelBinding_MissingBindRequiredValue"], x));
        mp.SetMissingKeyOrValueAccessor(() => Localizer["ModelBinding_MissingKeyOrValue"]);
        mp.SetMissingRequestBodyRequiredValueAccessor(() => Localizer["ModelBinding_MissingRequestBodyRequiredValue"]);
        mp.SetNonPropertyAttemptedValueIsInvalidAccessor((x) => f1(Localizer["ModelBinding_NonPropertyAttemptedValueIsInvalid"], x));
        mp.SetNonPropertyUnknownValueIsInvalidAccessor(() => Localizer["ModelBinding_NonPropertyUnknownValueIsInvalid"]);
        mp.SetNonPropertyValueMustBeANumberAccessor(() => Localizer["ModelBinding_NonPropertyValueMustBeANumber"]);
        mp.SetUnknownValueIsInvalidAccessor((x) => f1(Localizer["ModelBinding_UnknownValueIsInvalid"], x));
        mp.SetValueIsInvalidAccessor((x) => f1(Localizer["ModelBinding_ValueIsInvalid"], x));
        mp.SetValueMustBeANumberAccessor((x) => f1(Localizer["ModelBinding_ValueMustBeANumber"], x));
        mp.SetValueMustNotBeNullAccessor((x) => f1(Localizer["ModelBinding_ValueMustNotBeNull"], x));
      });

      services.AddMvc()
        // ローカライズに必要。Resx ファイルのフォルダパスを指定
        .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix, opts => { opts.ResourcesPath = "Resources"; })
        // DataAnnotations のローカライズに必要
        .AddDataAnnotationsLocalization(options =>
        {
          // DataAnnotation を使ったときのメッセージは SharedResource に集約する
          options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(SharedResource));
        });
    }

    // このメソッドはランタイムによって呼び出されます。 このメソッドを使用して、HTTP要求パイプラインを構成します。
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
      // ローカライズで使用するため IServiceProvider をプロパティに保持しておきます。
      ServiceProvider = app.ApplicationServices;

      // 標準の機能で切り替えたい言語を定義します。
      var supportedCultures = new[]
      {
        new CultureInfo("ja"),
        new CultureInfo("en"),
        new CultureInfo("es"),
      };

      // 標準の言語切り替え機能を有効にします。対応しているのは「クエリ文字列」「Cookie」「Accept-Language HTTP ヘッダー」です。
      app.UseRequestLocalization(new RequestLocalizationOptions
      {
        DefaultRequestCulture = new RequestCulture("ja"),
        SupportedCultures = supportedCultures,
        SupportedUICultures = supportedCultures
      });

      // 省略 (初期コード)
    }
  }
}

요점은 받는 services.AddControllersWithViews 메서드에 a를 추가하는 option Action 것입니다. options.ModelBindingMessageProvider 각 Set 메서드에 대해 지역화된 텍스트를 설정합니다.

번역된 텍스트는 IStringLocalizer 에서 검색됩니다. 라운드 코드의 비트인 직접 코드를 받을 수 IStringLocalizer 있는 방법은 없습니다. SharedResource.resx 코드를 생성하는 경우 로컬화된 값을 직접 생성합니다.

IStringLocalizer 지정한 키는 SharedResource.resx 추가한 키를 지정합니다. 각 집합 메서드의 내용과 일치하도록 설정합니다.

또한 각 메시지에는 형식 문자열 인수가 있으므로 {0}{1} 수신된 값을 대체할 수 있도록 Simple을 string.Format Func 사용합니다.

기본 유효성 검사 메시지 지역화

모델 속성의 특성이 프레임워크에 의해 결정되고 기본적으로 영어로 설정된 기본 오류 메시지가 Required StringLength 표시됩니다.

IValidationAttributeAdapterProvider인터페이스에서 파생된 클래스를 정의하여 지역화할 수 있습니다.

우선 SharedResource.resx 다국어인 텍스트를 등록합니다. 키의 이름은 임의이지만 프로그램을 단순화하려면 "Validator_< 유효성 검사 특성 이름>"의 형태로 등록합니다.

이름 주석
Validator_CompareAttribute {0} {1} 일치하지 않습니다. '{0}'과 '{1}'는 일치하지 않습니다.
Validator_CreditCardAttribute {0} 유효한 카드 번호가 아닙니다. {0} 필드는 유효한 신용 카드 번호가 아닙니다.
Validator_DataTypeAttribute_Date 유효한 날짜를 입력합니다. 유효한 날짜를 입력하십시오.
Validator_EmailAddressAttribute {0} 유효한 이메일 주소가 아닙니다. {0} 필드는 유효한 전자 메일 주소가 아닙니다.
Validator_FileExtensionsAttribute {0} 다음 확장이 있는 파일만 허용합니다. : {1} {0} 필드는 다음 확장된 파일만 허용합니다: {1}
Validator_MaxLengthAttribute {0} 최대 길이의 '{1}'을 가진 문자열 또는 배열 유형이어야 합니다. 필드 {0} 최대 길이의 문자열 또는 배열 유형이어야 합니다 {1}.
Validator_MinLengthAttribute {0} 최소 길이의 '{1}'을 가진 문자열 또는 배열 유형이어야 합니다. 필드 {0} '{1}'의 최소 길이가 있는 문자열 또는 배열 유형이어야 합니다.
Validator_PhoneAttribute {0} 유효한 전화 번호가 아닙니다. {0} 필드는 유효한 전화 번호가 아닙니다.
Validator_RangeAttribute {0} {1} {2} 범위여야 합니다. 필드 {0} {1} {2} 사이여야 합니다.
Validator_RegularExpressionAttribute {0} 정규식 '{1}'과 일치해야 합니다. 필드 {0} 정규 식 '{1}'과 일치해야 합니다.
Validator_RequiredAttribute {0} 필요합니다. {0} 필드가 필요합니다.
Validator_StringLengthAttribute {0} {1} 자리 내에 있어야 합니다. 필드 {0} 최대 {1} 길이의 문자열이어야 합니다.
Validator_UrlAttribute {0} 유효한 URL이 아닙니다. {0} 필드는 유효한 정규화된 http, https 또는 ftp URL이 아닙니다.
Validator_StringLengthAttributeWithMin {0} 최소한 {2} {1} 숫자여야 합니다. 필드 {0} 최대 {1} 최소 {2} 문자열이어야 합니다.

다음으로 다음 어댑터공급자 클래스를 만듭니다.

클래스 이름은 임의적이지만 이번에는 CustomValidationAttributeAdapterProvider 호출된 클래스를 작성하고 다음과 같이 코드를 작성합니다. 코드 파일의 위치는 임의이지만 샘플은 어댑터라는 폴더에 배치됩니다.

using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization;
using System;
using System.ComponentModel.DataAnnotations;

namespace LocalizationDefaultValidation
{
  public class CustomValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
  {
    /// <summary>コード簡略化のためのリソースキー命名規則プリフィックス</summary>
    private const string RESOURCE_KEY_PREFIX = "Validator_";

    private readonly IValidationAttributeAdapterProvider _fallback = new ValidationAttributeAdapterProvider();

    /// <summary>
    /// 指定された ValidationAttribute の IAttributeAdapter を返します。
    /// </summary>
    /// <param name="attribute">IAttributeAdapter を作成するための ValidationAttribute。</param>
    /// <param name="stringLocalizer">メッセージの作成に使用される IStringLocalizer。</param>
    /// <returns>指定された属性の IAttributeAdapter。</returns>
    IAttributeAdapter IValidationAttributeAdapterProvider.GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer stringLocalizer)
    {
      // すでにエラーメッセージが設定されている場合はそれを使用するのでここでは何も設定しない
      if (attribute.ErrorMessageResourceName != null) return _fallback.GetAttributeAdapter(attribute, stringLocalizer);

      // attribute には「Required」や「StringLength」などが設定されています
      Type attrType = attribute.GetType();
      
      // プリフィックスと属性のクラス名からローカライズ用のキーを生成します
      var key = RESOURCE_KEY_PREFIX + attrType.Name;

      // IStringLocalizer から指定したキーでローカライズされたテキストを取得します
      // ない場合は getString にそのまま key の値が入ります
      var getString = stringLocalizer[key];

      // 正しくローカライズされたテキストが取得できた場合は ErrorMessage に値をセットします。
      if (key != getString && attribute.ErrorMessage != getString)
      {
        attribute.ErrorMessage = getString;
      }

      // 設定した attribute を渡します
      return _fallback.GetAttributeAdapter(attribute, stringLocalizer);
    }
  }
}

모델의 속성을 Required 설정하거나 모든 유효성 검사에 대해 메서드가 StringLength IValidationAttributeAdapterProvider.GetAttributeAdapter 호출됩니다.

ErrorMessageResourceName 속성에 값이 포함된 경우 모델 측의 코드에는 이미 메시지 또는 지역화 키가 있으므로 현재와 같이 반환됩니다.

비어 있는 경우 접두사 및 유효성 검사 특성의 클래스 이름을 일치하여 지역화 키로 설정한 다음 키에서 지역화된 텍스트를 검색하여 ErrorMessage 로 설정합니다. 이렇게 하면 대부분의 기본 메시지를 지역화할 수 있습니다.

다음으로 이 클래스를 시작 .cs 등록합니다. 기본적으로 다음과 같이 추가할 수 있습니다.

// 省略

using Microsoft.AspNetCore.Mvc.DataAnnotations;

namespace LocalizationDefaultValidation
{
  public class Startup
  {
    // 省略

    // このメソッドはランタイムによって呼び出されます。 このメソッドを使用して、コンテナーにサービスを追加します。
    public void ConfigureServices(IServiceCollection services)
    {
      // 省略

      // 作成した CustomValidationAttributeAdapterProvider をシングルトンとして登録します
      services.AddSingleton<IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();
    }
    
    // 省略
  }
}

제대로 작동하는지 확인하기 위해 실행합니다.

매개 변수별로 기존 메시지 변경

예를 들어 StringLength 특성 메시지는 "{0} {1} 숫자 내에 있어야 합니다"와 같이 지역화됩니다. MinimumLength 속성이 설정된 경우 {0} "숫자 수 내에서 {2} 숫자 또는 더 많은 {1} 지정"으로 변경할 수 있습니다.

CustomValidationAttributeAdapterProvider 경우 클래스를 다음과 같이 확장합니다.

// 省略

namespace LocalizationDefaultValidation
{
  public class CustomValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
  {
    // 省略

    IAttributeAdapter IValidationAttributeAdapterProvider.GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer stringLocalizer)
    {
      if (attribute is StringLengthAttribute slAttribute)
      {
        // attribute が StringLengthAttribute で MinimumLength が設定されている場合はメッセージを変える
        if (slAttribute.MinimumLength >= 1)
        {
          attribute.ErrorMessage = stringLocalizer["Validator_StringLengthAttributeWithMin"];
          return _fallback.GetAttributeAdapter(slAttribute, stringLocalizer);
        }
      }

      // 省略
    }
  }
}

메서드는 각 유효성 검사에 대해 호출되므로 GetAttributeAdapter 전달된 변수가 특성인 경우 attribute 속성을 StringLengthAttribute MinimumLength 확인합니다. 설정하면 최소 숫자 수를 고려하고 대체하는 메시지도 ErrorMessage 가져옵니다.

실행, 확인 및 메시지가 변경 되면 괜찮습니다.

기타 일반 메시지

일부 메시지는 지금까지 이렇게 한 후에 는 지역화되지 않습니다. 예를 들어 자바스크립트에서만 결정되는 메시지입니다.

대상은 jquery.validate.js 파일에 나열된 메시지입니다.

이 파일을 직접 편집해서는 안 되므로 이러한 메시지를 지역화된 텍스트로 대체하려면 input 태그와 같은 특성을 추가하고 현지화된 텍스트를 설정해야 data-val-XXXX 합니다. XXXX 부품에는 위의 그림의 키가 포함되어 jquery.validate.js 있습니다. (예: data-val-number 등)

data-val-XXXX

예를 들어 연령 입력 필드에 숫자가 없는 문자가 포함될 때 메시지를 현지화해 보겠습니다. UserViewModel.Age Range 특성이 있고 출력 HTML은 data-val-range 입력 특성을 추가합니다.

<div class="form-group">
  <label class="control-label" for="Age">Age</label>
  <input class="form-control" type="number" data-val="true" data-val-range="Ageは0から150の範囲で指定してください。" data-val-range-max="150" data-val-range-min="0" data-val-required="Ageは必須です。" id="Age" name="Age" value="" />
  <span class="text-danger field-validation-valid" data-valmsg-for="Age" data-valmsg-replace="true"></span>
</div>

그러나 data-val-number 특성이 없기 때문에 숫자 확인 메시지가 로컬화되지 않습니다. 모델 측에 다른 특성을 추가하여 지역화된 메시지 추가 data-val-number 특성도 인쇄합니다.

첫 번째 단계는 숫자인지 확인하는 특성 IntAttribute 클래스를 만드는 것입니다. 여기에 한정되어 있지만 int 상황에 따라 자유롭게 변경할 수 있습니다. 코드 파일은 어디에나 있을 수 있지만 이번에는 어댑터 폴더에 있습니다.

using System.ComponentModel.DataAnnotations;

namespace LocalizationDefaultValidation
{
  public class IntAttribute : ValidationAttribute
  {
    public override bool IsValid(object value)
    {
      // 渡された値が int であれば有効な値とする
      return value is int;
    }
  }
}

설정된 값이 형식인 경우 int 일반 평결에 불과한 특성이지만 실제로는 int 형식의 속성으로 설정하면 100% 정상이므로 이 클래스 자체의 처리는 의미가 없습니다. 이 시간의 목적은 클라이언트에 지역화된 오류 메시지를 표시하는 것입니다.

다음으로 다음 어댑터 IntAttributeAdapter 클래스를 만듭니다. 어댑터 폴더에도 있습니다.

using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Localization;

namespace LocalizationDefaultValidation
{
  /// <summary>Int 属性のアダプター。</summary>
  public class IntAttributeAdapter : AttributeAdapterBase<IntAttribute>
  {
    /// <summary>受け取った IStringLocalizer を保持しておく</summary>
    IStringLocalizer _stringLocalizer;

    public IntAttributeAdapter(IntAttribute attribute, IStringLocalizer stringLocalizer)
        : base(attribute, stringLocalizer)
    {
      _stringLocalizer = stringLocalizer;
    }

    /// <summary>バリデーションの追加処理として呼ばれる。</summary>
    public override void AddValidation(ClientModelValidationContext context)
    {
      // 新たに data-val-number 属性をマージして追加します。値はローカライズしたテキストをセットします。
      MergeAttribute(context.Attributes, "data-val-number", _stringLocalizer["ModelBinding_NonPropertyValueMustBeANumber"]);
    }

    /// <summary>サーバーのエラーメッセージはそのまま返します。</summary>
    public override string GetErrorMessage(ModelValidationContextBase validationContext) => Attribute.ErrorMessage;
  }
}

여기서 요점은 AddValidation 메서드에 설명된 MergeAttribute 메서드입니다.

특성을 클라이언트로 되돌리기 위해 입력 태그의 data-val-number 특성으로 병합합니다. 특성에서 설정할 값은 지역화된 오류 메시지를 설정합니다.

이 어댑터를 이전에 만든 클래스에서 CustomValidationAttributeAdapterProvider 반환합니다.

// 省略

namespace LocalizationDefaultValidation
{
  public class CustomValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
  {
    // 省略
    
    IAttributeAdapter IValidationAttributeAdapterProvider.GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer stringLocalizer)
    {
      // IntAttribute の場合は IntAttributeAdapter 経由で返す
      if (attribute is IntAttribute intAttribute)
      {
        return new IntAttributeAdapter(intAttribute, stringLocalizer);
      }

      // 省略 (前に追加したコード)
    }
  }
}

유효성을 검사할 값이 있는 경우 IntAttribute 이전에 만든 값을 IntAttributeAdapter 반환합니다. 이제 Int 특성이 있는 속성이 클라이언트 측에 표시되면 data-val-number 추가됩니다.

마지막으로 UserViewModel.Age IntAttribute 에 추가합니다.

코드를 수정한 후 코드를 체크 아웃할지 확인해 보십시오.

<div class="form-group">
  <label class="control-label" for="Age">Age</label>
  <input class="form-control" type="number" data-val="true" data-val-number="数字を指定してください。" data-val-range="Ageは0から150の範囲で指定してください。" data-val-range-max="150" data-val-range-min="0" data-val-required="Ageは必須です。" id="Age" name="Age" value="" />
  <span class="text-danger field-validation-valid" data-valmsg-for="Age" data-valmsg-replace="true"></span>
</div>

다른 비지역화 된 메시지가 있지만 위에서 설명한 메서드에서 지역화 할 수 있습니다. 필요할 때 코드를 추가합니다.