In this article, I’ll show you how to translate a Web application built with ASP.NET and discuss two ways to do it. The first method you are familiar with is using static resources. The second method uses a third-party API. We will consider all the pros and cons of each technology. Of course, we can write code.
Use static resources for translation.
This method is widely used and often implemented in various projects. You don’t need any third-party packages or APIs. First, modify Program.cs
file and add supported cultures. Just add these lines.
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
var supportedCultures = new[]
{
new CultureInfo("en-US"),
new CultureInfo("fr-FR")
};
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
options.DefaultRequestCulture = new RequestCulture("en-US");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
builder.Services.AddControllersWithViews()
.AddViewLocalization()
.AddDataAnnotationsLocalization();
var locOptions = app.Services.GetRequiredService<IOptions<RequestLocalizationOptions>>();
app.UseRequestLocalization(locOptions.Value);
Since we will be translating two layouts, we need to create folders. The folder hierarchy should be the same as in the project. Please create a Resources folder and a derived folder, as shown in the figure.
In each folder, create a resource file and add the required translations.
To make it more useful, I’m going to add a drop-down menu for selecting a language. Go to _Layout.cshtml
and add this code.
We need to save cookies to maintain state. Then, after refreshing the page, your selected language will be retained. Go to HomeController
And add the POST method to handle cookies.
[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 ?? "https://dev.to/");
}
In the final steps, you need to modify the layout and replace the text you want to translate. Go to Index.cshtml
document.
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
@{
ViewData["Title"] = "Home Page";
}
class="text-center">
@Localizer["LearnMore"]
Also, please go to _Layout.cshtml
file and change the navigation menu and footer manifests.
Let’s look at this. The default localization looks like this.
If you change the language, you will see the page translated to the desired culture.
If you refresh the page, it still has the French localization. If you restart the app, you’ll still see the French translation. Your browser retains the selected culture in a cookie.
Now, let’s consider the advantages and disadvantages of this approach.
advantage
- Simple
- No other packages required
- widely used
- completely free
shortcoming
- You need to translate independently
- You can translate it, this is wrong
- Text needs to be typed by hand
- Difficult to test
- difficult to maintain
Using third-party packages for translation
This method involves using a third-party API. I have implemented a service called DeepL that can process HTML text. To register, please visit https://www.deepl.com/. The service allows you to translate up to 500,000 characters for free.
You must add a valid credit card to register and create a subscription.
When you do, go to your profile.
Next, go to the API Keys tab and copy the API key.
Now, let’s write the code. You must also modify Program.cs
but you can copy it from a previous project.
This approach is best because it doesn’t require modifying the layout except in certain cases. We’ll add a drop-down menu like the previous example.
You must make another modification. DeepL may incorrectly handle special characters such as copyright signs. Solved by wrapping DIV containers. DeepL attempts to translate special character codes. For this case, the API provides a special attribute and category where you can disable translation. It will work fine in a DIV container. Another problem is that the ASP operation name should be different from the value. Otherwise it will not be translated. That’s why I changed the value from “Privacy” to “Policy”. Changing values is easier than performing the same operation through ASP operations.
Without style it would look uncute. You need to make inline blocks.
Add styles to the container.
.grid-container {
display: grid;
grid-template-columns: auto auto;
grid-gap: 5px;
width: fit-content;
white-space: nowrap;
}
Since we use the same mechanism to switch languages, you should add SetLanguage()
Method to HomeController
We used it in the previous example.
Before implementing the translation logic, we need to install two packages.
Translation requires this package:
dotnet add package DeepL.net --version 1.11.0
This package is required for processing HTML files:
dotnet add package HtmlAgilityPack --version 1.11.71
Once done, modify your existing method, Index(),
exist HomeController
. I’ll explain what happened there.
public async Task<IActionResult> Index()
{
var currentCulture = CultureInfo.CurrentCulture.Name;
var sourceLanguage = "en";
string targetLanguage;
var htmlContent = await RenderViewToStringAsync("Index");
switch (currentCulture)
{
case "en-US":
return Content(htmlContent, "text/html");
case "fr-FR":
targetLanguage = "fr";
break;
default:
return BadRequest("Unsupported language.");
}
var nodes = ExtractNodes(htmlContent);
var cacheKey = string.Join("_", nodes) + $"_{sourceLanguage}_{targetLanguage}";
if (!cache.TryGetValue(cacheKey, out string[]? texts))
{
var translator = new Translator("YourApiKey");
var text = await translator.TranslateTextAsync(nodes, sourceLanguage, targetLanguage);
texts = text.Select(x => x.Text).ToArray();
var cacheOptions = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
};
cache.Set(cacheKey, texts, cacheOptions);
}
for (var i = 0; i < nodes.Length; i++)
{
var oldNode = nodes.ElementAt(i);
if (texts == null) continue;
var newNode = texts.ElementAt(i);
htmlContent = htmlContent.Replace(oldNode, newNode);
}
return Content(htmlContent, "text/html");
}
But add another method we call Index()
method.
private async Task RenderViewToStringAsync(string viewName)
{
await using var writer = new StringWriter();
var viewResult = viewEngine.FindView(ControllerContext, viewName, isMainPage: true);
if (!viewResult.Success)
{
throw new FileNotFoundException($"View {viewName} not found");
}
ViewData["Title"] = "Home Page";
var viewContext = new ViewContext(
ControllerContext,
viewResult.View,
ViewData,
TempData,
writer,
new HtmlHelperOptions()
);
await viewResult.View.RenderAsync(viewContext);
return writer.ToString();
}
This method is required to parse the HTML code and return it as a string. Now, let’s go back to the Index() method. When we parse HTML, we check the current culture. If the culture is EN, we do nothing and return unmodified HTML content. We don’t need to translate to English because this language is used by default. If the culture is FR, we set the target language. Now you should add another method:
private static string[] ExtractNodes(string htmlContent)
{
var nodes = new List<string>();
var tags = new[] { "//title", "//ul", "//h1", "//p", "//footer" };
var htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(htmlContent);
foreach (var tag in tags)
{
var node = htmlDoc.DocumentNode.SelectSingleNode(tag);
if (node.InnerHtml != null)
{
nodes.Add(node.InnerHtml);
}
}
return nodes.ToArray();
}
This method extracts the specified block of HTML code. It works like a filter. For example, if we declare //ul
tag, then this method will return the contents of the block. Lean usage flow requires this approach. Since DeepL has character limitations, we should use fewer characters and translate only those blocks that are needed.
Another optimization is caching. When a page is translated, we don’t need to translate it again. You can use another cache provider if needed. If the cache is empty, the extracted code needs to be translated. You must use the API key obtained previously. Once you receive the translated HTML code, you’ll need to replace the code in the main HTML document.
Let’s look at this.
Now, let’s consider the advantages and disadvantages of this approach.
advantage
- No need to create resources yourself
- Artificial Intelligence Translation
- No need to maintain every translation
- No need to replace values by hand in layout
- Easy to test
- Save your time
shortcoming
- More complex implementation
- Subscription required
- Free subscription has limitations
- Limited number of languages
- Problems with special characters and ASP operations
in conclusion
I achieved the translation in two ways. The first one is more commonly used because it is simpler and completely free. The second option offers a free quote, but it’s not enough to translate all web pages without paying. A software engineer’s time is more expensive than a subscription to a translation service.
The source code is provided by association.
I hope this article was helpful to you and see you next time. Happy coding!