In this article, I'll show you how to translate a web application built using ASP.NET and discuss two implementation methods. The first method you are good at knowing uses static resources. The second method uses third-party API. We'll consider all the pros and cons of each technique. And sure, we'll be writing code.
Translation using static resources.
This method is widely used and often implemented in various projects. You don't need any third-party packages or APIs. First, modify the Program.cs
file and add supported cultures. Just add these rows.
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'll translate two layouts, we need to create folders. The hierarchy of folders should be the same as in the project. Please create the Resources folder and derived folders, as shown in the picture.
In each folder, create a resource file and add a needed translation.
For more practicality, I'll add a dropdown for select languages. Go to the _Layout.cshtml
and add this code.
<form asp-controller="Home" asp-action="SetLanguage" method="post">
<select name="culture" onchange="this.form.submit();">
<!option value="en-US" @(CultureInfo.CurrentCulture.Name == "en-US" ? "selected" : "en-US")>English</!option>
<!option value="fr-FR" @(CultureInfo.CurrentCulture.Name == "fr-FR" ? "selected" : "")>Français</!option>
</select>
</form>
We need to save cookies to keep the state. Then, after refreshing the page, your selected language will be kept. Go to the HomeController
and add the POST method for handling 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 ?? "/");
}
In the last steps, you need to modify layouts and replace the text you want to translate. Go to the Index.cshtml
file.
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1 class="display-4">@Localizer["Welcome"]</h1>
<p>@Localizer["LearnMore"]</p>
</div>
Also, go to the _Layout.cshtml
file and change the list for the navigation menu and footer.
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">@Localizer["Home"]</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">@Localizer["Privacy"]</a>
</li>
</ul>
<footer class="border-top footer text-muted">
<div class="container">
© 2024 - TranslatePageResource - <a asp-area="" asp-controller="Home" asp-action="Privacy">@Localizer["Privacy"]</a>
</div>
</footer>
Let's check this out. Default localization looks like that.
You'll see the translated page to the desired culture if you change language.
If you refresh the page, it will still have the French localization. If you restart the application, you will still see the French translation. Your browser is keeping a selected culture in cookies.
Now, let's consider the pros and cons of this approach.
Pros
- simplicity
- no needed other packages
- widely used
- completely free
Cons
- you need to translate independently
- you can translate it's wrong
- the text needs to be typed with hands
- difficult to test
- hard to maintain
Translation using a third-party package
This approach involves using a third-party API. I have implemented a service called DeepL that can process HTML text. To register, go to https://www.deepl.com/. This service allows you to translate up to 500000 characters without any payment.
You must add a valid credit card to register and create a subscription.
When you do it, go to your profile.
Next, go to the API keys tab and copy the API key.
Now, let's write code. You must also modify Program.cs
, but you can copy it from a previous project.
This approach is the best because it does not require modification layouts except in some cases. We will just add a dropdown like in the previous sample.
You must make another modification. The DeepL can incorrectly handle special characters like copyright signs. It is resolved by wrapping the DIV container. The DeepL tries to translate special char code. For this case, API provided a special property and class where you can prohibit translation. It'll work correctly in the DIV container. Another issue is that the ASP action name should differ from the value. Otherwise, it won't be translated. That's the reason why I changed the value from Privacy to Policy. Changing a value is more effortless than taking the same with an ASP action.
<footer class="border-top footer text-muted">
<div class="container grid-container">
<div class="notranslate" translate="no">©</div>
<div>
2024 - TranslatePageApi -
<a asp-area="" asp-controller="Home" asp-action="Privacy">Policy</a>
</div>
</div>
</footer>
It'll look not lovely without styles. You need to make inline blocks.
Add styles for 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 for switching languages, you should add the SetLanguage()
method to the HomeController
that we used in the previous sample.
Before implementing the translation logic, we need to install two packages.
This package is needed for translation:
dotnet add package DeepL.net --version 1.11.0
This package is required for handling HTML documents:
dotnet add package HtmlAgilityPack --version 1.11.71
After you do it, please modify your existing method, Index(),
in HomeController
. I'll explain what's going on 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 that we call into the Index()
method.
private async Task<string> 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 needed to parse HTML code and return it as a string. Now, let's go back to the Index() method. When we parsed HTML, we checked the current culture. If the culture is EN, we don't do anything and return unmodified HTML content. We don't need to translate to English since 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 indicated blocks of HTML code. It works like a filter. For instance, if we declare the //ul
tag, then the method will return the contents of this block. This method is needed for lean-use traffic. Since DeepL has limitations by chars, we should decrease using chars and translate only those needed blocks.
Another optimization is caching. When the page was translated, we didn't need to translate it again. You can use another cache provider if you want. If the cache is empty, you need to translate the extracted code. You must use the API key that you got earlier. You need to replace the code in the main HTML document as soon as you receive the translated HTML code.
Let's check this out.
Now, let's consider the pros and cons of this way.
Pros
- no need to create resources with hands
- AI translation
- no need to maintain each translation
- no need to replace values with hands in layouts
- easy to test
- saves your time
Cons
- more complicated implementation
- needed subscription
- free subscription has limitations
- limited quantity of languages
- have issues with special chars and ASP actions
Conclusions
I implemented translation in two ways. The first is more used because it is more straightforward and completely free. The second option offers a free quote, but it is not enough to translate all web pages without paying. The time of a software engineer is more expensive than the cost of a translation service subscription.
Source code by the LINK.
I hope this article was helpful to you, and see you next time. Happy coding!
Top comments (0)