Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implement Multilingual ASP.NET 8 Applications Using a Single Shared RESX File

Tech 1

Most existing ASP.NET Core 8 MVC localization tutorials are either designed for older .NET versions or lack clear guidance for consolidating all localized strings into a single shared RESX file. The following implementation uses the official shared resource pattern to avoid duplicate translation entries across controllers, views, and application components.

Shared Resource Pattern Overview

The default ASP.NET Core 8 localization workflow creates separate RESX files for each controller and view, which leads to redundant translations for common strings (e.g., submit buttons, error messages, navigation labels). The shared resource pattern uses an empty marker class to group all translation entries, so you only maintain one set of RESX files per supported culture.

Implementation Steps

Configure Localization Services and Middleware

First, register localization services in Program.cs and define supported cultures. The example below supports English, French, German, and Italian, with English set as the default:

namespace SingleResxLocalization
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);
            
            RegisterLocalizationServices(builder);
            
            builder.Services.AddControllersWithViews();
            
            var app = builder.Build();
            
            app.UseRequestLocalization();
            
            if (!app.Environment.IsDevelopment())
            {
                app.UseExceptionHandler("/Home/Error");
                app.UseHsts();
            }
            
            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseRouting();
            app.UseAuthorization();
            
            app.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=SetCulture}/{id?}");
                
            app.Run();
        }
        
        private static void RegisterLocalizationServices(WebApplicationBuilder builder)
        {
            ArgumentNullException.ThrowIfNull(builder);
            
            builder.Services.AddLocalization(opt => opt.ResourcesPath = "LocalizationResources");
            builder.Services.AddMvc()
                .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);
                
            builder.Services.Configure<RequestLocalizationOptions>(opt =>
            {
                var supportedCultures = new[] {"en", "fr", "de", "it"};
                opt.SetDefaultCulture(supportedCultures[0])
                    .AddSupportedCultures(supportedCultures)
                    .AddSupportedUICultures(supportedCultures);
            });
        }
    }
}

Create Marker Class for Shared Resources

Create an empty marker class to associate all shared RESX files with a single type. The class namespace must match your application's root namespace to avoid resource resolution errors:

// GlobalResourceMarker.cs
namespace SingleResxLocalization
{
    // Empty marker class used to group all shared localization resources
    // Class name can be modified as long as all RESX files and DI references match
    public class GlobalResourceMarker
    {
    }
}

The marker class can be placed in any project folder, as long as its namespace matches the application root namespace. If you encounter resource resolution issues, use the fully qualified type name in dependency injection declarations (e.g., IStringLocalizer<SingleResxLocalization.GlobalResourceMarker>).

Create RESX Resource Files

Create a folder named LocalizationResources in your project root, then add RESX files for each supported culture using the naming format GlobalResourceMarker.[culture-code].resx (e.g., GlobalResourceMarker.en.resx, GlobalResourceMarker.fr.resx). Add all your translated string entries to these files, using consistent keys across all culture variants.

Culture Selection Workflow

ASP.NET Core 8 includes three default culture providers: query string, cookie, and accept-language header. Cookie-based culture selection is the most common approach for user-facing applications, as it persists user preferences across sessions.

The folowing method sets the culture cookie that the localization middleware uses to resolve the active culture for each request:

private void SetCultureCookie(HttpContext context, string cultureCode)
{
    ArgumentNullException.ThrowIfNull(cultureCode);
    ArgumentNullException.ThrowIfNull(context);
    
    context.Response.Cookies.Append(
        CookieRequestCultureProvider.DefaultCookieName,
        CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(cultureCode)),
        new CookieOptions { Expires = DateTimeOffset.UtcNow.AddMonths(2) }
    );
}

The cookie is valid for 2 months by default, and you can inspect its value using browser developer tools to debug culture resolution issues.

Use Localization in Controllers

Inject IStringLocalizer<GlobalResourceMarker> and IHtmlLocalizer<GlobalResourceMarker> into your controllers via constructor dependency injection to access translated strings:

using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using Microsoft.AspNetCore.Mvc.Localization;
using SingleResxLocalization.Models;

namespace SingleResxLocalization.Controllers
{
    public class HomeController : Controller
    {
        private readonly IStringLocalizer<GlobalResourceMarker> _textLocalizer;
        private readonly IHtmlLocalizer<GlobalResourceMarker> _markupLocalizer;
        private readonly ILogger<HomeController> _logger;
        
        public HomeController(ILogger<HomeController> logger,
            IStringLocalizer<GlobalResourceMarker> textLocalizer,
            IHtmlLocalizer<GlobalResourceMarker> markupLocalizer)
        {
            _logger = logger;
            _textLocalizer = textLocalizer;
            _markupLocalizer = markupLocalizer;
        }
        
        public IActionResult SetCulture(CultureSelectionModel model)
        {
            if (model.FormSubmitted)
            {
                SetCultureCookie(HttpContext, model.ChosenCulture);
                return LocalRedirect("/Home/LocalizationDemo");
            }
            
            model.AvailableCultures = new List<SelectListItem>
            {
                new() { Text = "English", Value = "en" },
                new() { Text = "French", Value = "fr" },
                new() { Text = "German", Value = "de" },
                new() { Text = "Italian", Value = "it" }
            };
            return View(model);
        }
        
        public IActionResult LocalizationDemo(LocalizationDemoModel model)
        {
            model.ControllerLocalizedText = _textLocalizer["WelcomeMessage"];
            model.ControllerLocalizedMarkup = _markupLocalizer["WelcomeMessage"];
            return View(model);
        }
        
        public IActionResult Error()
        {
            return View(new ErrorModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }
}

Use Localization in Razor Views

Inject the localizer services directly into Razor views to access translated strings in UI markup:

@* SetCulture.cshtml *@
@model CultureSelectionModel

<div style="max-width: 550px; margin: 2rem auto;">
    <form method="post">
        <fieldset class="border rounded p-4">
            <legend class="w-auto px-2">Select Display Language</legend>
            <div class="mb-3">
                <label asp-for="ChosenCulture" class="form-label"></label>
                <select asp-for="ChosenCulture" asp-items="@Model.AvailableCultures" class="form-select"></select>
                <input type="hidden" name="FormSubmitted" value="true" />
            </div>
            <button type="submit" class="btn btn-primary float-end">Save Preference</button>
        </fieldset>
    </form>
</div>
@* LocalizationDemo.cshtml *@
@using Microsoft.Extensions.Localization
@using Microsoft.AspNetCore.Mvc.Localization
@model LocalizationDemoModel

@inject IStringLocalizer<GlobalResourceMarker> TextLocalizer
@inject IHtmlLocalizer<GlobalResourceMarker> MarkupLocalizer

<div style="max-width: 650px; margin: 2rem auto;">
    <div class="alert alert-info">
        Controller localized text (IStringLocalizer): @Model.ControllerLocalizedText
    </div>
    <div class="alert alert-info">
        View localized text (IStringLocalizer): @TextLocalizer["WelcomeMessage"]
    </div>
    <div class="alert alert-info">
        Controller localized markup (IHtmlLocalizer): @Model.ControllerLocalizedMarkup
    </div>
    <div class="alert alert-info">
        View localized markup (IHtmlLocalizer): @MarkupLocalizer["WelcomeMessage"]
    </div>
</div>

Supporting Model Classes

// CultureSelectionModel.cs
namespace SingleResxLocalization.Models
{
    public class CultureSelectionModel
    {
        public string ChosenCulture { get; set; } = "en";
        public bool FormSubmitted { get; set; } = false;
        public List<SelectListItem>? AvailableCultures { get; set; }
    }
}
// LocalizationDemoModel.cs
namespace SingleResxLocalization.Models
{
    public class LocalizationDemoModel
    {
        public string? ControllerLocalizedText { get; set; }
        public LocalizedHtmlString? ControllerLocalizedMarkup { get; set; }
    }
}

IHtmlLocalizer Behavior Note

IHtmlLocalizer<GlobalResourceMarker> correctly resolves plain text translation entries, but does not render HTML tags from resource entries by default to mitigate cross-site scripting (XSS) risks. If you need to render HTML content from a resource entry, explicitly use @Html.Raw(MarkupLocalizer["HtmlResourceKey"]) only for trusted resuorce content.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.