Learnixo
Back to blog
Backend Systemsintermediate

Localization in ASP.NET Core Web API: Multi-Language Support

Add multi-language support to ASP.NET Core Web APIs. Covers IStringLocalizer, resource files (.resx), culture middleware, controller-based vs shared resources, Accept-Language header, and data annotation localization.

LearnixoJune 4, 20265 min read
.NETC#Localizationi18nASP.NET CoreGlobalizationMulti-Language
Share:𝕏

What is Localization?

Localization (l10n) adapts your application to a specific culture — language, date formats, number formats, and currency. An API that serves users across multiple countries should return messages and data in the user's preferred language.

GET /api/orders/123
Accept-Language: ar

Response:
{ "message": "تم إنشاء الطلب بنجاح" }   ← Arabic

GET /api/orders/123
Accept-Language: en-US

Response:
{ "message": "Order created successfully" }  ← English

Setup

C#
// Program.cs
builder.Services.AddLocalization(options =>
    options.ResourcesPath = "Resources");

builder.Services.Configure<RequestLocalizationOptions>(options =>
{
    var supportedCultures = new[] { "en-US", "ar", "fr", "de", "es" };

    options.SetDefaultCulture("en-US")
           .AddSupportedCultures(supportedCultures)
           .AddSupportedUICultures(supportedCultures);

    // How to determine the culture for each request
    options.RequestCultureProviders = new List<IRequestCultureProvider>
    {
        new AcceptLanguageHeaderRequestCultureProvider(),  // from Accept-Language header
        new QueryStringRequestCultureProvider(),           // ?culture=ar
        new CookieRequestCultureProvider()                 // from cookie
    };
});

// Add middleware — must be BEFORE UseRouting and UseAuthorization
app.UseRequestLocalization();

Resource Files

Resource files (.resx) store translations as key-value pairs.

Resources/
├── Controllers/
│   ├── OrdersController.en-US.resx   ← English (default)
│   ├── OrdersController.ar.resx      ← Arabic
│   └── OrdersController.fr.resx      ← French
└── SharedResource.en-US.resx
    SharedResource.ar.resx

OrdersController.en-US.resx: | Key | Value | |---|---| | OrderCreated | Order created successfully | | OrderNotFound | Order was not found | | InsufficientStock | Insufficient stock for product |

OrdersController.ar.resx: | Key | Value | |---|---| | OrderCreated | تم إنشاء الطلب بنجاح | | OrderNotFound | الطلب غير موجود | | InsufficientStock | المخزون غير كافٍ للمنتج |

Create .resx files in Visual Studio: right-click Resources folder → Add → New Item → Resources File.


Controller-Based Localisation

One resource file per controller — best for large APIs with distinct domains.

C#
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
    private readonly IStringLocalizer<OrdersController> _localizer;
    private readonly IOrderRepository _orders;

    public OrdersController(
        IStringLocalizer<OrdersController> localizer,
        IOrderRepository orders)
    {
        _localizer = localizer;
        _orders    = orders;
    }

    [HttpPost]
    public async Task<IActionResult> Create(
        [FromBody] CreateOrderRequest request,
        CancellationToken ct)
    {
        var order = await _orders.CreateAsync(request, ct);

        return CreatedAtAction(nameof(GetById), new { id = order.Id }, new
        {
            order.Id,
            Message = _localizer["OrderCreated"].Value
        });
    }

    [HttpGet("{id:guid}")]
    public async Task<IActionResult> GetById(Guid id, CancellationToken ct)
    {
        var order = await _orders.GetByIdAsync(id, ct);
        if (order is null)
            return NotFound(new { Message = string.Format(_localizer["OrderNotFound"], id) });

        return Ok(order);
    }
}

Shared Resources

When many controllers share the same translations, use a shared resource.

C#
// Models/SharedResource.cs — marker class (can be empty)
public class SharedResource { }
Resources/
├── SharedResource.en-US.resx    ← shared translations
└── SharedResource.ar.resx

SharedResource.en-US.resx: | Key | Value | |---|---| | InvalidEmail | Invalid email address | | Required | is required | | ServerError | An unexpected error occurred |

C#
// Inject shared resource in any controller
public class AuthController : ControllerBase
{
    private readonly IStringLocalizer<SharedResource> _sharedLocalizer;

    public AuthController(IStringLocalizer<SharedResource> sharedLocalizer)
        => _sharedLocalizer = sharedLocalizer;

    [HttpPost("login")]
    public IActionResult Login([FromBody] LoginRequest request)
    {
        if (string.IsNullOrEmpty(request.Email))
            return BadRequest(_sharedLocalizer["Required", "Email"].Value);
        // ...
    }
}

Localising Validation Messages

C#
// DataAnnotations with localised messages
public class CreateOrderRequest
{
    [Required(ErrorMessage = "CustomerIdRequired")]
    public string CustomerId { get; set; } = "";

    [Range(1, 1000, ErrorMessage = "QuantityRange")]
    public int Quantity { get; set; }
}

// Program.cs — wire up localisation for DataAnnotations
builder.Services.AddControllers()
    .AddDataAnnotationsLocalization(options =>
    {
        options.DataAnnotationLocalizerProvider = (type, factory) =>
            factory.Create(typeof(SharedResource));
    });

SharedResource.ar.resx: | Key | Value | |---|---| | CustomerIdRequired | معرّف العميل مطلوب | | QuantityRange | يجب أن تكون الكمية بين 1 و 1000 |


Localising Exception Messages

C#
// Custom exception with localised message
public class BusinessException : Exception
{
    public string ResourceKey { get; }
    public object[] Args { get; }

    public BusinessException(string resourceKey, params object[] args)
        : base(resourceKey)
    {
        ResourceKey = resourceKey;
        Args        = args;
    }
}

// Global exception middleware with localisation
public class LocalisedExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IStringLocalizer<SharedResource> _localizer;

    public LocalisedExceptionMiddleware(
        RequestDelegate next,
        IStringLocalizer<SharedResource> localizer)
    {
        _next      = next;
        _localizer = localizer;
    }

    public async Task InvokeAsync(HttpContext ctx)
    {
        try
        {
            await _next(ctx);
        }
        catch (BusinessException ex)
        {
            var message = ex.Args.Length > 0
                ? string.Format(_localizer[ex.ResourceKey], ex.Args)
                : _localizer[ex.ResourceKey].Value;

            ctx.Response.StatusCode = 422;
            await ctx.Response.WriteAsJsonAsync(new { message });
        }
    }
}

Testing with Postman

Add the Accept-Language header to simulate different locales:

GET /api/orders
Accept-Language: ar

GET /api/orders
Accept-Language: fr-FR

GET /api/orders
Accept-Language: en-US, en;q=0.9, ar;q=0.8   ← quality values for priority

Currency and Date Localisation

C#
// Localise numbers, dates, and currencies in responses
public class LocalisedOrderDto
{
    public Guid   Id           { get; set; }
    public string Total        { get; set; } = "";
    public string CreatedAt    { get; set; } = "";

    public static LocalisedOrderDto FromOrder(Order order)
    {
        var culture = CultureInfo.CurrentCulture;
        return new LocalisedOrderDto
        {
            Id        = order.Id,
            Total     = order.Total.ToString("C", culture),      // £100.00 or $100.00 or 100,00 €
            CreatedAt = order.CreatedAt.ToString("D", culture)   // "Wednesday, 04 June 2026" or "الأربعاء, 4 يونيو 2026"
        };
    }
}

Interview Questions

Q: What is the difference between localisation and globalisation in .NET? Globalisation (globalization) makes code aware of cultural differences — date formats, number formats, currency. Localisation applies specific translations and adaptations for a particular culture. IStringLocalizer handles localisation; CultureInfo handles globalization formatting.

Q: How does ASP.NET Core determine the culture for a request? Through RequestCultureProviders checked in order: AcceptLanguageHeaderRequestCultureProvider reads the Accept-Language header, QueryStringRequestCultureProvider reads ?culture=, CookieRequestCultureProvider reads a cookie. The first matching provider wins.

Q: What is a shared resource and when would you use it? A single .resx file accessible across all controllers via IStringLocalizer<SharedResource>. Use it for strings that appear in many places — validation messages, common errors, standard responses. Use controller-specific resources for domain-specific strings.

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.