DEV Community

Cover image for Web application translation in two ways.
Serhii Korol
Serhii Korol

Posted on

Web application translation in two ways.

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);
Enter fullscreen mode Exit fullscreen mode

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.

Ifolders

In each folder, create a resource file and add a needed translation.

resources

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>
Enter fullscreen mode Exit fullscreen mode

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 ?? "/");
    }
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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">
        &copy; 2024 - TranslatePageResource - <a asp-area="" asp-controller="Home" asp-action="Privacy">@Localizer["Privacy"]</a>
    </div>
</footer>
Enter fullscreen mode Exit fullscreen mode

Let's check this out. Default localization looks like that.

en

You'll see the translated page to the desired culture if you change language.

fr

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.

plan

You must add a valid credit card to register and create a subscription.

register form

When you do it, go to your profile.

profile

Next, go to the API keys tab and copy the API key.

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">&copy;</div> 
        <div>
            2024 - TranslatePageApi -
            <a asp-area="" asp-controller="Home" asp-action="Privacy">Policy</a>
        </div>
    </div>
</footer>
Enter fullscreen mode Exit fullscreen mode

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;
} 
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This package is required for handling HTML documents:

dotnet add package HtmlAgilityPack --version 1.11.71
Enter fullscreen mode Exit fullscreen mode

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");
    }
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
Enter fullscreen mode Exit fullscreen mode

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.

en version

fr version

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!

Buy Me A Beer

Top comments (0)