
ASP.NET Core 현지화 완벽 가이드
IStringLocalizer부터 프로덕션까지: ASP.NET Core에서 리소스 기반 현지화를 설정한 다음 AI로 번역을 자동화해요.
현지화 서비스 활성화
Program.cs에서 AddLocalization()으로 현지화 서비스를 등록하고, 지원할 문화권을 구성한 다음, 요청 현지화 미들웨어를 추가해요. 그러면 ASP.NET Core 애플리케이션의 전체 현지화 파이프라인이 연결돼요.
using Microsoft.AspNetCore.Localization;
using System.Globalization;
var builder = WebApplication.CreateBuilder(args);
// 1. Register localization services
builder.Services.AddLocalization(o => o.ResourcesPath = "Resources");
// 2. Add MVC with view/data-annotation localization
builder.Services.AddControllersWithViews()
.AddViewLocalization()
.AddDataAnnotationsLocalization();
var app = builder.Build();
// 3. Configure supported cultures
var supportedCultures = new[] { "en", "de", "ja", "es", "pt-BR" }
.Select(c => new CultureInfo(c)).ToArray();
app.UseRequestLocalization(new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture("en"),
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures,
});
app.UseStaticFiles();
app.UseRouting();
app.MapControllers();
app.Run();RESX 리소스 파일 생성
ASP.NET Core는 번역에 RESX(XML 리소스) 파일을 사용해요. 문화권별, 클래스별로 HomeController.en.resx, HomeController.de.resx 같은 파일을 하나씩 만들어요. 프레임워크는 현재 요청의 문화권에 따라 올바른 파일을 결정해요.
<!-- Resources/Controllers/HomeController.en.resx -->
<?xml version="1.0" encoding="utf-8"?>
<root>
<data name="Welcome" xml:space="preserve">
<value>Welcome to our application</value>
</data>
<data name="Greeting" xml:space="preserve">
<value>Hello, {0}!</value>
</data>
</root>
<!-- Resources/Controllers/HomeController.de.resx -->
<?xml version="1.0" encoding="utf-8"?>
<root>
<data name="Welcome" xml:space="preserve">
<value>Willkommen in unserer Anwendung</value>
</data>
<data name="Greeting" xml:space="preserve">
<value>Hallo, {0}!</value>
</data>
</root><!-- Resources/SharedResource.en.resx — shared across controllers -->
<?xml version="1.0" encoding="utf-8"?>
<root>
<data name="AppName" xml:space="preserve">
<value>My Application</value>
</data>
<data name="Save" xml:space="preserve"><value>Save</value></data>
<data name="Cancel" xml:space="preserve"><value>Cancel</value></data>
</root>
// Marker class (empty — only used for type lookup)
namespace MyApp;
public class SharedResource { }컨트롤러와 서비스에서 IStringLocalizer 사용
의존성 주입을 통해 모든 컨트롤러, 서비스 또는 미들웨어에 IStringLocalizer<T>를 주입해요. 제네릭 형식 매개변수 T가 불러올 RESX 파일을 결정해요. 선택적 서식 매개변수와 함께 대괄호 구문 localizer["Key"]를 사용하여 번역된 문자열을 가져와요.
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
public class HomeController(
IStringLocalizer<HomeController> localizer,
IStringLocalizer<SharedResource> shared) : Controller
{
public IActionResult Index()
{
ViewData["Welcome"] = localizer["Welcome"];
ViewData["AppName"] = shared["AppName"];
// String interpolation with format parameters
var greeting = localizer["Greeting", User.Identity?.Name ?? "Guest"];
return View(new HomeViewModel { Greeting = greeting });
}
}요청 문화권 미들웨어 구성
ASP.NET Core는 쿼리 문자열, 쿠키, Accept-Language 헤더 순으로 공급자 체인을 사용해 요청 문화권을 결정해요. /de/home 같은 URL 경로 세그먼트에서 문화권을 읽는 사용자 정의 공급자도 추가할 수 있어요.
// Culture resolved in order: QueryString, Cookie, Accept-Language
// Custom provider: read culture from URL route segment /de/home
public class RouteDataRequestCultureProvider : RequestCultureProvider
{
public override Task<ProviderCultureResult?> DetermineProviderCultureResult(
HttpContext httpContext)
{
var culture = httpContext.GetRouteValue("culture")?.ToString();
if (string.IsNullOrEmpty(culture))
return NullProviderCultureResult;
return Task.FromResult<ProviderCultureResult?>(
new ProviderCultureResult(culture));
}
}
// Register in Program.cs (route provider first = highest priority):
app.UseRequestLocalization(new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture("en"),
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures,
RequestCultureProviders = new List<IRequestCultureProvider>
{
new RouteDataRequestCultureProvider(),
new QueryStringRequestCultureProvider(),
new CookieRequestCultureProvider(),
new AcceptLanguageHeaderRequestCultureProvider(),
}
});// Language switcher: persist choice in cookie
[HttpPost]
public IActionResult SetLanguage(string culture, string returnUrl)
{
Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) });
return LocalRedirect(returnUrl);
}데이터 애너테이션 현지화
[Required], [StringLength], [Display] 같은 유효성 검사 특성의 ErrorMessage 또는 Name 속성을 RESX 키 이름으로 설정하여 현지화할 수 있어요. Program.cs에서 AddDataAnnotationsLocalization()을 호출하여 활성화해요.
using System.ComponentModel.DataAnnotations;
public class RegisterViewModel
{
[Required(ErrorMessage = "NameRequired")]
[Display(Name = "FullName")]
[StringLength(100, ErrorMessage = "NameLength", MinimumLength = 2)]
public string Name { get; set; } = string.Empty;
[Required(ErrorMessage = "EmailRequired")]
[EmailAddress(ErrorMessage = "EmailInvalid")]
[Display(Name = "EmailAddress")]
public string Email { get; set; } = string.Empty;
[Required(ErrorMessage = "PasswordRequired")]
[StringLength(100, ErrorMessage = "PasswordLength", MinimumLength = 8)]
[Display(Name = "Password")]
public string Password { get; set; } = string.Empty;
}
// RESX keys map to ErrorMessage/Name values:
// RegisterViewModel.de.resx: NameRequired = "Name ist erforderlich"
// RegisterViewModel.de.resx: FullName = "Vollständiger Name"복수형 및 ICU 메시지 처리
.NET에는 ICU와 같은 복수형 규칙 지원이 내장되어 있지 않아요. 간단한 경우에는 코드 분기와 별도의 RESX 키(ItemCount_One, ItemCount_Other)를 사용해요. 모든 CLDR 복수형 범주를 지원하는 전체 ICU MessageFormat이 필요하다면 MessageFormat.NET 라이브러리를 사용하세요.
// Option 1: Separate RESX keys with code switch
// HomeController.en.resx: ItemCount_One = "You have {0} item"
// HomeController.en.resx: ItemCount_Other = "You have {0} items"
public string GetItemCount(int count)
{
var key = count == 1 ? "ItemCount_One" : "ItemCount_Other";
return _localizer[key, count];
}
// Option 2: ICU MessageFormat (dotnet add package MessageFormat.NET)
using Jeffijoe.MessageFormat;
var formatter = new MessageFormatter();
var pattern = "{count, plural, one {# item} other {# items}} in your cart";
var result = formatter.FormatMessage(pattern,
new Dictionary<string, object> { { "count", 5 } });
// => "5 items in your cart"
// Arabic: 6 plural forms (zero, one, two, few, many, other)
var arPattern = @"{count, plural,
zero {لا عناصر} one {عنصر واحد} two {عنصران}
few {# عناصر} many {# عنصرًا} other {# عنصر}}";Razor 뷰 현지화
Razor 뷰에서 @inject를 통해 IViewLocalizer를 사용해요. 뷰의 파일 경로를 기준으로 RESX 파일을 결정해요. 마크업을 포함하면서 HTML에 안전한 문자열에는 IHtmlLocalizer를 사용해요. asp-for 및 asp-validation-for 같은 태그 도우미는 현지화된 Display 및 ErrorMessage 특성을 자동으로 사용해요.
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
@inject IHtmlLocalizer<SharedResource> SharedHtml
<h1>@Localizer["Welcome"]</h1>
<p>@Localizer["Greeting", User.Identity?.Name]</p>
@* IHtmlLocalizer: does NOT escape — use for RESX values with HTML *@
<p>@SharedHtml["TermsNotice"]</p>
@* Tag Helpers auto-localize Display/ErrorMessage attributes *@
<form asp-action="Register">
<label asp-for="Name"></label>
<input asp-for="Name" />
<span asp-validation-for="Name"></span>
<button type="submit">@Localizer["Submit"]</button>
</form>RESX 번역 자동화
현지화 설정을 마쳤다면 AI로 RESX 파일을 번역해요. IDE에서 AI 어시스턴트에게 원본 RESX 번역을 요청하거나 CI/CD 파이프라인에서 i18n Agent CLI를 사용하여 번역을 동기화해요.
# In your IDE, ask your AI assistant:
> Translate Resources/Controllers/HomeController.en.resx to German, Japanese, Spanish
# HomeController.de.resx created (1.2s)
# HomeController.ja.resx created (1.5s)
# HomeController.es.resx created (1.1s)
# Or use the CLI in CI/CD:
npx i18n-agent translate Resources/Controllers/HomeController.en.resx --lang de,ja,es번역 품질 자동화
LocaleChain.NET으로 로케일 폴백 수정
.NET의 내장 CultureInfo.Parent 계층 구조는 BCP 47 태그를 잘라내는 방식만 사용해요. pt-BR은 pt, InvariantCulture 순으로 폴백하며 pt-PT를 건너뛰어요. LocaleChain.NET은 전체 .NET 생태계에 로케일별로 구성 가능한 폴백 체인을 제공해요.
dotnet add package I18nAgent.LocaleChainusing I18nAgent.LocaleChain;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddLocalization(o => o.ResourcesPath = "Resources");
// Zero-config: built-in chains (pt-BR -> pt-PT -> pt -> en, etc.)
LocaleChain.Configure();
// Or customize specific chains:
LocaleChain.Configure(new Dictionary<string, string[]>
{
["pt-BR"] = new[] { "pt-PT", "pt", "en" },
["es-MX"] = new[] { "es-419", "es", "en" },
});
// Register the chain-aware string localizer
builder.Services.AddSingleton(
typeof(IStringLocalizer<>),
typeof(LocaleChainStringLocalizer<>));흔히 발생하는 문제
요청에 문화권이 설정되지 않음
RESX 파일을 찾을 수 없음
잘못된 미들웨어 순서
백그라운드 스레드가 잘못된 문화권 사용
권장 프로젝트 구조
MyAspNetApp/
├── Controllers/
│ └── HomeController.cs
├── ViewModels/
│ └── RegisterViewModel.cs
├── Views/
│ └── Home/
│ └── Index.cshtml
├── Resources/
│ ├── Controllers/
│ │ ├── HomeController.en.resx # English (source)
│ │ ├── HomeController.de.resx # German
│ │ └── HomeController.ja.resx # Japanese
│ ├── ViewModels/
│ │ ├── RegisterViewModel.en.resx
│ │ └── RegisterViewModel.de.resx
│ ├── Views/Home/
│ │ ├── Index.en.resx
│ │ └── Index.de.resx
│ └── SharedResource.en.resx
├── SharedResource.cs # Marker class
├── Program.cs
└── MyAspNetApp.csproj지금 i18n Agent 사용해 보기
번역 파일을 여기에 드롭
JSON, YAML, PO, XML, CSV, Markdown, Properties
또는 클릭하여 파일 선택
대상 언어
I18nAgent.LocaleChain을 활용한 로케일 폴백
pt-BR 같은 지역 로케일에 번역 키가 없으면 .NET은 상위 로케일 pt를 먼저 확인하지 않고 고정 문화권으로 바로 이동해요.
dotnet add package I18nAgent.LocaleChainusing I18nAgent.LocaleChain;
LocaleChain.Configure(new Dictionary<string, string[]>
{
["pt-BR"] = new[] {"pt", "en"},
["zh-Hant-HK"] = new[] {"zh-Hant", "zh", "en"},
});지원되는 프레임워크 전체 목록과 75개의 기본 제공 체인은 로케일 폴백 가이드에서 확인하세요. Learn more →