Skip to main content

ASP.NET Core 현지화 완벽 가이드

IStringLocalizer부터 프로덕션까지: ASP.NET Core에서 리소스 기반 현지화를 설정한 다음 AI로 번역을 자동화해요.

1

현지화 서비스 활성화

Program.cs에서 AddLocalization()으로 현지화 서비스를 등록하고, 지원할 문화권을 구성한 다음, 요청 현지화 미들웨어를 추가해요. 그러면 ASP.NET Core 애플리케이션의 전체 현지화 파이프라인이 연결돼요.

AddLocalization()은 DI 컨테이너에 IStringLocalizer와 IStringLocalizerFactory를 등록해요. ResourcesPath는 프레임워크에 .resx 파일 위치를 알려 줘요. AddViewLocalization()은 Razor 뷰에서 IViewLocalizer를 활성화하고, AddDataAnnotationsLocalization()은 현지화된 유효성 검사 메시지를 활성화해요.
Program.cs
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();
2

RESX 리소스 파일 생성

ASP.NET Core는 번역에 RESX(XML 리소스) 파일을 사용해요. 문화권별, 클래스별로 HomeController.en.resx, HomeController.de.resx 같은 파일을 하나씩 만들어요. 프레임워크는 현재 요청의 문화권에 따라 올바른 파일을 결정해요.

Resources/Controllers/HomeController.{culture}.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>
컨트롤러와 뷰에서 공유하는 문자열(버튼 레이블, 탐색 항목, 공통 유효성 검사 메시지)에는 자체 RESX 파일이 있는 SharedResource 클래스를 사용하세요. 그러면 수십 개의 컨트롤러별 RESX 파일에 키가 중복되지 않아요.
Shared resources for cross-cutting strings
<!-- 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 { }
3

컨트롤러와 서비스에서 IStringLocalizer 사용

의존성 주입을 통해 모든 컨트롤러, 서비스 또는 미들웨어에 IStringLocalizer&lt;T&gt;를 주입해요. 제네릭 형식 매개변수 T가 불러올 RESX 파일을 결정해요. 선택적 서식 매개변수와 함께 대괄호 구문 localizer["Key"]를 사용하여 번역된 문자열을 가져와요.

Controllers/HomeController.cs
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 });
    }
}
IStringLocalizer가 번역된 값 대신 키 이름을 반환한다면 세 가지를 확인하세요. (1) RESX 파일 이름이 클래스 네임스페이스와 일치하는지, (2) AddLocalization()의 ResourcesPath가 올바른 폴더를 가리키는지, (3) Visual Studio에서 RESX 파일의 Build Action이 Embedded Resource로 설정되어 있는지 확인하세요.
4

요청 문화권 미들웨어 구성

ASP.NET Core는 쿼리 문자열, 쿠키, Accept-Language 헤더 순으로 공급자 체인을 사용해 요청 문화권을 결정해요. /de/home 같은 URL 경로 세그먼트에서 문화권을 읽는 사용자 정의 공급자도 추가할 수 있어요.

Custom RouteDataRequestCultureProvider
// 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 Action
// 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);
}
미들웨어 순서가 중요해요. UseRequestLocalization()은 UseRouting() 뒤, UseEndpoints() 또는 MapControllers() 앞에서 호출해야 해요. 너무 늦게 배치하면 컨트롤러가 실행될 때 문화권이 설정되지 않아요.
5

데이터 애너테이션 현지화

[Required], [StringLength], [Display] 같은 유효성 검사 특성의 ErrorMessage 또는 Name 속성을 RESX 키 이름으로 설정하여 현지화할 수 있어요. Program.cs에서 AddDataAnnotationsLocalization()을 호출하여 활성화해요.

ViewModels/RegisterViewModel.cs
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"
데이터 애너테이션 현지화는 Controller가 아니라 ViewModel 클래스 이름으로 RESX 파일을 찾아요. RegisterViewModel의 경우 프레임워크는 Resources/ViewModels/RegisterViewModel.de.resx를 찾아요. RESX 파일 이름이 컨트롤러를 기준으로 되어 있다면 유효성 검사 메시지가 현지화되지 않아요.
6

복수형 및 ICU 메시지 처리

.NET에는 ICU와 같은 복수형 규칙 지원이 내장되어 있지 않아요. 간단한 경우에는 코드 분기와 별도의 RESX 키(ItemCount_One, ItemCount_Other)를 사용해요. 모든 CLDR 복수형 범주를 지원하는 전체 ICU MessageFormat이 필요하다면 MessageFormat.NET 라이브러리를 사용하세요.

Plural handling strategies
// 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 {# عنصر}}";
단수형을 감지하는 데 count == 1을 절대 사용하지 마세요. 프랑스어는 0을 단수로 취급해요. 러시아어에는 'few'와 'many'에 해당하는 별도 형식이 있고, 아랍어에는 6개의 복수형 범주가 있어요. CLDR을 인식하는 복수형 규칙이나 이를 올바르게 처리하는 MessageFormat.NET 같은 라이브러리를 사용하세요.
7

Razor 뷰 현지화

Razor 뷰에서 @inject를 통해 IViewLocalizer를 사용해요. 뷰의 파일 경로를 기준으로 RESX 파일을 결정해요. 마크업을 포함하면서 HTML에 안전한 문자열에는 IHtmlLocalizer를 사용해요. asp-for 및 asp-validation-for 같은 태그 도우미는 현지화된 Display 및 ErrorMessage 특성을 자동으로 사용해요.

Views/Home/Index.cshtml
@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>
IViewLocalizer는 뷰 경로를 기준으로 RESX 파일을 결정해요. Views/Home/Index.cshtml은 Resources/Views/Home/Index.de.resx를 찾아요. 여러 뷰에서 문자열을 공유하려면 IStringLocalizer&lt;SharedResource&gt;를 별도로 주입하세요.
8

RESX 번역 자동화

현지화 설정을 마쳤다면 AI로 RESX 파일을 번역해요. IDE에서 AI 어시스턴트에게 원본 RESX 번역을 요청하거나 CI/CD 파이프라인에서 i18n Agent CLI를 사용하여 번역을 동기화해요.

Terminal
# 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
점진적으로 번역하세요. 원본 RESX 파일에 새 키를 추가할 때 모든 파일을 다시 생성하지 말고 변경분만 번역해요. 그러면 사람이 검토한 번역을 보존하고 불필요한 변경을 최소화할 수 있어요.

번역 품질 자동화

i18n-validate로 출시 전에 누락된 키와 손상된 플레이스홀더를 찾아요. 실제 번역이 준비되기 전에 i18n-pseudo의 의사 번역으로 UI를 테스트해요.

LocaleChain.NET으로 로케일 폴백 수정

.NET의 내장 CultureInfo.Parent 계층 구조는 BCP 47 태그를 잘라내는 방식만 사용해요. pt-BR은 pt, InvariantCulture 순으로 폴백하며 pt-PT를 건너뛰어요. LocaleChain.NET은 전체 .NET 생태계에 로케일별로 구성 가능한 폴백 체인을 제공해요.

LocaleChain.NET이 없으면 포르투갈어 문자열이 누락되었을 때 pt-BR 사용자에게 영어가 표시돼요. 완전한 pt-PT 번역이 있어도 마찬가지예요. 같은 문제가 es-MX(es-419를 건너뜀), zh-Hant(zh-Hans를 건너뜀) 및 수십 개의 다른 지역 변형에도 영향을 줘요.
Terminal
dotnet add package I18nAgent.LocaleChain
Program.cs
using 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<>));

흔히 발생하는 문제

요청에 문화권이 설정되지 않음

번역이 항상 기본 언어로 표시돼요. 미들웨어 파이프라인에서 UseRequestLocalization()이 호출되는지, 문화권 공급자가 구성되어 있는지 확인하세요. 브라우저가 Accept-Language 헤더를 전송하는지도 확인하세요. 쿼리 문자열의 ?culture=de로 테스트하여 미들웨어가 작동하는지 확인해요.

RESX 파일을 찾을 수 없음

IStringLocalizer가 번역된 값 대신 키 이름을 반환해요. 가장 흔한 원인은 RESX 파일 이름이 ResourcesPath를 기준으로 한 클래스의 전체 네임스페이스 경로와 일치하지 않는 경우예요. Microsoft.Extensions.Localization의 디버그 로깅을 활성화하여 프레임워크가 검색하는 경로를 확인하세요.

잘못된 미들웨어 순서

UseRequestLocalization()은 UseEndpoints()와 MapControllers() 앞에 와야 해요. 뒤에 배치하면 컨트롤러가 실행될 때 요청 문화권이 설정되지 않아요. .NET 6+ 최소 호스팅에서는 app.MapControllers()보다 먼저 호출하세요.

백그라운드 스레드가 잘못된 문화권 사용

CultureInfo.CurrentCulture와 CurrentUICulture는 스레드별로 적용돼요. 백그라운드 작업(Task.Run, 호스팅된 서비스)은 요청 문화권이 아니라 스레드 풀의 문화권을 상속해요. 백그라운드 작업을 시작할 때 문화권을 명시적으로 캡처하여 설정하세요.

권장 프로젝트 구조

Project Structure
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를 먼저 확인하지 않고 고정 문화권으로 바로 이동해요.

Terminal
dotnet add package I18nAgent.LocaleChain
Configuration
using I18nAgent.LocaleChain;

LocaleChain.Configure(new Dictionary<string, string[]>
{
    ["pt-BR"] = new[] {"pt", "en"},
    ["zh-Hant-HK"] = new[] {"zh-Hant", "zh", "en"},
});

지원되는 프레임워크 전체 목록과 75개의 기본 제공 체인은 로케일 폴백 가이드에서 확인하세요. Learn more →

자주 묻는 질문